2

Background

I'm trying to have a toolbar show just action items. No title. And I want them to fill the space as much as possible, and if there is no space for some, have them on the overflow menu item.

This can include all kinds of action items (including those with customized action-view).

The problem

For some reason, even though I've set all padding/spacing on the toolbar, and even though I set SHOW_AS_ACTION_IF_ROOM for each of the action items, it doesn't occur.

It shows about 2 items max, even though there is a lot of space left:

enter image description here

What I've tried

I tried to set all kinds of resetting of padding/spacing for the toolbar:

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize"
        android:padding="0px" app:contentInsetEnd="0px" app:contentInsetEndWithActions="0px" app:contentInsetLeft="0px"
        app:contentInsetRight="0px" app:contentInsetStart="0px" app:contentInsetStartWithNavigation="0px"
        app:layout_constraintEnd_toEndOf="parent" app:logo="@null" app:title="@null" app:titleMargin="0px"
        tools:background="#ffcc0000" />

And to test it, I used :

        for (i in 0..10) 
            toolbar.menu.add("item $i").setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)

Sadly, as I said, this didn't help.

I also tried to write some code using ActionMenuView instead of Toolbar, but it has too many function that are called only from the library and depend on it. I couldn't find a way to make it look similar to the Toolbar, and it also has some weird clicking effects issues in some cases.

The question

How can I set the Toolbar to have all items view themselves as long as there is enough space, and if some can't fit, let them be in the overflow menu?

Is there a way to override the behavior of the Toolbar? Where is this behavior even written?

android developer
  • 114,585
  • 152
  • 739
  • 1,270

1 Answers1

2

OK I got something to solve it, but it has a few important notes and assumptions. I think that with some adjustments, it can also solve a similar question I've asked in the past, to have the action items on the left. All written in the code here:

ToolbarAdjuster.kt

object ToolbarAdjuster {
    /**
     * a special behavior for Toolbar, to put all of its menu items that can fit - to actually fit, and the rest to be in overflow menu
     * Important notes and assumptions:
     * 1. You should call it each time you reset the menu items
     * 2. Toolbar has a size. You can use in `onCreateOptionsMenu` if it's the actionBar, or `doOnPreDraw` otherwise.
     * 3. Only action items that have actionView can be on the toolbar. The rest should always be in the overflow menu
     * 4. Toolbar should consist only of action items. No spacing, no padding, no title, ... Otherwise the calculation will be incorrect.
     * Meaning:
     * android:padding="0px" app:contentInsetEnd="0px" app:contentInsetEndWithActions="0px" app:contentInsetLeft="0px"
     * app:contentInsetRight="0px" app:contentInsetStart="0px" app:contentInsetStartWithNavigation="0px"
     * app:layout_constraintEnd_toEndOf="parent" app:logo="@null" app:title="@null" app:titleMargin="0px"
     */
    fun adjustToolbar(context: Context, toolbar: Toolbar, menu: Menu) {
        val toolbarWidth = toolbar.width
        val indexToWidthMap = HashMap<Int, Int>()
        val menuItemsCount = menu.size
        var sizeSoFar = 0
        var foundNeedForOverflowItem = false
        //first find if we will need an overflow menu item:
        for (i in 0 until menuItemsCount) {
            val menuItem = menu[i]
            val menuItemView = menuItem.actionView
            if (!menuItem.isVisible)
                continue
            if (menuItemView == null) {
                foundNeedForOverflowItem = true
                continue
            }
            menuItemView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
            val menuItemViewWidth = menuItemView.measuredWidth
            indexToWidthMap[i] = menuItemViewWidth
            if (menuItemViewWidth + sizeSoFar > toolbarWidth) {
                foundNeedForOverflowItem = true
                break
            }
            sizeSoFar += menuItemViewWidth
        }
        //now we know if we need an overflow menu or not, so go over again, and set each menu item to how it will be shown
        var spaceLeft = if (foundNeedForOverflowItem) toolbarWidth - getDefaultOverflowWidth(context) else toolbarWidth
        for (i in 0 until menuItemsCount) {
            val menuItem = menu[i]
            val menuItemView = menuItem.actionView
            if (!menuItem.isVisible)
                continue
            if (menuItemView == null || spaceLeft <= 0) {
                menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
                continue
            }
            val measuredAItemViewWidth = indexToWidthMap[i]!!
            if (spaceLeft - measuredAItemViewWidth < 0) {
                //this item's view can't fit into the space we have left, so none of the next ones will fit
                spaceLeft = 0
                menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
            } else {
                //this item's view can still fit
                spaceLeft -= measuredAItemViewWidth
                menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
            }
        }
    }

    private fun getDefaultOverflowWidth(context: Context): Int {
        val overflowMenuButton = OverflowMenuButton(context)
        overflowMenuButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
        val overflowCellSize = overflowMenuButton.measuredWidth
        return if (overflowCellSize > 0) overflowCellSize else TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40f, context.resources.displayMetrics).toInt()
    }

    /**fake view, copied from ActionMenuPresenter, which is used only to get its width*/
    private class OverflowMenuButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.actionOverflowButtonStyle) : AppCompatImageView(context, attrs, defStyleAttr)
}

example usage:

MainActivity.kt

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        title = null
        setSupportActionBar(toolbar)
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        for (i in 0..10) {
            val actionView = LayoutInflater.from(this).inflate(R.layout.action_item, toolbar, false)
            actionView.textView.text = "item $i"
            menu.add("item $i").setActionView(actionView).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
        }
        for (i in 0..10)
            menu.add("item $i").setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
        ToolbarAdjuster.adjustToolbar(this, toolbar, menu)
        return super.onCreateOptionsMenu(menu)
    }

}

action_item.xml

<androidx.appcompat.widget.AppCompatTextView
    android:id="@+id/textView" xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="match_parent"
    android:background="?attr/selectableItemBackground" android:clickable="true" android:drawableLeft="@android:drawable/ic_dialog_email"
    android:drawablePadding="8dp" android:focusable="true" android:focusableInTouchMode="false"
    android:gravity="center_vertical|start" android:paddingLeft="15dp" android:paddingRight="15dp"
    android:text="text" tools:background="#11ff0000"
    tools:layout_gravity="center" tools:layout_height="?attr/actionBarSize" />

activity_main.xml

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" tools:context=".MainActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize"
        android:padding="0px" app:contentInsetEnd="0px" app:contentInsetEndWithActions="0px" app:contentInsetLeft="0px"
        app:contentInsetRight="0px" app:contentInsetStart="0px" app:contentInsetStartWithNavigation="0px"
        app:layout_constraintEnd_toEndOf="parent" app:logo="@null" app:title="@null" app:titleMargin="0px"
        tools:background="#ffcc0000" />

</FrameLayout>

And here is the result:

enter image description here

android developer
  • 114,585
  • 152
  • 739
  • 1,270