Search code examples
androidandroid-actionbarandroid-toolbaraction-menu

How to fully mimic Action item view in the toolbar, for a customized one?


Background

I have an action item that's not quite standard. It has its own layout, because I've failed to create a nice Drawable for it (written about it here).

Basically, it's just a TextView with a background that wraps its short text that it shows.

The problem

Sadly I can't find a way of fully mimic it to look like normal ones:

  1. The ripple effect (and probably the simple clicking effect on older versions too) doesn't have the same color and size.
  2. There isn't a toast for when I long click on it
  3. Maybe other things I haven't noticed?

Here are screenshots to demonstrate the differences:

The new, non standard, action item:

enter image description here

A native action item:

enter image description here

What I've tried

For the color of the ripple effect, I think I can use "colorControlHighlight", but I can't find out which color is the one used by default, correctly. I've looked inside the "values.xml" file of the support library, and noticed that it's "ripple_material_dark" color (or "ripple_material_light", in case the toolbar is supposed to be white) , but this seems a bit like a hack.

Not sure about the size, but looking at the layout inspector, I think the view has padding:

enter image description here

I've also noticed that the view class name of the toolbar is ActionMenuItemView . I tried to look at its code (probably avaialble online too, here) , but didn't notice anything mentioned there about background. For padding I think it just tries to put the icon in the middle:

enter image description here

Anyway, this is the current code of the POC:

MainActivity.kt

class MainActivity : AppCompatActivity() {
    lateinit var goToTodayView: View
    lateinit var goToTodayTextView: TextView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        goToTodayView = LayoutInflater.from(this).inflate(R.layout.go_to_today_action_item, toolbar, false)
        goToTodayTextView = goToTodayView.goToTodayTextView
        goToTodayTextView.setBackgroundDrawable(AppCompatResources.getDrawable(this, R.drawable.ic_backtodate))
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menu.add("goToToday").setActionView(goToTodayView).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
        menu.add("asd").setIcon(R.drawable.abc_ic_menu_copy_mtrl_am_alpha).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) //for comparison
        return super.onCreateOptionsMenu(menu)
    }
}

go_to_today_action_item.xml

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
    android:background="?attr/selectableItemBackgroundBorderless" android:backgroundTint="#fff" android:clickable="true"
    android:focusable="true">

    <TextView
        android:id="@+id/goToTodayTextView" android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:layout_gravity="center" android:gravity="center" android:text="1"
        android:textColor="#fff" android:textSize="12dp" tools:background="@drawable/ic_backtodate" tools:layout_gravity="center"/>
</FrameLayout>

The questions

  1. How do I set the same background as a normal action item?
  2. How do I put the toast like a normal action item?
  3. Are there other things that are different between normal action item and one that I set with a layout?
  4. In other words, is it possible to fully mimic native action items? Maybe using the ActionMenuItemView class somehow? Maybe a very different solution from what I tried?

EDIT: for the background of the action items, I made it a bit like the original ones, but it's still not the same. Clicking effect seems a tiny bit different, and I haven't found how to show the toast of the action item upon long clicking on it. Here's the result:

enter image description here

Anyway, here's what I did:

go_to_today_action_item.xml

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="?attr/actionBarSize" android:layout_height="?attr/actionBarSize" android:clickable="true"
    android:focusable="true">

    <ImageView
        android:layout_width="@dimen/action_item_background_size"
        android:layout_height="@dimen/action_item_background_size" android:layout_gravity="center"
        android:background="@drawable/action_item_selector" android:duplicateParentState="true"/>

    <TextView
        android:id="@+id/goToTodayTextView" android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:layout_gravity="center" android:gravity="center" android:text="31" android:textColor="#fff"
        android:textSize="12dp" tools:background="@drawable/ic_backtodate" tools:layout_gravity="center"/>
</FrameLayout>

drawable/action_item_selector.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <solid android:color="@color/clicking_semi_white_ripple_color"/>
        </shape>
    </item>
    <item android:drawable="@android:color/transparent"/>
</selector>

drawable-v21/action_item_selector.xml

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="@color/clicking_semi_white_ripple_color">
    <item android:id="@android:id/mask">
        <shape android:shape="oval">
            <solid android:color="@android:color/white"/>
        </shape>
    </item>
</ripple>

colors.xml

<color name="clicking_semi_white_ripple_color">#33ffffff</color>

values/dimens.xml

<dimen name="action_item_background_size">48dp</dimen>

values-v21/dimens.xml

<dimen name="action_item_background_size">40dp</dimen>

Solution

  • The following code will use your layout to populate a standard menu item for the date. As a standard menu item, it will exhibit all the characteristics that you are looking for and should be compatible with future changes to a menu item's behavior.

    The basic concept is to inflate the layout with the boxed date and use its drawing cache to create a bitmap that is then used as the drawable for the menu item's icon.

    MainActivity.kt

    class MainActivity : AppCompatActivity() {
        private var goToTodayView: View? = null
        private var goToTodayTextView: TextView? = null
        private val textDrawable: TextDrawable? = null
        private var mOptionsMenu: Menu? = null
    
        public override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            setSupportActionBar(findViewById<View>(R.id.toolbar) as Toolbar)
            testWithCustomViews()
        }
    
        private fun updateDateView() {
            if (mOptionsMenu == null)
                return
            goToTodayView!!.invalidate()
            goToTodayView!!.buildDrawingCache()
            val bmp = Bitmap.createBitmap(goToTodayView!!.drawingCache)
            val d = BitmapDrawable(resources, bmp)
            mOptionsMenu!!.getItem(0).icon = d
        }
    
        private fun testWithCustomViews() {
            val toolbar = findViewById<View>(R.id.toolbar) as Toolbar
    
            goToTodayView = LayoutInflater.from(this).inflate(R.layout.go_to_today_action_item, toolbar, false)
            goToTodayView!!.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
            goToTodayView!!.layout(0, 0, goToTodayView!!.measuredWidth, goToTodayView!!.measuredHeight)
            goToTodayTextView = goToTodayView!!.findViewById(R.id.goToTodayTextView)
            goToTodayTextView!!.setBackgroundDrawable(AppCompatResources.getDrawable(this, R.drawable.ic_backtodate))
            goToTodayView!!.isDrawingCacheEnabled = true
            val handler = Handler()
            val runnable = object : Runnable {
                internal var i = 0
    
                override fun run() {
                    if (isFinishing || isDestroyed)
                        return
                    goToTodayTextView!!.text = (i + 1).toString()
                    i = (i + 1) % 31
                    updateDateView()
                    handler.postDelayed(this, 1000)
                }
            }
            runnable.run()
        }
    
        override fun onCreateOptionsMenu(menu: Menu): Boolean {
            mOptionsMenu = menu
            menu.add("goToToday").setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
            updateDateView()
            menu.add("asd").setIcon(R.drawable.abc_ic_menu_copy_mtrl_am_alpha).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
            return super.onCreateOptionsMenu(menu)
        }
    }
    

    Since go_to_today_action_item.xml now doesn't need the extra views to mimic standard menu item behavior, it can be stripped down. In fact, the menu item icon appears too small without adjustments. Change go_to_today_action_item.xml to the following:

    <TextView
        android:id="@+id/goToTodayTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:background="@drawable/ic_backtodate"
        android:gravity="center"
        android:text="31"
        android:textColor="#fff"
        android:textSize="12dp"
        tools:layout_gravity="center" />
    

    enter image description here