Tags
android, combined views, custom-ui, feature, fixed height, grid in a list view, grid-view, highlight, intercept touch event, kinetic list view, list-view, listview, measure, mozilla, scrolling, top items
A long list of anything would feel boring. Each and every item in the list has equal priority. Even if the sort order changes to show difference in priority, the physical appearance of the individual views are still the same. Think of browsing through 100+ bookmarks. Clearly a user would navigate to only 5-10 bookmarks often. Isn’t there a better way to showcase these top bookmarks? How about featuring the top friends that you talk to in a chat application, among 200+ contacts? The new design concept emerging these days to mitigate this issue has a grid of featured items with a big list following it.
How do we implement this in Android? If the number of entries is fixed, we could use a LinearLayout
for the grid. The ListView
could follow it. However if they both have different parents, the scrolling is a major issue, as ListView
has its own scrolling. The alternative approach would be add the LinearLayout
as a header to the ListView
. This way, the grid of entries would scroll with the list. But usually, though the number of entries is fixed, the entries themselves aren’t fixed. They are shown as a result of a DB query or from a data set returned by the server. In such a case, GridView
would be better as it is backed by an adapter. The problem here is that, both GridView
and ListView
are scrollable themselves, and it is not a good approach to embed a scrollable container inside another scrollable container. How do we make one of the views — the GridView, actually — “unscrollable”?
@Override public int getColumnWidth() { // This method will be called from onMeasure() too. // It's better to use getMeasuredWidth(), as it is safe in this case. final int totalHorizontalSpacing = mNumColumns > 0 ? (mNumColumns - 1) * mHorizontalSpacing : 0; return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - totalHorizontalSpacing) / mNumColumns; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Sets the padding for this view. super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int measuredWidth = getMeasuredWidth(); final int childWidth = getColumnWidth(); int childHeight = 0; // If there's an adapter, use it to calculate the height of this view. final ListAdapter adapter = getAdapter(); final int count; // There shouldn't be any inherent size (due to padding) if there are no child views. if (adapter == null || (count = adapter.getCount()) == 0) { setMeasuredDimension(0, 0); return; } // Get the first child from the adapter. final View child = adapter.getView(0, null, this); if (child != null) { // Set a default LayoutParams on the child, if it doesn't have one on its own. AbsListView.LayoutParams params = (AbsListView.LayoutParams) child.getLayoutParams(); if (params == null) { params = new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT, AbsListView.LayoutParams.WRAP_CONTENT); child.setLayoutParams(params); } // Measure the exact width of the child, and the height based on the width. // Note: the child takes care of calculating its height. int childWidthSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); child.measure(childWidthSpec, childHeightSpec); childHeight = child.getMeasuredHeight(); } // Number of rows required to 'mTotal' items. final int rows = (int) Math.ceil((double) mTotal / mNumColumns); final int childrenHeight = childHeight * rows; final int totalVerticalSpacing = rows > 0 ? (rows - 1) * mVerticalSpacing : 0; // Total height of this view. final int measuredHeight = childrenHeight + getPaddingTop() + getPaddingBottom() + totalVerticalSpacing; setMeasuredDimension(measuredWidth, measuredHeight); }
That’s it! We basically see if there is an adapter. If so, we ask the first child to measure itself, given some constraints. Based on the height of the first child, we calculate the number of rows required, and hence the total height of the view. This will be set as the measured dimension for the view. One thing to note here is that, the vertical spacing and the horizontal spacing aren’t available as getters in pre-ICS versions. It’s better to get them in the constructor and store them as class variables.
There is a weird case in Firefox for Android, that doesn’t allow the scroll from the GridView to be passed to the ListView. I couldn’t reproduce the actual scenario in a sample app. In case, you ever land in a similar scenario, here’s the recipe to solve it.
// In your ListView. @Override public boolean onInterceptTouchEvent(MotionEvent event) { switch(event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { // Store the event by obtaining a copy. // Android doesn't like storing the event as such. mMotionEvent = MotionEvent.obtain(event); break; } case MotionEvent.ACTION_MOVE: { // If there is a move, make sure its more than the touch slop. // The user's intention would have been to tap on the featured items. if ((mMotionEvent != null) && (Math.abs(event.getY() - mMotionEvent.getY()) > mTouchSlop)) { // The user is scrolling. Pass the last event to this view, // and make this view scroll. onTouchEvent(mMotionEvent); // By returning true here, this ListView will receive // all further touch events. return true; } break; } default: { mMotionEvent = null; break; } } // Do default interception. return super.onInterceptTouchEvent(event); }
That’s all folks! Show the most important contacts, top news articles, top photos as featured ones easily.
Thanks much for sharing this, Sriram.
Just a design-related-query: Any suggestions on intuitive ways to display information that’s represented in a tabular format in a PC-Browser?
Taking the cues from Gmail-Inbox on PC Vs it’s Android Experience, I agree with the data-identification-based-on-a-single-field approach, but am looking for any more intuitive/creative ways to display this information with allowing the user to perform the create,edit, delete tasks as well from with in the same app.
Thanks in advance,
VC
I don’t understand your requirement properly. There are different UI patterns to display tabs in Android. Like normal tab widgets to view pagers.
It’s certainly an interesting idea but you’re engaging two different ways of consuming data and minimising the amount of data that can be consumed at a glance.
Nice! works pretty well. The only thing that is missing is actually defining the member variables so it compiles.
i had a pretty similar approach but simpler 🙂
@Override
public void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
// Calculate entire height by providing a very large height hint.
// But do not use the highest 2 bits of this integer; those are
// reserved for the MeasureSpec mode.
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
ViewGroup.LayoutParams params = this.getLayoutParams();
params.height = getMeasuredHeight();
}
Hello sriram, can we have the source code please ?
can we have the source code please ?
Pingback: 如何在Android的ListView中膨胀GridView? – FIXBBS