Overflow menus:ActionViewをツールバーへ表示(actionViewClass属性)

投稿日:  更新日:

ツールバー(アクションバー)へActionViewを表示させる方法を紹介します。

スポンサーリンク

ActionViewとは

メニューのアイテムを選択した時、特定のViewをツールバー(アクションバー)へ表示させることが可能です。

ツールバーに表示されたViewのことをActionViewといいます。

ActionViewとは

図はActionViewとしてSeekBarを指定した場合です。

スポンサーリンク

ActionViewの実装

ActionViewはitem要素のactionViewClass属性へ、表示したいViewのクラス名を指定します。
※showAsAction属性のcollapseActionViewは「ActionViewを折り畳む・折り畳まない」で説明します。ここでは触れません。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_tone"
        android:title="Tone"
        android:icon="@drawable/baseline_invert_colors_24"
        app:showAsAction="always|collapseActionView"
        app:actionViewClass="androidx.appcompat.widget.AppCompatSeekBar" />
    <item
        android:id="@+id/menu_file"
        android:title="File" />
    <item
        android:id="@+id/menu_edit"
        android:title="Edit" />
    <item
        android:id="@+id/menu_help"
        android:title="Help" />
</menu>

後は「Overflow menusの実装」で述べた通り、メニューの定義をActivity#onCrteateOptionsMenuでMenuオブジェクトへ組み込めば、ActionViewが表示されるようになります。

ActionViewのインスタンスはMenuItem#actionViewで取り出せます。インスタンスはViewなのでキャストが必要です。

class MainActivity : AppCompatActivity() {
    ...
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        val inflater: MenuInflater = menuInflater
        inflater.inflate(R.menu.menu_items, menu)
		
        val _toneItem = menu.findItem(R.id.menu_tone)

        // SeekBarのツマミを操作
        val _seekBar = _toneItem.actionView as AppCompatSeekBar
        val _seekBarListener = object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(
                bar: SeekBar, progress: Int, fromUser: Boolean) {
                // 背景の明暗を変更
            }
            override fun onStartTrackingTouch(bar: SeekBar) { }
            override fun onStopTrackingTouch(bar: SeekBar) { }
        }
        _seekBar.setOnSeekBarChangeListener(_seekBarListener)
        _seekBar.max = 255
        _seekBar.progress = 255

        return true
    }
	...
}

もしも、ActionViewの動作を定義したければ、取り出したインスタンスに対して行います。サンプルはSeekBarのツマミに連動して、背景の明暗(白⇔黒)を変化させる動作を定義しました。

気付かれたと思いますが…

このサンプルを実行した結果を見ると、SeekBarの横幅が狭くなっています。操作性を考慮すれば幅を広げたくなります。

これはActionViewがコンテンツの原形サイズで表示されるためです。

スポンサーリンク

ActionViewをツールバー幅で表示

ActionViewはコンテンツの原形サイズで表示されます。これをツールバー幅に広げて表示する方法を紹介します。

方法は2つあり、どちらもカスタムViewを作成して、そのカスタムViewをActionViewに指定するやり方です。

なぜ原形サイズで表示される?

ツールバーはViewGroupを継承したコンテナタイプのViewです。

メニューのアイテムが選択された時、ActionViewはツールバーへViewGroup#addView( )されて、子Viewになります。

これがActionViewを表示する動作です。

...
import androidx.appcompat.view.CollapsibleActionView;
...

public class Toolbar extends ViewGroup {
    ...
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }
    ...
    private class ExpandedActionViewMenuPresenter implements MenuPresenter {
        ...
        @Override
        public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
            ...
            mExpandedActionView = item.getActionView();
            mCurrentExpandedItem = item;
            ViewParent expandedActionParent = mExpandedActionView.getParent();
            if (expandedActionParent != Toolbar.this) {
                if (expandedActionParent instanceof ViewGroup) {
                    ((ViewGroup) expandedActionParent).removeView(mExpandedActionView);
                }
                final LayoutParams lp = generateDefaultLayoutParams();
                lp.gravity = GravityCompat.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK);
                lp.mViewType = LayoutParams.EXPANDED;
                mExpandedActionView.setLayoutParams(lp);
                addView(mExpandedActionView);
            }

            removeChildrenForExpandedActionView();
            requestLayout();
            item.setActionViewExpanded(true);

            if (mExpandedActionView instanceof CollapsibleActionView) {
                ((CollapsibleActionView) mExpandedActionView).onActionViewExpanded();
            }

            return true;
        }
		...
    }
	...
}

上記のソースコードを見ると、ActionViewのLayoutParamsがToolbar#generateDefaultLayoutParams( )で作成されていることが分かります。

作成されるLayoutParamsのwidth/height属性はWRAP_CONTENTです。これがコンテンツの原形サイズになる理由です。

方法1:MATCH_PARENTに変更

CollapsibleActionViewはインターフェイスです。

ActionViewに実装されていると、ツールバーへaddViewした後にCollapsibleActionView#onActionViewExpanded( )が呼び出されます。

このonActionViewExpanded( )内でLayoutParamsのwidthをMATCH_PARENTに書き換えれば、ActionViewの幅が変更できます。

そのために、CollapsibleActionViewインターフェイスを実装したカスタムViewを作成して、ActionViewに指定します。

class ActionSeekBar : AppCompatSeekBar, CollapsibleActionView {
    constructor(context: Context)
            : super(context)
    constructor(context: Context, attrs: AttributeSet?)
            : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
            : super(context, attrs, defStyleAttr)

    override fun onActionViewExpanded() {
        val _lp = (layoutParams as Toolbar.LayoutParams).apply {
            width = Toolbar.LayoutParams.MATCH_PARENT
        }
        layoutParams = _lp
    }

    override fun onActionViewCollapsed() { }
}
    <item
        android:id="@+id/menu_tone"
        android:title="Tone"
        android:icon="@drawable/baseline_invert_colors_24"
        app:showAsAction="always|collapseActionView"
        app:actionViewClass="パッケージ名.ActionSeekBar" />
【注意:CollapsibleActionViewは2つある】

Toolbarは(1)を参照しているので、方法1を採用する場合はカスタムViewで発生する「非推奨」の警告を無視するしかありません。

  (1)androidx.appcompat.view.CollapsibleActionView
    androidx.appcompat:appcompat:1.2.0で非推奨(Deprecated扱い)
    Toolbarで参照(importされている)

  (2)android.view.CollapsibleActionView
    リリースノートに「(1)の代わりに使用する」と記述あり

方法2:onMeasure( )で変更

ViewのサイズはView#onMeasure( )内で決められます。その様子を示したのが下図です。
※「親:Toolbar、子:SeekBar」に対応

View#onMeasure( )の動作概要

Viewツリーの全ての親Viewと子View間で[1]~[3]の処理が行われます。その中にToolbarとActionView間も含まれます。

[2]の「サイズの割り当て指示」で与えられるwidthMeasureSpecならびにheightMeasureSpecは、int型でありながらMSBの2ビットがmodeという特別な意味を持っています。

MeasureSpecのビット構成

modeサイズの指示サイズの算出
UNSPECIFIED
(0x00000000)
親Viewのサイズが決まっていない場合です。
サイズの指示はありません。
好きなサイズを自身のサイズにします。
EXACTLY
(0x40000000)
要望がMATCH_PARENTや固定サイズ(例:100dp)の場合です。
親Viewによって確定されたサイズが指示されます。
MATCH_PARENTであれば、配置が可能な最大サイズです。
固定サイズであれば、そのサイズです。
親Viewによって確定されたサイズを自身のサイズにします。
AT_MOST
(0x80000000)
要望がWRAP_CONTENTの場合です。
親Viewによって配置が可能な最大サイズが指示されます。
子Viewのサイズは最大サイズ以下にする必要があります。
コンテンツのサイズを算出して、
「≦最大サイズ」であれば、コンテンツサイズを自身のサイズにします。
「>最大サイズ」であれば、最大サイズを自身のサイズにします。
サイズにmin、max、margin、paddingの影響を考慮するかどうかはコンテンツ次第です。
※mode:View.MeasureSpec.XXX
※( )内は値
※コンテンツ:TextViewであれば文字列、ImageViewであれば画像のこと

このmodeをAT_MOST⇒EXACTRYに置き換えれば、子ViewがMATCH_PARENTを要望しているとonMesure( )で判断されて、ActionViewの幅が変更できます。

そのために、onMeasure( )を上書きしたカスタムViewを作成して、ActionViewに指定します。

class ActionSeekBar : AppCompatSeekBar {
    constructor(context: Context)
            : super(context)
    constructor(context: Context, attrs: AttributeSet?)
            : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int)
            : super(context, attrs, defStyleAttr)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var _widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val _width = MeasureSpec.getSize(widthMeasureSpec)
        when(_widthMode) {
            MeasureSpec.AT_MOST -> {
                _widthMode = MeasureSpec.EXACTLY
            }
            else -> { }
        }
        super.onMeasure(  // サイズの算出はスーパークラスに任せる
            MeasureSpec.makeMeasureSpec(_width, _widthMode), heightMeasureSpec)
    }
}
    <item
        android:id="@+id/menu_tone"
        android:title="Tone"
        android:icon="@drawable/baseline_invert_colors_24"
        app:showAsAction="always|collapseActionView"
        app:actionViewClass="パッケージ名.ActionSeekBar" />
スポンサーリンク

ActionViewを折り畳む・折り畳まない

item要素のshowAsAction属性にcollapseActionViewを指定すると、ActionViewはメニューのアイテムを選択することでツールバーに展開されます。

これをActionViewがアイテムへ格納されたように見えることから「折り畳む」と表現しています。

「ActionViewの実装」で紹介したサンプルは「折り畳む」です。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_tone"
        android:title="Tone"
        android:icon="@drawable/baseline_invert_colors_24"
        app:showAsAction="always|collapseActionView"
        app:actionViewClass="androidx.appcompat.widget.AppCompatSeekBar" />
    ...
</menu>

item要素のshowAsAction属性にcollapseActionViewを指定しないと、ActionViewは初めからツールバーに展開された状態になります。

これを「折り畳む」に反して「折り畳まない」と表現しています。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_tone"
        android:title="Tone"
        android:icon="@drawable/baseline_invert_colors_24"
        app:showAsAction="always"
        app:actionViewClass="androidx.appcompat.widget.AppCompatSeekBar" />
    ...
</menu>

ActionViewを折り畳まない

スポンサーリンク

ActionViewの展開・格納イベント処理

MenuItem.OnActionExpandListenerインターフェイスをMenuItemに設定することで、ActionViewが展開・格納された時のイベントをコールバックで受け取ることができます。

class MainActivity : AppCompatActivity() {
    ...
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        val inflater: MenuInflater = menuInflater
        inflater.inflate(R.menu.menu_items, menu)

        val _toneItem = menu.findItem(R.id.menu_tone)
        val _expandListener = object : MenuItem.OnActionExpandListener {
            override fun onMenuItemActionExpand(item: MenuItem): Boolean {
                // ActionViewを展開した時の処理
                return true // falseにすると展開出来ない
            }
            override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
                // ActionViewを格納した時の処理
                return true // falseにすると格納出来ない
            }
        }
        _toneItem.setOnActionExpandListener(_expandListener)
		...

        return true
    }
	...
}

イベントの発火タイミングは図のようになっています。

ActionViewの展開・格納イベントタイミング

MenuItem.OnActionExpandListenerインターフェイスのコールバックはaddViewとremoveViewの前に発火します。その点に注意してください。

スポンサーリンク

関連記事:

Material Design ComponentsにMenusというカテゴリがあります。 「Menus(メニュー)」なので、言葉のとおり、選択する項目を一覧表示する機能です。 Menusはメニューの表現方法によって、幾つかの種類があります。 このMenusの種類と特徴を紹介します。 ...
Overflow menusの実装方法を紹介します。 ...
Overflow menusはメニューのアイテムをツールバー(アクションバー)へ表示することが出来ます。 その方法を紹介します。 ...
ツールバー(アクションバー)へActionLayoutを表示させる方法を紹介します。 ...
スポンサーリンク