ツールバー(アクションバー)へActionLayoutを表示させる方法を紹介します。
目次
ActionLayoutとは
メニューのアイテムを選択した時、特定のLayoutをツールバー(アクションバー)へ表示させることが可能です。
LayoutはView階層をxmlで定義したレイアウトリソース(res/layout/XXX.xml)のことです。
ツールバーに表示されたLayoutのことをActionLayoutといいます。
図はトップViewにLinearLayoutを配置したActionLayoutの場合です。LinearLayoutは3つの子View(ImageButton+SeekBar+ImageButton)を持ちます。
ActionLayoutの実装
ActionLayoutはitem要素のactionLayout属性へ、表示したいLayoutのリソース名を指定します。
※showAsAction属性のcollapseActionViewは「ActionLayoutを折り畳む・折り畳まない」で説明します。ここでは触れません。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageButton android:id="@+id/imbDark" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/transparent" app:srcCompat="@drawable/baseline_remove_circle_outline_24" /> <SeekBar android:id="@+id/skbTone" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_weight="1" android:minWidth="50dp" android:max="255" android:progress="255" /> <ImageButton android:id="@+id/imbLight" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/transparent" app:srcCompat="@drawable/baseline_add_circle_outline_24" /> </LinearLayout>
<?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:actionLayout="@layout/action_layout" /> <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オブジェクトへ組み込めば、ActionLayoutが表示されるようになります。
ActionLayoutのインスタンスは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) val _actLay = _toneItem.actionView as LinearLayout // SeekBarのツマミで操作 val _seekBar = _actLay.findViewById<AppCompatSeekBar>(R.id.skbTone) 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) // ボダンで操作 _actLay.findViewById<ImageButton>(R.id.imbLight).setOnClickListener { // 背景を明るくする } _actLay.findViewById<ImageButton>(R.id.imbDark).setOnClickListener { // 背景を暗くする } return true } ... }
もしも、ActionLayoutの動作を定義したければ、取り出したインスタンスに対して行います。サンプルはSeekBarのツマミとImageButtonのクリックに連動して、背景の明暗(白⇔黒)を変化させる動作を定義しました。
気付かれたと思いますが…
ActionLayoutのトップViewをlayout_width=”match_parent”と指定し、幅いっぱいに表示されることを期待していますが、このサンプルを実行した結果を見ると、ActionLayoutの横幅が狭くなっています。
これはActionLayoutがコンテンツの原形サイズ(SeekBarはminWidth="50dp"を確保)で表示されるためです。
ActionLayoutをツールバー幅で表示
ActionLayoutはコンテンツの原形サイズで表示されます。これをツールバー幅に広げて表示する方法を紹介します。
方法は2つあり、どちらもカスタムGroupViewを作成して、そのカスタムGroupViewをActionLayoutのトップViewに指定するやり方です。
なぜ原形サイズで表示される?
ツールバーはViewGroupを継承したコンテナタイプのViewです。
メニューのアイテムが選択された時、ActionLayoutはツールバーへViewGroup#addView( )されて、子Viewになります。
これがActionLayoutを表示する動作です。
... 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; } ... } ... }
上記のソースコードを見ると、ActionLayoutのLayoutParamsがToolbar#generateDefaultLayoutParams( )で作成されていることが分かります。
作成されるLayoutParamsのwidth/height属性はWRAP_CONTENTです。これがコンテンツの原形サイズになる理由です。
方法1:MATCH_PARENTに変更
CollapsibleActionViewはインターフェイスです。
ActionLayoutのトップViewに実装されていると、ツールバーへaddViewした後にCollapsibleActionView#onActionViewExpanded( )が呼び出されます。
このonActionViewExpanded( )内でLayoutParamsのwidthをMATCH_PARENTに書き換えれば、ActionLayoutの幅が変更できます。
そのために、CollapsibleActionViewインターフェイスを実装したカスタムGroupViewを作成して、ActionLayoutのトップViewに指定します。
class ActionLinearLayout : LinearLayout, 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) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) override fun onActionViewExpanded() { val _lp = (layoutParams as Toolbar.LayoutParams).apply { width = Toolbar.LayoutParams.MATCH_PARENT } layoutParams = _lp } override fun onActionViewCollapsed() { Log.i(TAG, "onActionViewCollapsed") } }
<?xml version="1.0" encoding="utf-8"?> <パッケージ名.ActionLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageButton ... /> <SeekBar ... /> <ImageButton ... /> </パッケージ名.ActionLinearLayout>
Toolbarは(1)を参照しているので、方法1を採用する場合はカスタムViewGroupで発生する「非推奨」の警告を無視するしかありません。
(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、子:LinearLayout、孫:ImageButton/SeekBar」に対応
Viewツリーの全ての親Viewと子View間で[1]~[3]の処理が行われます。その中にToolbarとActionLayout間も含まれます。
[2]の「サイズの割り当て指示」で与えられるwidthMeasureSpecならびにheightMeasureSpecは、int型でありながらMSBの2ビットがmodeという特別な意味を持っています。
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( )で判断されて、ActionLayoutの幅が変更できます。
そのために、onMeasure( )を上書きしたカスタムGroupViewを作成して、ActionLayoutのトップViewに指定します。
class ActionLinearLayout : LinearLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) 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) } }
<?xml version="1.0" encoding="utf-8"?> <パッケージ名.ActionLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageButton ... /> <SeekBar ... /> <ImageButton ... /> </パッケージ名.ActionLinearLayout>
ActionLayoutを折り畳む・折り畳まない
item要素のshowAsAction属性にcollapseActionViewを指定すると、ActionLayoutはメニューのアイテムを選択することでツールバーに展開されます。
これをActionLayoutがアイテムへ格納されたように見えることから「折り畳む」と表現しています。
「ActionLayoutの実装」で紹介したサンプルは「折り畳む」です。
<?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:actionLayout="@layout/action_layout" /> ... </menu>
item要素のshowAsAction属性にcollapseActionViewを指定しないと、ActionLayoutは初めからツールバーに展開された状態になります。
これを「折り畳む」に反して「折り畳まない」と表現しています。
<?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:actionLayout="@layout/action_layout" /> ... </menu>
ActionLayoutの展開・格納イベント処理
MenuItem.OnActionExpandListenerインターフェイスをMenuItemに設定することで、ActionLayoutが展開・格納された時のイベントをコールバックで受け取ることができます。
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 { // ActionLayoutを展開した時の処理 return true // falseにすると展開出来ない } override fun onMenuItemActionCollapse(item: MenuItem): Boolean { // ActionLayoutを格納した時の処理 return true // falseにすると格納出来ない } } _toneItem.setOnActionExpandListener(_expandListener) ... return true } ... }
イベントの発火タイミングは図のようになっています。
MenuItem.OnActionExpandListenerインターフェイスのコールバックはaddViewとremoveViewの前に発火します。その点に注意してください。
関連記事: