Keep Android ImageView in a Constant 4:3 Aspect Ratio
Recently I had a specification for showing thumbnail images in a constant 4:3 aspect ratio regardless of the aspect ratio of whatever image would be displayed. I found a lot of references to manipulating and scaling the image, but not many for scaling the ImageView itself. I did find one good clue from Bob Lee in this Stackoverflow post, so I adapted it.
In a subclass of ImageView you can override onMeasure(), as below, to force a 4:3 or other aspect ratio on the instances. If you are making thumbnails, you’ll also want to scale the bitmaps so they “fit” per your specifications. The specs from my UI designer were pretty specialized so I won’t include the code I used for scaling.
@Override protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
// let the default measuring occur, then force the desired aspect ratio
// on the view (not the drawable).
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
//force a 4:3 aspect ratio
int height = Math.round(width * .75f);
setMeasuredDimension(width, height);
}
Detect Dismissal of the Soft Keyboard
When your app has multiple text entry fields that must be checked for validity before the next action happens, it can be important to know that the user dismissed the soft keyboard via the back button. Typically you would want to listen for key and/or touch events from the fields so as to run your validation routine. However, there is not an easy way to get the information about dismissal of the keyboard.
There was a helpful answer posted to Stackoverflow by Steelight who offered an override of the onKeyPreIme method that you add to your new instance of the EditText class. This is fine if you can do e = new EditText(inflater.getContext()) but not so good if you have to get your EditText instances via e = (EditText) findViewById().
Try creating a subclass of EditText and overriding its onKeyPreIme method. Then you can use this subclass anywhere, especially if you put it in a library project. If you make the onKeyPreIme method send a key event and your instances of this subclass listen for key events, you can detect that the keyboard was dismissed and do whatever you’d do when any key event is received.
/**
* This class overrides the onKeyPreIme method to dispatch a key event if the
* KeyEvent passed to onKeyPreIme has a key code of KeyEvent.KEYCODE_BACK.
* This allows key event listeners to detect that the soft keyboard was
* dismissed.
*
*/
public class ExtendedEditText extends EditText {
public ExtendedEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public ExtendedEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ExtendedEditText(Context context) {
super(context);
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
dispatchKeyEvent(event);
return false;
}
return super.onKeyPreIme(keyCode, event);
}
}
Creating Groups Or Categories For Your Settings Preferences
The Android API for creating a list of preference headers does not provide a means of categorizing or grouping those headers. Gouchet wrote about his investigation of the code in Android’s Settings application and offered a header adapter class that supports category headers. His class is based upon the header adapter found in the Settings.java class of that application.
While comparing Gouchet’s adapter with the adapter found in Settings.java, I noticed that Gouchet’s adapter omitted some of the code that supports view recycling. While Gouchet’s article was mainly about providing On/Off switches for a preference, a robust header adapter should handle view recycling. To keep you from having to track down Settings.java and extract the adapter code, I’ll show my version of HeaderAdapter. I did not need headers that provide On/Off switches so I left that part out. If you need such headers, use Gouchet’s download of his example.
private static class PrefsHeaderAdapter extends ArrayAdapter</pre>
<header>{
static final int HEADER_TYPE_CATEGORY = 0;
static final int HEADER_TYPE_NORMAL = 1;
private static class HeaderViewHolder {
ImageView icon;
TextView title;
TextView summary;
}
private LayoutInflater mInflater;
public PrefsHeaderAdapter(Context context, List
<header>objects) {
super(context, 0, objects);
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
HeaderViewHolder holder;
Header header = getItem(position);
int headerType = getHeaderType(header);
View view = null;
if (convertView == null) {
holder = new HeaderViewHolder();
switch (headerType) {
case HEADER_TYPE_CATEGORY:
view = mInflater.inflate(android.R.layout.preference_category, parent, false);
holder.title = ((TextView) view.findViewById(android.R.id.title));
holder.title.setText(header.getTitle(getContext().getResources()));
view.setTag(holder);
break;
case HEADER_TYPE_NORMAL:
view = mInflater.inflate(R.layout.preference_header_item, parent, false);
ImageView image = ((ImageView) view.findViewById(android.R.id.icon));
holder.icon = image;
image.setImageResource(header.iconRes);
holder.title = ((TextView) view.findViewById(android.R.id.title));
holder.title.setText(header.getTitle(getContext().getResources()));
holder.summary = ((TextView) view.findViewById(android.R.id.summary));
holder.summary.setText(header.getSummary(getContext().getResources()));
view.setTag(holder);
break;
default:
break;
}
} else {
view = convertView;
holder = (HeaderViewHolder) view.getTag();
}
// All view fields must be updated every time, because the view may
// be recycled
switch (headerType) {
case HEADER_TYPE_CATEGORY:
holder.title.setText(header.getTitle(getContext().getResources()));
break;
case HEADER_TYPE_NORMAL:
if (null != holder.icon) {
holder.icon.setImageResource(header.iconRes);
}
holder.title.setText(header.getTitle(getContext().getResources()));
CharSequence summary = header.getSummary(getContext().getResources());
if (null != summary) {
if (!TextUtils.isEmpty(summary)) {
holder.summary.setVisibility(View.VISIBLE);
holder.summary.setText(summary);
} else {
holder.summary.setVisibility(View.GONE);
}
}
break;
default:
}
return view;
}
public static int getHeaderType(Header header) {
if ((header.fragment == null) && (header.intent == null)) {
return HEADER_TYPE_CATEGORY;
}
return HEADER_TYPE_NORMAL;
}
}
Using this adapter allowed me to create a grouping header labeled “CONNECTIONS” above a list of server connections in the Settings area of our app (see below for portion of the screen).
Note that if your settings allows users to add or remove items, such as accounts, then view recycling can result in an unexpected type of view holder. You might get a normal type from “convertView” but the header type is category for that position. The code in Settings.java manipulates index counters when an account is added. I added an int field to HeaderViewHolder and set it to the header type. Then if the holder retrieved from the view tag is not the same type as the header, I null it out and create a new one.
The Android Vertical Line Problem
Getting a thin vertical line to use as a divider or other indicator is surprisingly difficult. Various solutions to this problem have been offered at Stackoverflow, e.g. “Android vertical line XML“. Recently I needed a vertical line to use as a divider between items in an open source tooltip component. The default divider was provided as a narrow Textview with a gray background and no text, which was contained within a RelativeLayout. That’s a rather heavyweight implementation, and the divider did not extend the full height of the tooltip, either.
I tried several of the approaches outlined in the Stackoverflow post referenced above and elsewhere. These included a rotated shape drawable and a view with a layout width of 2 pixels, but these did not work. Usually I got a result like below, where the gray divider view caused the tooltip to expand in size and the divider crowded the remaining items out of the tooltip. (An item is made up of text and an icon, like the “Next” and down arrow, although each is optional. See below.)
I then adapted a technique we’d used before to get a divider appearing in TextViews used as table cells. We subclassed the TextView and overrode the onDraw method. We drew a narrow vertical line at the left edge to make a nice divider between adjacent table cells. Here’s the (somewhat condensed) code:
private Paint paint = new Paint();
private void init(Context context) {
paint.setColor(context.getResources().getColor(
R.color.cell_divider_line));
paint.setStrokeWidth(0f);
paint.setStyle(Paint.Style.FILL);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawLine(0, 0, 0, getHeight(), paint);
}
This tooltip component supports both horizontal and vertical orientations so dividers are needed for both cases. The original views for each orientation had a RelativeLayout as the outer view that contains a single tooltip item (an icon and text), so I subclassed RelativeLayout, as below (condensed). This RelativeLayoutWithDivider class provides for a right divider, a bottom divider or no divider.
public enum DividerOrientation {RIGHT, BOTTOM, NONE}
private Paint paint ;
private Context context;
private DividerOrientation orientation = DividerOrientation.NONE;
private void init() {
paint = new Paint();
paint.setColor(context.getResources().getColor(
R.color.cell_divider_line));
paint.setStrokeWidth(3f);
paint.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
switch (orientation) {
case RIGHT:
init();
canvas.drawLine(getWidth(), 0, getWidth(), getHeight(), paint);
break;
case BOTTOM:
init();
canvas.drawLine(0, getHeight(), getWidth(), getHeight(), paint);
break;
default:
//no divider line
break;
}
}
public void setDividerOrientation(DividerOrientation orientation) {
this.orientation = orientation;
}

Logic in the method the populates the items in the tooltip determines which enum to pass on a call to setDividerOrientation such that single items don’t show a divider nor does the last item. (That method is found in the QuickAction class of the open-source project.)

This will make more sense when I show the layout XML for a horizontal tooltip item. Here the parent view is our subclassed RelativeLayout. Nothing had to change in this XML save the class name in the outermost tag.
Fixing The Command File For Android’s uiautomatorviewer Tool
The uiautomatorviewer is a “GUI tool to scan and analyze the UI components”, and was made available in Revision 21 of the Android SDK Tools. The instructions for running the tool tell us to open a terminal window, navigate to <android-sdk>/tools and run the “uiautomatorviewer” command. On Windows, that is a command file with a “bat” extension.
The UI for the tool opens up and the instructions tell us next to capture a screen for analysis. Unfortunately this failed for me. The error message implied that the Android adb tool is not installed. It most certainly is installed, so I began a search for a solution. I didn’t find a solution initially, although I did find that other developers were seeing the same problem.
It was when I decided to search on Google+ that I found this post by Endian Ogino. He worked out that the com.android.uiautomator.bindir property, being passed as a Java argument, was blank. Once he edited uiautomatorviewer.bat to supply the path to the <android-sdk>/tools folder, the screenshot action started working. Since his solution did not appear in a traditional Google search, I thought I’d write about it in this blog so that, hopefully, it could turn up in searches more often.
Now, can anyone help me understand why many of the attempts at screenshotting various views in my application fail to generate any XML for display? I’ve searched on the error message and the exception name without any luck.
Run Hierarchy Viewer without using the Android emulator
The Android Hierarchy Viewer is a useful tool for optimizing your user interface. However for security purposes it can only be used on apps running in the Android emulator. This is a pain because the emulator is slow to start up and slow to load the app. Another headache is the flawed support for aspect ratios that emulate tablets. This results in crashes. Fortunately there is another way.
Using an Android virtual machine running locally allows the Hierarchy Viewer to connect and investigate your app. I installed the VirtualBox general-purpose full virtualizer for x86 hardware. After that I needed an Android VM so got the VMLite Android and followed the directions at that link. Once I did a “adb connect localhost” from a command line, I went into my Eclipse workspace and modified the Run configuration for my app. I temporarily set the Target tab to “Always prompt to pick device”. Once the prompt came up, I selected the VM instead of my physical tablet. The app installed and displayed very quickly.
Next, I had to get around the headache that has existed in the Hierarchy Viewer tool apparently since version 8 of the tools. The problem was that neither the colored performance indicators nor duration information appeared in any of the views. Using this “solution“, I got those key features to show up.
With the app running in the VM, the Hierarchy Viewer happily dug in and rounded up the performance data for me. This is a very viable alternative to using the Android emulator. I appreciate Ed Burnette for suggesting this approach and giving me the URL’s to VirtualBox and VMLite Android.
Repositioning Toast Messages
I work on a large app that shows “business intelligence” reports. A feature of such reports allows the designer to add one or more images to the report along with more typical components like graphs and tables. The images can have “alt text”, and we want to show that in a Toast message if the user taps on the image. By default, Toast messages show up horizontally centered and near the bottom of the screen. Since that location may not be over the selected image, I decided to fix that by explicitly repositioning the Toast.
In the screen shot above, you can see two images inside of horizontal rectangles. Prior to the fix, tapping on the image of the dog, causes the Toast with the “alt text” message to display on top of the fighter jet.
The Toast class has a method for adjusting the position:
toast.setGravity(Gravity.CENTER, xOffset, yOffset);
The x and y offsets are relative to the position determined by your choice for the gravity parameter. By starting from a centered gravity, I’ll be moving the position to be over the tapped image by changing the offsets. As near as I can tell, (Gravity.CENTER, 0,0) represents the center of the screen so negative and positive offsets are needed to correctly position the Toast.
We use an OnClickListener to detect the tap, and the ImageView is the view supplied to the onClick(View v) method in the listener. To obtain the coordinates of the RelativeLayout holding the ImageView, I obtained the parent view of the image view, then called getGlobalVisibleRect(Rect gvr) on the parent. The “gvr” rectangle contains the coordinates of the view over which we want to position the Toast. Calling getRootView() on the image view gives us the outermost view of the app, and we can use its coordinates to represent the limits of the viewable area.
Now it is a matter of calculating the “center” of the image and expressing that as an appropriate x and y offset. I determined the center of the viewable area by dividing the root view’s right and bottom values in half. The center of the image’s parent view was trickier. Look at the comments in the code below to see how I did it.
public void onClick(View v) {
int xOffset = 0;
int yOffset = 0;
Rect gvr = new Rect();
View parent = (View) v.getParent();// v is the image,
//parent is the rectangle holding it.
if (parent.getGlobalVisibleRect(gvr)) {
Log.v("image left", Integer.toString(gvr.left));
Log.v("image right", Integer.toString(gvr.right));
Log.v("image top", Integer.toString(gvr.top));
Log.v("image bottom", Integer.toString(gvr.bottom));
View root = v.getRootView();
int halfwayWidth = root.getRight() / 2;
int halfwayHeight = root.getBottom() / 2;
//get the horizontal center
int parentCenterX = ((gvr.right - gvr.left) / 2) + gvr.left;
//get the vertical center
int parentCenterY = (gvr.bottom - gvr.top) / 2 + gvr.top;
if (parentCenterY <= halfwayHeight) {
yOffset = -(halfwayHeight - parentCenterY);//this image is above the center of gravity, i.e. the halfwayHeight
} else {
yOffset = parentCenterY - halfwayHeight;
}
if (parentCenterX < halfwayWidth) { //this view is left of center xOffset = -(halfwayWidth - parentCenterX); } if (parentCenterX >= halfwayWidth) {
//this view is right of center
xOffset = parentCenterX - halfwayWidth;
}
}
Toast toast = Toast.makeText(activity, altText, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, xOffset, yOffset);
toast.show();
}
});
Now knowing the center of the screen and center of the image, you can pass an appropriate x and y offset to the Toast. The sign of the offset will have to be negative for positions left and above the center.
Here’s the Toast showing over the image of the dog.
Now looking at a report with three images in portrait orientation, we see that the Toast is properly positioned horizontally.
As always, I’d like to hear about smarter ways to do these things.



