How to fully mimic Action item view in the toolbar

2019-05-10 12:44发布

问题:

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:

A native action item:

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:

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:

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:

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>

回答1:

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" />