卡片滚动条

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

在活动中滚动卡片

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));
    }