When I started working at KeepSafe in March 2014, my first major project was to update the UI of our Android application to follow Google’s design guidelines for Android 4.0+.
As part of this project, we wanted to move our album manipulation actions into a menu that would pop up when the user clicked an overflow icon on the album being modified. We felt this would be a familiar action because its behavior is analogous to the overflow menu in the Action Bar, and more easily discoverable than the long-press context menu that we previously had.
However, making this menu appear exactly as I wanted proved to be less trivial than its fairly idiomatic nature would suggest. I did a lot of code-diving and customization to construct the menu, show its icons, move the menu position, and fade out the unselected albums.
Constructing the Menu
Fortunately, there was a PopupMenu class in the AppCompat support library. I could use it to create the menu in a way that fit with the Action Bar and overall visual style of Android 4.0.
First, I needed to add the overflow icon to the ListView item representing the album.
<?xml version=”1.0" encoding=”UTF-8"?> <FrameLayout xmlns:android=”http://schemas.android.com/apk/res/android" android:layout_width=”match_parent” android:layout_height=”wrap_content”> <! — Other content here → <ImageView android:id=”@+id/album_overflow” android:layout_width=”34dp” android:layout_height=”40dp” android:layout_gravity=”top|right” android:padding=”0dp” android:scaleType=”center” android:src=”@drawable/icon_overflow” /> </FrameLayout>
I also needed to create a menu resource which would inflate into the PopupMenu to define the content of the menu.
<?xml version=”1.0" encoding=”UTF-8"?> <menu xmlns:android=”http://schemas.android.com/apk/res/android"> <item android:id=”@+id/album_overflow_rename” android:icon=”@drawable/icon_rename” android:title=”@string/rename” /> <item android:id=”@+id/album_overflow_lock” android:icon=”@drawable/icon_lock” android:title=”@string/lock” /> <item android:id=”@+id/album_overflow_unlock” android:icon=”@drawable/icon_unlock” android:title=”@string/unlock” /> <item android:id=”@+id/album_overflow_delete” android:icon=”@drawable/icon_delete” android:title=”@string/delete” /> <item android:id=”@+id/album_overflow_set_cover” android:icon=”@drawable/icon_set_cover” android:title=”@string/set_cover” /> </menu>
Then I needed an OnClickListener to set on the overflow icon from Java code so the menu appeared when the user clicked on it. My first attempt looked something like this:
https://gist.github.com/philippb/e1851df1a3b22b426044
Then I just had to attach a listener to the overflow of each album in the ListView’s adapter:
@Override public View getView(int position, View row, ViewGroup parent) { //Do view inflation
Album album = getItem(position); View overflow = row.findViewById(R.id.album_overflow); overflow.setOnClickListener(new OnAlbumOverflowSelectedListener(getContext, album); //return inflated view }
Let’s see what that looks like:
Not too bad! But it wasn’t quite like the mockup our designer had given me. I would have to do some customization.
Showing Icons
The first issue I wanted to address was the lack of icons in the menu. Our designer created great icons for each item, which I had referenced in the menu resource, but for some reason they weren’t being used. Adding icons to the menu would help to set it apart from the background, and draw attention to each item while conveying its function. And it would be a shame to waste the work that went into making them.
From my experience working with other Android menu types, like the Action Bar and context menus, I knew the icon declarations in XML were valid. I suspected they weren’t gone forever–they were just hiding somewhere. Unfortunately the public API for PopupMenu contains no functions with any relation to icons.
Not being one to give up easily, I went source code-diving. I found that a PopupMenu contained a private reference to a member called mPopup–a suspiciously important-sounding name–of the type android.support.v7.internal.view.menu.MenuPopupHelper. That seemed promising, so I looked at the source for MenuPopupHelper, and sure enough, there it was: public void setForceShowIcon(boolean forceShow).
However, this was a method on object that was not only a private member but an object of an internal class. My job was not so easily accomplished with calling a single method–no, I would have to use Java’s reflection APIs to circumvent these restrictions.
public void onClick(View v) { PopupMenu popupMenu = new PopupMenu(mContext, v); popupMenu.inflate(R.menu.album_overflow_menu);
// Force icons to show Object menuHelper; Class[] argTypes; try { Field fMenuHelper = PopupMenu.class.getDeclaredField(“mPopup”); fMenuHelper.setAccessible(true); menuHelper = fMenuHelper.get(popupMenu); argTypes = new Class[] { boolean.class }; menuHelper.getClass().getDeclaredMethod(“setForceShowIcon”, argTypes).invoke(menuHelper, true); } catch (Exception e) { // Possible exceptions are NoSuchMethodError and // NoSuchFieldError // // In either case, an exception indicates something is wrong // with the reflection code, or the // structure of the PopupMenu class or its dependencies has // changed. // // These exceptions should never happen since we’re shipping the // AppCompat library in our own apk, // but in the case that they do, we simply can’t force icons to // display, so log the error and // show the menu normally.
Log.w(TAG, “error forcing menu icons to show”, e); popupMenu.show(); return; } popupMenu.show(); }
Here’s the result:
Moving the Menu Around
The next custom thing we wanted to do was to move the menu to the left a bit. Our designer didn’t like the menu being flush with the right edge of the screen and wanted to move it inward so it stood out more. Once again, there was nothing in the public API to accomplish this, so I was back to source code-diving.
In the MenuPopupHelper class that we looked at above, I found another field conspicuously named mPopup. This time, it was an object of type android.support.v7.internal.widget.ListPopupWindow. A window was exactly what I wanted to move, so I went digging in this class and found the method public void setHorizontalOffset(int offset). But this method only sets a member variable. After searching for that variable’s usages, I found that it is also necessary to invoke public void show() in order to update the position of the window.
There was still one more obstacle in my way though. Despite the reflection code for these methods executing correctly, the menu didn’t move!
After playing with different values for the offset, I found that the location of a zero offset is not what’s originally reflected on the screen. As it turns out, the base position of the window is for the top left corner of the menu window to be anchored to the bottom left corner of the anchor view (in this case, the overflow image). Since this would result in the window being rendered mostly offscreen, Android was moving the window to the left during the draw until it was fully onscreen. This meant that even though I moved the window a little bit to the left relative to its default position, it was still partially offscreen, and thus ended up in the same place.
To place the window where I wanted, I needed to move it to the left, not by the width of the overflow image as I had originally thought, but rather by the entire width of the menu window. Luckily, ListPopupWindow provides a method public int getWidth() to get this width.
The final code looks like this:
public void onClick(View v) { // code from above // …
popupMenu.show();
// Try to force some horizontal offset try { Field fListPopup = menuHelper.getClass().getDeclaredField(“mPopup”); fListPopup.setAccessible(true); Object listPopup = fListPopup.get(menuHelper); argTypes = new Class[] { int.class }; Class listPopupClass = listPopup.getClass();
// Get the width of the popup window int width = (Integer) listPopupClass.getDeclaredMethod(“getWidth”) .invoke(listPopup);
// Invoke setHorizontalOffset() with the negative width to move // left by that distance listPopupClass.getDeclaredMethod(“setHorizontalOffset”, argTypes).invoke(listPopup, -width);
// Invoke show() to update the window’s position listPopupClass.getDeclaredMethod(“show”).invoke(listPopup); } catch (Exception e) { // Again, an exception here indicates a programming error rather // than an exceptional condition // at runtime Log.w(TAG, “Unable to force offset”, e); } }
And the result is:
Fading Out the Other Albums
There was a final thing to do with the menu. In order to make it clear both that a menu had been opened and which album those menu options would affect, we wanted to fade out all the albums except the selected one.
Luckily these views were readily available. All I had to do was find the ones that hadn’t been selected and apply a fade-out animation to them, as well as set a listener to fade them back in when the menu was dismissed.
https://gist.github.com/philippb/d9d025fc5af8f1e6152e
My final product:
Originally published at keepsafe.github.io on November 19, 2014. Written by Adam Smialek