Glass を使用すると、スクロールやアニメーションなど、カードとの豊かなインタラクションを構築できます。
アクティビティでカードをスクロールする
Glass ディスプレイとタッチパッドは、Glass のタイムラインのようにスワイプ可能なカードを表示する場合に適しています。アクティビティを作成する場合は、CardScrollView
ウィジェットを使用して同じタイプの効果を作成できます。
CardScrollAdapter
を実装して、CardScrollView
にカードを提供します。標準のビュー階層を独自に構築することも、CardBuilder
クラスを使用することもできます。CardScrollAdapter
をカードのサプライヤーとして使用するCardScrollView
を作成します。- アクティビティのコンテンツ ビューを
CardScrollView
に設定するか、CardScrollView
をレイアウトに表示します。
3 つのカードをスクロールする簡単な実装例を次に示します。
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);
}
});
}
スクロール カードのアニメーション化
スクロール カードでは、ナビゲーション、挿入、削除の 3 つのアニメーションを使用できます。
- カードセットの指定された位置にカードに挿入操作を実装します。
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
が提供するカードのサブセット(通常はユーザーに表示されるカードなど)のみを読み込みます。そのため、カードは以下の 4 つの一般的な状態のいずれかになります。
- 接続解除 - 現時点では、カードのスクロール ビューにこのカードは必要ありません。以前にカードを接続してから取り外した場合は、カードの
onDetachedToWindow()
メソッドによって通知されます。 - Attached - カードが「有効化」に近づいているため、カードのスクロール ビューにより、アダプターからカードが
getView()
でリクエストされます。その場合は、カードのonAttachedToWindow()
メソッドによって通知されます。 - Activated - カードはユーザーに表示されますが、カードのスクロール ビューではユーザーに表示するようカードが「選択」されていません。この場合、'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
でカードを 1 枚作成してください。
水平方向のフィードバックに関するフィードバック
Glass に組み込まれた多くの機能では、前後にスワイプしてもアクションが実行されない場合に「タグの切り替え」に関するフィードバックを提供します。たとえば、写真の撮影後にスワイプすると、このフィードバックが表示されます。
没入型アプリで水平方向のスワイプ操作を使用せず、アプリ固有の機能を実行する場合は、1 つのカードが配置されている 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; } } }
アクティビティの
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)); }