卡片滚动条

借助 Glass,您可以与卡片进行丰富的互动,例如滚动和动画。

Activity 中的滚动卡片

Glass 显示屏和触控板非常适合用来显示可滑动的卡片,例如 Glass 时间轴。如果您要构建 activity,可以使用 CardScrollView widget 创建相同类型的效果。

  1. 实现 CardScrollAdapter,以便为 CardScrollView 提供卡片。您可以自行构建标准视图层次结构,也可以使用 CardBuilder 类。
  2. 创建一个使用 CardScrollAdapter 作为卡片供应商的 CardScrollView
  3. 将 activity 的内容视图设置为 CardScrollView 或在布局中显示 CardScrollView

下面是一个简单的实现,可以滚动显示三张卡片:

public class CardScrollActivity extends Activity {

    private List<CardBuilder> mCards;
    private CardScrollView mCardScrollView;
    private ExampleCardScrollAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        createCards();

        mCardScrollView = new CardScrollView(this);
        mAdapter = new ExampleCardScrollAdapter();
        mCardScrollView.setAdapter(mAdapter);
        mCardScrollView.activate();
        setContentView(mCardScrollView);
    }

    private void createCards() {
        mCards = new ArrayList<CardBuilder>();

        mCards.add(new CardBuilder(this, CardBuilder.Layout.TEXT)
                .setText("This card has a footer.")
                .setFootnote("I'm the footer!"));

        mCards.add(new CardBuilder(this, CardBuilder.Layout.CAPTION)
                .setText("This card has a puppy background image.")
                .setFootnote("How can you resist?")
                .addImage(R.drawable.puppy_bg));

        mCards.add(new CardBuilder(this, CardBuilder.Layout.COLUMNS)
                .setText("This card has a mosaic of puppies.")
                .setFootnote("Aren't they precious?")
                .addImage(R.drawable.puppy_small_1);
                .addImage(R.drawable.puppy_small_2);
                .addImage(R.drawable.puppy_small_3));
    }

    private class ExampleCardScrollAdapter extends CardScrollAdapter {

        @Override
        public int getPosition(Object item) {
            return mCards.indexOf(item);
        }

        @Override
        public int getCount() {
            return mCards.size();
        }

        @Override
        public Object getItem(int position) {
            return mCards.get(position);
        }

        @Override
        public int getViewTypeCount() {
            return CardBuilder.getViewTypeCount();
        }

        @Override
        public int getItemViewType(int position){
            return mCards.get(position).getItemViewType();
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return mCards.get(position).getView(convertView, parent);
        }
    }
}

与滚动卡片互动

由于 CardScrollView 扩展了 AdapterView,因此您可以实现标准的 Android 监听器。

  1. CardScrollView 调用继承的 setOnItemClickListener()
  2. 为点按事件实现 onItemClick() 处理程序。

下面是前面示例的扩展,当您点按卡片时,点按音效会播放:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        setupClickListener();
        setContentView(mCardScrollView);
    }

    private void setupClickListener() {
        mCardScrollView.setOnItemClickListener(new AdapterView.OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
                am.playSoundEffect(Sounds.TAP);
            }
        });
    }

为滚动卡片添加动画效果

滚动卡片有三种动画:导航、插入和删除。

  1. 在卡片组内的指定位置对卡片实现插入或删除操作。
  2. 调用 animate() 并使用 CardScrollView.Animation 枚举中的值。
  3. 为了打造更流畅的动画,请移除对 notifyDataSetChanged() 的任何引用。animate() 方法可处理数据集视图的更新。

    private class ExampleCardScrollAdapter extends CardScrollAdapter {
        ...
    
        // Inserts a card into the adapter, without notifying.
        public void insertCardWithoutNotification(int position, CardBuilder card) {
            mCards.add(position, card);
        }
    }
    
    private void insertNewCard(int position, CardBuilder card) {
        // Insert new card in the adapter, but don't call
        // notifyDataSetChanged() yet. Instead, request proper animation
        // to inserted card from card scroller, which will notify the
        // adapter at the right time during the animation.
        mAdapter.insertCardWithoutNotification(position, card);
        mCardScrollView.animate(position, CardScrollView.Animation.INSERTION);
    }
    

有关滚动卡片的性能和实现提示

在创建卡片滚动条时,请注意以下设计和性能影响。

卡片生命周期

为了提高性能,CardScrollView 只会加载 CardScrollAdapter 提供的部分卡片(通常是用户可见的卡片,等等)。因此,卡片可能处于以下四种常规状态之一:

  • 已分离 - 卡片滚动视图目前不需要此卡片。 如果卡片先前已挂接,然后被分离,卡片的 onDetachedToWindow() 方法会通知您。
  • Attached - 卡片滚动视图通过 getView() 从适配器请求卡片,因为卡片即将被“激活”。发生这种情况时,该卡片的 onAttachedToWindow() 方法会通知您。
  • 已激活 - 卡片对用户部分可见,但卡片滚动视图尚未“选择”向用户显示的卡片。在这种情况下,'isActivated()' 方法会返回 true
  • 已选择 - 卡片占用了用户整个屏幕。调用 getSelectedView() 将返回当前选定的卡。在这种情况下,isSelected() 方法会返回 true。

如果您要为卡片视图添加动画效果或执行其他操作,请启动和停止 onAttachedToWindow()onDetachedToWindow() 中的操作以节省资源。

卡片回收

从卡连接到已分离的卡时,与卡关联的视图对象可以被正在挂接的卡回收和使用。使用更新的信息回收视图比创建新视图效率更高。

为了利用卡片回收利用,请实现 CardScrollAdapter 类的 getItemViewType()getViewTypeCount()getView() 方法。然后,您可以使用 CardBuilder 类中的某些便捷方法在 CardScrollAdapter 中实现回收,如以下示例所示:

private List<CardBuilder> mCards;
...
/**
 * Returns the number of view types for the CardBuilder class. The
 * CardBuilder class has a convenience method that returns this value for
 * you.
 */
@Override
public int getViewTypeCount() {
    return CardBuilder.getViewTypeCount();
}

/**
 * Returns the view type of this card, so the system can figure out
 * if it can be recycled. The CardBuilder.getItemViewType() method
 * returns it's own type.
 */
@Override
public int getItemViewType(int position){
    return mCards.get(position).getItemViewType();
}

/**
 * When requesting a card from the adapter, recycle the view if possible.
 * The CardBuilder.getView() method automatically recycles the convertView
 * it receives, if possible, or creates a new view if convertView is null or
 * of the wrong type.
 */
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    return  mCards.get(position).getView(convertView, parent);
}

实现稳定的卡 ID

选择某个卡片并向用户显示后,您可能不想对底层适配器进行更改以影响用户当时看到的卡片。例如,如果用户正在查看所选卡片,而该卡片的左侧已移除,则正在查看的卡片可能会向左移动,因为默认情况下,CardScrollAdapter 会在发生更改时将 ID 重新分配给基础数据集。

如果逻辑上合理地分配了卡片的唯一 ID,您可以在底层数据集中保持一致的 ID 以防止出现上述问题。为此,请替换 hasStableIds() 并返回 true。这会向系统指定 CardScrollAdapter 会在数据集更改之间保持稳定的 ID。此外,还应实现 getItemId(),以便为适配器中的卡片返回适当的唯一 ID。默认实现会返回适配器在适配器中的位置索引,这本身并不稳定。

CardScrollAdapter 为空

当您为适配器设置空数据集时,默认视图是黑屏。如果您希望在这些情况下显示其他视图,请勿使用 setEmptyView()。 而应在 CardScrollAdapter 中创建一张卡片。

水平拖动反馈

许多内置 Glass 的沉浸式功能在向后滑动和向前滑动时都不会执行任何操作。例如,在拍照后滑动时可以看到此反馈。

如果您的沉浸感不使用水平滑动手势来执行应用专用功能,请通过将布局封装在包含一张卡片的 CardScrollView 内来提供此拖动效果。

  1. 将以下辅助类复制到您的项目中:

    public class TuggableView extends CardScrollView {
    
        private final View mContentView;
    
        /**
         * Initializes a TuggableView that uses the specified layout
         * resource for its user interface.
         */
        public TuggableView(Context context, int layoutResId) {
            this(context, LayoutInflater.from(context)
                    .inflate(layoutResId, null));
        }
    
        /**
         * Initializes a TuggableView that uses the specified view
         * for its user interface.
         */
        public TuggableView(Context context, View view) {
            super(context);
    
            mContentView = view;
            setAdapter(new SingleCardAdapter());
            activate();
        }
    
        /**
         * Overridden to return false so that all motion events still
         * bubble up to the activity's onGenericMotionEvent() method after
         * they are handled by the card scroller. This allows the activity
         * to handle TAP gestures using a GestureDetector instead of the
         * card scroller's OnItemClickedListener.
         */
        @Override
        protected boolean dispatchGenericFocusedEvent(MotionEvent event) {
            super.dispatchGenericFocusedEvent(event);
            return false;
        }
    
        /** Holds the single "card" inside the card scroll view. */
        private class SingleCardAdapter extends CardScrollAdapter {
    
            @Override
            public int getPosition(Object item) {
                return 0;
            }
    
            @Override
            public int getCount() {
                return 1;
            }
    
            @Override
            public Object getItem(int position) {
                return mContentView;
            }
    
            @Override
            public View getView(int position, View recycleView,
                    ViewGroup parent) {
                return mContentView;
            }
        }
    }
    
  2. 修改 activity 中的 onCreate 方法,以显示包含布局的 CardScrollView

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        // was: setContentView(R.layout.main_activity);
        setContentView(new TuggableView(this, R.layout.main_activity));
    }