Custom row appearance
Let's get back to the ListAdapter
implementation and design our ListView
row appearance. Open the Resources | Layout folder, create a new .xml
file for the cell appearance, call it CustomCell.xml
, and copy in the following XML code:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent" android:weightSum="4"> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1"> <ImageView android:id="@+id/image" android:layout_width="100dp" android:layout_height="100dp" android:adjustViewBounds="true" /> </LinearLayout> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="3" android:weightSum="2"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" /> <TextView android:id="@+id/date" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" /> </LinearLayout> </LinearLayout>
We are creating the same layout as the custom cell made for iOS, but in Android we will use the ImageView
and TextView
objects. Now that we have our custom cell, we can implement the the GetView
function. The GetView
function is exactly like the GetCell
function in the preceding UITableSource
implementation. Open up the ListAdapter.cs
file and continue with the list adapter implementation:
public class ListAdapter : BaseAdapter { private List<GalleryItem> _items; private Activity _context; public ListAdapter(Activity context) : base() { _context = context; _items = new List<GalleryItem>(); } public override Java.Lang.Object GetItem (int position) { return null; } public override long GetItemId(int position) { return position; } public override int Count { get { return items.Count; } } }
We override the Count
property and functions GetItemId
and GetItem
, to return the number of gallery items in our list. These override functions are exactly the same as the overrides in Java for any BaseAdapter
inherited class. Now for the GetView
function:
public override View GetView(int position, View convertView, ViewGroup parent) { View view = convertView; // re-use an existing view, if one is available if (view == null) { // otherwise create a new one view = context.LayoutInflater.Inflate(Resource.Layout.CustomCell, null); } // set image var imageView = view.FindViewById<ImageView> (Resource.Id.image); BitmapHelpers.CreateBitmap (imageView, _items [position].ImageData); // set labels var titleTextView = view.FindViewById<TextView> (Resource.Id.title); titleTextView.Text = _items[position].Title; var dateTextView = view.FindViewById<TextView> (Resource.Id.date); dateTextView.Text = _items[position].Date; return view; } private async void createBitmap(ImageView imageView, byte[] imageData) { try { if (imageData != null) { var bm = await BitmapFactory.DecodeByteArrayAsync(imageData, 0, imageData.Length); if (bm != null) { imageView.SetImageBitmap(bm); } } } catch (Exception e) { Console.WriteLine ("Bitmap creation failed: " + e); } }
Notice in the GetView
function we are using the CustomCell
layout for each row; we also have a private
method for creating our bitmaps from each model's byte array.
If we have a look at the current implementation, what do we notice here?
We are creating a bitmap every time the cell requires this data again for the view; is this efficient? No, we should be reusing bitmaps and memory as much as possible.
This tends to be a common issue with Android ListView
.
What is the most memory efficient way to reuse bitmaps across hundreds of items in a ListView
while scrolling and staying smooth as we move down the list at various speeds? How can we tackle this problem? Let's have a look at how we can approach this problem.
Firstly, we need to implement an object called ImageHandler
. This will contain the logic for retrieving byte arrays from all gallery images on an Android device. Create a new file, name it ImageHandler
, and start importing these namespaces:
namespace Gallery.Droid { using System; using System.Collections.Generic; using Android.Database; using Android.Content; using Android.Provider; using Gallery.Shared; public static class ImageHandler { } }
This class will include a function, GetFiles
, which will create gallery items based upon the items pulled from any device's gallery using the ContentResolver
interface:
public static IEnumerable<GalleryItem> GetFiles(Context context) { ContentResolver cr = context.ContentResolver; string[] columns = new string[] { MediaStore.Images.ImageColumns.Id, MediaStore.Images.ImageColumns.Title, MediaStore.Images.ImageColumns.Data, MediaStore.Images.ImageColumns.DateAdded, MediaStore.Images.ImageColumns.MimeType, MediaStore.Images.ImageColumns.Size, }; var cursor = cr.Query(MediaStore.Images.Media.ExternalContentUri, columns, null, null, null); int columnIndex = cursor.GetColumnIndex(columns[2]); int index = 0; // create max 100 items while (cursor.MoveToNext () && index < 100) { index++; var url = cursor.GetString(columnIndex); var imageData = createCompressedImageDataFromBitmap (url); yield return new GalleryItem () { Title = cursor.GetString(1), Date = cursor.GetString(3), ImageData = imageData, ImageUri = url, }; } }
Using ContentResolver
(used to access the content model), we resolve URIs to specific content providers. A content provider provides queries to content, in our case image files. We simply create an access query off the main context's ContentResolver
instance, and we provide an array of columns for the query to retrieve (for example, file titles, file data, file size, and so on). The first parameter is as follows:
"MediaStore.Images.Media.ExternalContentUri"
This is used for retrieving the URI to each piece of content returned from the query. Finally, we now have a cursor to iterate through, exactly like an Enumerable
, which will loop to the end until there are no more items, and for each iteration we pull the data and URI columns and create a new GalleryItem
. You will notice a little trick here with the yield
keyword: if we call this function, it will actually return the entire Enumerable
from start to finish. Calling the function starts for each-ing over the object; the function is called again until it yields. In the return from calling this function, we get an Enumerable
of all the items retrieved from the query as gallery items with image information and local URI.