カード スクローラー

Glass を使用すると、スクロールやアニメーションなど、カードとの豊かなインタラクションを構築できます。

アクティビティでカードをスクロールする

Glass ディスプレイとタッチパッドは、Glass のタイムラインのようにスワイプ可能なカードを表示する場合に適しています。アクティビティを作成する場合は、CardScrollView ウィジェットを使用して同じタイプの効果を作成できます。

  1. CardScrollAdapter を実装して、CardScrollView にカードを提供します。標準のビュー階層を独自に構築することも、CardBuilder クラスを使用することもできます。
  2. CardScrollAdapter をカードのサプライヤーとして使用する CardScrollView を作成します。
  3. アクティビティのコンテンツ ビューを 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);
        }
    }
}

スクロール カードの操作

CardScrollViewAdapterView を拡張するため、標準の 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);
            }
        });
    }

スクロール カードのアニメーション化

スクロール カードでは、ナビゲーション、挿入、削除の 3 つのアニメーションを使用できます。

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

スクロール カードのパフォーマンスと実装のヒント

カード スクローラーを作成する際は、次の設計とパフォーマンスへの影響に留意してください。

カードのライフサイクル

パフォーマンスを向上させるために、CardScrollViewCardScrollAdapter が提供するカードのサブセット(通常はユーザーに表示されるカードなど)のみを読み込みます。そのため、カードは以下の 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 内にレイアウトをラップすることで、このような動作を実現します。

  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. アクティビティの 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));
    }