借助 Glass,您可以与卡片进行丰富的互动,例如滚动和动画。
Activity 中的滚动卡片
Glass 显示屏和触控板非常适合用来显示可滑动的卡片,例如 Glass 时间轴。如果您要构建 activity,可以使用 CardScrollView
widget 创建相同类型的效果。
- 实现
CardScrollAdapter
,以便为CardScrollView
提供卡片。您可以自行构建标准视图层次结构,也可以使用CardBuilder
类。 - 创建一个使用
CardScrollAdapter
作为卡片供应商的CardScrollView
。 - 将 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 监听器。
- 对
CardScrollView
调用继承的setOnItemClickListener()
。 - 为点按事件实现
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);
}
});
}
为滚动卡片添加动画效果
滚动卡片有三种动画:导航、插入和删除。
- 在卡片组内的指定位置对卡片实现插入或删除操作。
- 调用
animate()
并使用CardScrollView.Animation
枚举中的值。 为了打造更流畅的动画,请移除对
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
内来提供此拖动效果。
将以下辅助类复制到您的项目中:
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; } } }
修改 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)); }