View.isSelected/isActivatedについて調べたこと

こんにちは、 id:numanuma08 です。 最近、recyclerview-selectionを利用する機会がありました。recyclerview-selectionのドキュメントには、Viewの選択状態はsetActivatedを使って変更するべきと書かれています。

In Adapter#onBindViewHolder, set the "activated" status on view. Note that the status should be "activated" not "selected". See View.html#setActivated for details.

しかし、Viewには似たような用途っぽいメソッドにsetSelectedも用意されています。なぜsetActivatedを使うべきなのか調べました。

リファレンスを参照する

何はなくともリファレンスを参照します。まずはsetSelectedから。

Changes the selection state of this view. A view can be selected or not. Note that selection is not the same as focus. Views are typically selected in the context of an AdapterView like ListView or GridView; the selected view is the view that is highlighted.

ビューの選択状態を変更します。ビューを選択することもしないこともできます。選択とフォーカスは同じでないことに注意しましょう。ビューは典型的にはListViewやGridViewといったAdapterViewのコンテキストの中で選択されます。

なんだか、選択状態を表現するのにsetSelectedでも良さそうな雰囲気ですが。次はsetActivatedのドキュメントを読みます。

Changes the activated state of this view. A view can be activated or not. Note that activation is not the same as selection. Selection is a transient property, representing the view (hierarchy) the user is currently interacting with. Activation is a longer-term state that the user can move views in and out of. For example, in a list view with single or multiple selection enabled, the views in the current selection set are activated. (Um, yeah, we are deeply sorry about the terminology here.) The activated state is propagated down to children of the view it is set on.

ビューの有効状態を変更します。ビューを有効化することもしないこともできます。有効化は選択と違うことに気をつけてください。選択は一時的なプロパティで、現在ユーザーが操作しているビュー(階層)を表します。有効化は長期的な状態でユーザーがビューを出し入れ可能です。例えば単一または複数選択が可能なリストビューでは現在選択されているセットのビューが有効です。(あー、専門用語で本当にごめんなさい)。有効状態は子どもの階層のビューに伝達されます。

謝られてしまいました。ただ、どうやらリスト中の要素を選んで操作する場合、setActivatedが適しているように思えます。一時的な状態、長期的な状態という表現から推察するに次のような使い分けができるかと思います。

  • isSelected: リスト中の要素を長押しなどで選択している状態
  • isActivated: リスト中の要素をタップしてチェックボックスがチェックされた状態

また、ビュー階層についても言及がありsetSelectedは単一の階層でsetActivatedは子どもの階層も含みます。やはり、リスト要素タップ後にチェックボックスをチェックしたり背景を変えるなどはsetActivatedで設定された値を参照するのが正しそうです。

ソースコードを眺める

リファレンスを参照した結果どうやらrecyclerview-selectionでsetAcitvatedを使うのが正しそうと分かりました。ここで、実際の実装がどうなっているかView.javaのソースコードを眺めて確認します。まずはsetSelectedから。

public void setSelected(boolean selected) {
    //noinspection DoubleNegation
    if (((mPrivateFlags & PFLAG_SELECTED) != 0) != selected) {
        mPrivateFlags = (mPrivateFlags & ~PFLAG_SELECTED) | (selected ? PFLAG_SELECTED : 0);
        if (!selected) resetPressedState();
        invalidate(true);
        refreshDrawableState();
        dispatchSetSelected(selected);
        if (selected) {
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
        } else {
            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
        }
    }
}

特にこれといった処理は行われていないように思えます。mPrivateFlagsというViewの様々な状態を格納するプロパティの更新を行ったあと、invalidateで描画の更新、その後はアクセシビリティ関連の通知を発行しています。

次はsetActivatedを見ます。

public void setActivated(boolean activated) {
    //noinspection DoubleNegation
    if (((mPrivateFlags & PFLAG_ACTIVATED) != 0) != activated) {
        mPrivateFlags = (mPrivateFlags & ~PFLAG_ACTIVATED) | (activated ? PFLAG_ACTIVATED : 0);
        invalidate(true);
        refreshDrawableState();
        dispatchSetActivated(activated);
    }
}

こちらも特にこれと言った処理はありません。mPrivateFlagsの更新と描画の更新です。

イマイチこれと言った差分もないので次はViewGroupを見ます。ViewGroupではdispatchSetSelecteddispatchSetActivatedを実装してselected/activatedが変更されたときの処理が実装されていました。

@Override
public void dispatchSetSelected(boolean selected) {
    final View[] children = mChildren;
    final int count = mChildrenCount;
    for (int i = 0; i < count; i++) {
        children[i].setSelected(selected);
    }
}

@Override
public void dispatchSetActivated(boolean activated) {
    final View[] children = mChildren;
    final int count = mChildrenCount;
    for (int i = 0; i < count; i++) {
        children[i].setActivated(activated);
    }
}

同じやないか・・・。selected/activatedともにViewGroupで管理している子ビューにその変更が通知されています。ドキュメントの内容と違う・・・。

もうちょっと具体的な実装を探すためにTextViewの実装を見てみます。TextViewsetSelectedが実装されていますが、setActivatedは実装されていませんでした。

public void setSelected(boolean selected) {
    boolean wasSelected = isSelected();

    super.setSelected(selected);

    if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
        if (selected) {
            startMarquee();
        } else {
            stopMarquee();
        }
    }
}

marqueeするんかい。ellipsizemarqueeが設定されておりかつ一文が一行に収まらないときテキストが横スクロールします。

Android SDK内の実装ではこれと言ったselected/activatedの使い分けがはっきりとしませんでした。

他にもxmlのリソースファイルも調べたのですが、SDK内で定義されているクラスや仕組みの中にactivatedを利用したものはありませんでした。

まとめ

recyclerview-selectionで利用するためView.setSelectedView.setActivatedの違いを調べました。ドキュメントを読むとsetActivatedを利用するのが正しいそうですが、SDK内部の実装は特にこれと言った使い分けはされてないようでした。したがって実装上はsetActivatedsetSelectedのどちらを利用しても大きな問題は起こらないかもしれません。しかし、ドキュメントにあるように選択と有効化は別のものと定義されています。そのため、利用するデバイスや環境によっては動作に違いが生じるかもしれません。しっかりと選択、有効化の違いを意識して実装を行いたいです。