I’ve started coding Android again and in all honestly it fields like jumping head first into a mine field. Everything is littered with hidden problems that you have to carefully navigate around. This time I’ll try to cherry pick some of the problems I’ve had whilst getting a normal list view to work. This is probably the first and most standard thing you would want to implement when building an app yet all the resources are so scarce in detail when it comes to the only slightly more advanced features like laying out multiple items in the list or filling your screen with more than just a handful of items. So here it goes, below is an image of what I tried to achieve:
Thumbnail pictures
One of the things I wanted was a thumbnail picture sized with free width (according to aspect ratio) but with a clamped height, not higher that of the list item height. This took me a good 4 hours before I came across a couple of sources saying that no matter how many combinations of layout_height, layout_width etc you try you will not get the image not to occupy unnecessary space when rescaling. So, in the end, I used ScaleImageView written by Maurycy Wojtowicz. Basically what it does is to recalculate the actual width of the image when I rescale it by height. I would’ve thought the standard ImageView would do this but apparently not, so instead of hurting your head with getting the image to work try this from the start. In addition to just adding the class you also need to create a resource file: resvaluesattrs.xml, this file should contain the following to be able to reference the new custom view in xml:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="ScaleImageView"/> </resources>
That is to say, this was the problem once I had retrieved the images. Of course they weren’t on disk so I had to fetch them from a URL, just this requires a big bit of code I would’ve thought would be included by default in the framework. For my downloading I used this following AsyncTask implementation that upon completion of download assigns the ImageView the bitmap (taken from here). Something to consider here is to also cache this download (as you’ll come to see why below).
public class AsyncImageLoader extends AsyncTask<String, Void, Bitmap> { private final WeakReference<ImageView> mImageView; private final String mImageUrl; private final String mTag = getClass().getName(); public AsyncImageLoader(CharSequence imageUrl, ImageView imageView) { mImageUrl = imageUrl.toString(); mImageView = new WeakReference<ImageView>(imageView); } public static void LoadUrlTo(CharSequence imageUrl, ImageView imageView) { new AsyncImageLoader(imageUrl, imageView).StartLoading(); } public void StartLoading() { execute(); } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); if(isCancelled()) return; ImageView imageView = mImageView.get(); if(imageView != null) imageView.setImageBitmap(bitmap); } @Override protected Bitmap doInBackground(String... strings) { final AndroidHttpClient client = AndroidHttpClient.newInstance("Android"); final HttpGet getRequest = new HttpGet(mImageUrl); try { HttpResponse response = client.execute(getRequest); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != HttpStatus.SC_OK) { Log.w(mTag, "Error " + statusCode + " while retrieving bitmap from " + mImageUrl); return null; } final HttpEntity entity = response.getEntity(); if (entity != null) { InputStream inputStream = null; try { inputStream = entity.getContent(); final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); return bitmap; } finally { if (inputStream != null) { inputStream.close(); } entity.consumeContent(); } } } cat1ch (Exception e) { getRequest.abort(); Log.w(mTag, "Error while retrieving bitmap from " + mImageUrl); } finally { if (client != null) { client.close(); } } return null; } }
Weighted layout
Another big issue was getting the layout to play nicely, much to do with the fact that my image was taking up to much space but also due to the tricky nature of weighed layouts in Android. There are a lot of resources about this out there, unfortunately not enough to not make the Android Developer Docs come out on top, the least descriptive one. Instead, I would warmly recommend this one that visually really explains how the weighted values work. Basically, the layout_weight describes how big a portion of the sum of all weights the element should grab of the remaining free space. So what we would be used to in percentages 25+25+50% would be described as 1+1+2 and that still just acts on the free space so elements with wrap_content will still get their fair share of space anyway.
The layout I ended up with in the list image above looks like this:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="@dimen/list_item_vertical_padding" android:paddingBottom="@dimen/list_item_vertical_padding" android:divider="?android:dividerHorizontal" android:showDividers="middle"> <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="@dimen/list_item_divider_padding"> <TextView style="@android:style/TextAppearance.Holo.Large" android:id="@+id/id1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" tools:text="22:04"/> <TextView style="@android:style/TextAppearance.Holo.Small" android:id="@+id/item_item_less_important_when" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/id2" android:layout_centerHorizontal="true" tools:text="design time text" android:layout_marginRight="@dimen/list_item_divider_padding"/> </RelativeLayout> <LinearLayout android:layout_marginLeft="@dimen/list_item_divider_padding" android:orientation="vertical" android:layout_height="wrap_content" android:layout_weight="1" android:layout_width="0dp" android:layout_marginRight="@dimen/list_item_info_value_margin"> <TextView style="@android:style/TextAppearance.Holo.Large" android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/id3" android:ellipsize="end" android:singleLine="true" tools:text="design time title" /> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:showDividers="middle" android:divider="?android:dividerHorizontal" android:dividerPadding="@dimen/list_item_divider_padding" android:orientation="horizontal"> <TextView style="@android:style/TextAppearance.Holo.Small" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/id4" android:layout_marginLeft="@dimen/list_item_info_value_margin" android:layout_marginRight="@dimen/list_item_info_value_margin" tools:text="design time text" /> <TextView style="@android:style/TextAppearance.Holo.Small" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/id5" android:layout_marginLeft="@dimen/list_item_info_value_margin" android:layout_marginRight="@dimen/list_item_info_value_margin" tools:text="design time text" /> </LinearLayout> </LinearLayout> <greycastle.myapp.ScaleImageView android:layout_height="@dimen/item_item_image_height" android:layout_width="wrap_content" android:id="@+id/item_item_image" android:layout_marginLeft="@dimen/list_item_info_value_margin" android:layout_marginRight="@dimen/list_item_info_value_margin" android:scaleType="fitEnd" tools:src="@drawable/design_time_image" /> </LinearLayout>
List item view caching
If you’ve got to this page you’ve probably already implemented your first custom list adapter, maybe using something like this:
public class MyItemListAdapter extends ArrayAdapter<MyItem> { private final LayoutInflater mInflater; private final Resources mResources; private int mResourceId; public MyItemListAdapter(Context context, int resourceId, List<MyItem> list){ super(context, resourceId); mResources = context.getResources(); mResourceId = resourceId; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public View getView(int position, View view, ViewGroup parent) { if(view == null) view = mInflater.inflate(mResourceId, parent, false); // Get items from view and set properties... return view; } }
You may also have been a bit curious about what this if(view == null) means in all the examples and I just found out the hard way.
If you have a list of a thousand items you can image that keeping them all in memory would be a bit painful, therefore Android caches the item views for you to repopulate when new items come into view… wait what? Yes, sure it makes sense but it also means that your getView() actually should not have as a main focus to create new views but to populate views because only for the first set of items being displayed in the list will you actually create new views. Most applications using a list will definitely have more items than that in a list, after all otherwise a list would be somewhat overkill. This means that we need to take great care into doing a set of different things:
1.Make sure you set all item properties, if you’ve hidden things previously make sure to unhide them and clear out any old values
2. If you “change” your view, say remove stuff, these will be permanently gone
3. Your items will be recreated each time they come into view, as such, don’t load stuff in here or at least make sure you cache them
This article explains the above a bit more in detail.
One thing you can do that might ease this is to create view types, I’m not entirely convinced by using these soft integer id’s for types but it’s better than nothing, in short you override a getViewType method in the adapter and return the type of view you have for an item, this way you can handle objects of different kinds making sure the view they reuse is of the same layout time, ie:
private int getItemType(MyItem item){ if(item.hasImage) return mTypeWithImage; else if(item.isDivider) return mTypeIsDivider; return mNormalType; } @Override public int getItemViewType(int position) { return getItemType(getItem(position)); } @Override public View getView(int position, View view, ViewGroup parent) { MyItem item = getItem(position); if(view == null) { int type = getItemType(item); if(type == mTypeWithImage) view = mInflater.inflate(mLayoutForImageItem, parent, false); else if(type == mTypeIsDivider) view = mInflater.inflate(mDividerLayout, parent, false); else if(type == mNormalType) view = mInflater.inflate(mItemLayout, parent, false); } // ... }
I’ll anything else I come across in the days to come.
If you got here before wasting hours bashing your head against this, you’re welcome if not, you have my condolences, I’m going to try and reassemble my own patience now and try to get on with it. Thanks for reading.