Headers and Footers on an MvxListView

Full source for this post can be found on GitHub:  HeaderedGridView

Last week I posted about adding a header to a GridView.  Today I’ll be discussing how to add a header (and footer!) to a MvxListView.  The MvxListView is a data-binding friendly ListView from MvvmCross.  Adding a header here is a little different because we need an instance of HeaderViewListAdapter that implements IMvxAdapter .  Other than that, the code should be very familiar.

Requirements

Just so it is clear what we are building, here are my requirements for this control:

  1. Ability to add a header and footer to the MvxListView in xml markup
  2. The header is to be laid out inline with the rest of the items, at the top
  3. The footer is to be laid out inline with the rest of the items, at the bottom
  4. The header and footer need to scroll with the rest of the content, i.e., they are not always visible
  5. The header and footer must be able to use MvvmCross bindings

Some quick screenshots from the sample project will help us understand what this should look like.

top.png

The list is scrolled to the top with purple header visible

middle.png

Shows the list at some point in the middle when neither header nor footer is visible

bottom.png

The list scrolled all the way to the bottom with the green footer visible

Code

Full source for this post can be found on GitHub:  HeaderedGridView

The MvxListView assumes that the adapter it has implements IMvxAdapter.  We can create a simple wrapper class that inherits from HeaderViewListAdapter and implements that interface.  The constructor will accept the list of headers and footers like the base class in addition to accepting an IMvxAdapter to wrap.  All of the interface methods for IMvxAdapter can then be passed down to the internal adapter.

In the snippet of HeaderMxAdapter above we have two constructors.  The first can be used if you do not want to add a footer.  It simply calls into the second passing an empty list of footers to add to the adapter.  The only other thing the constructor does is to squirrel away the adapter in a private field for later use.

The implementation for DropDownItemTemplateId is simple and indicative of the rest of the methods in the class.  It merely calls into that squirreled away adapter, calling its implementation of DropDownItemTemplateId.  The rest of the methods are exactly the same and have been omitted for brevity.

Next step is to add a pair of attributes to use for our header and footer.  This is exactly the same as the headered grid view and is done in attrs.xml.

Some boiler-plate code in MvxListViewWithHeader will read these values so we know what views to use for the header and footer.  Like last time, I’m using some helper classes to do the parsing.  They are included in the project.

Next we need to create the views for the header and footer .

First we check if the id is the default header id, if it is then we don’t have a view defined in the xml, and we just return null.  After that we get the current binding context and use it to inflate our view.  This is the magic that lets us bind within our header and footer.

Once we have the view, we need to wrap it in a list of FixedViewInfo which is what the HeaderMxAdapter expects in its constructor.

Nothing earth shattering here.  If the view is null, return an empty list, otherwise construct the FixedViewInfo and add it to the list.  Returning an empty list allows us to specify only the header, only the footer, or neither without causing any errors.

A couple of helper methods make help to make it clear and concrete what GetFixedViewInfos does.

All of this is called from the constructor which creates the MvxAdapter and wraps it in our new HeaderMvxAdapter.  It then set the adapter property in the base class.

The constructor is also responsible for reading in the item template id which the MvxListView uses to display the individual items in the list.

Using the Control

You would use the MvxListViewWithHeader the same way as a MvxListView.  The only difference is that you can now optionally specify a header or footer attribute which would point to another layout.  The following is taken from the FirstView in the sample.

The header and footer layout in the sample are very simple.  They are just a TextView with the text property bound to the view model.  Here is the header layout, but the footer is almost identical:

Future Improvements

Like the headered grid view, this implementation only allows a single header. If we had need for multiple headers we could copy the  onFinishedInflate method from ListView.  It collects all the child layouts for the list view and adds them to the list of headers.

Happy Coding

Headered Grid View in MvvmCross. Now with bindings!

Full source code for this article can be downloaded on GitHub:  HeaderedGridView.

For the easy import feature of Yarly, I needed to create a view that would easily allow a user to select an existing photo to import into the app or to take new photo. A grid view of photos from the camera roll but with the first item as a button that would load the camera would serve nicely for this use case. See the screen shot below of an early version of the UI:

Yarly Screen Shots 2014-04-20_194747_242000

This reminded me of the AddHeaderView method available on ListView. With that as a guide, I decided to add similar functionality to the GridView.  Since I’m using MvvmCross, I also want to make sure that I can use bindings in the header control. While I’m at it, having to explicitly call AddHeaderView in my activity code is useful, but I’d much rather to be able to do this in markup. 

Requirements

To sum up the above requirements:

  1. Ability to add a header to a GridView in xml markup
  2. The header is laid out inline with the rest of the items in the GridView
  3. The header scrolls with the rest of the content, i.e., it is not always visible
  4. The header must be able to use MvvmCross bindings

Code

A full sample project can be downloaded from my GitHub repo:  HeaderedGridView.

In attrs.xml, add an attribute to use for our header. 

<!-- attrs.xml --> <declare-styleable name="GridView">

   <attr name="header" format="reference" /> </declare-styleable>

In the HeaderedGridView, all we need to do now is check for the presence of this attribute, and if it exists, inflate the view.  Reading the header id is straight forward processing of the IAttributeSet.  I use some helper classes to iterate and dispose of the attributes in a more C# way.  The details aren’t very important, but the relevant classes are included in the sample solution.  The MvvmCross class MvxAndroidBindingContextHelpers can return the current binding context which can be used to inflate and bind the header at the same time.

// HeaderedGridView.cs private void ProcessAttrs(Context c, IAttributeSet attrs) {

     _headerId = DEFAULT_HEADER_ID;

     using (var attributes = c.ObtainDisposableStyledAttributes(attrs, Resource.Styleable.GridView))

     {

         foreach (var a in attributes)

         {

             switch (a)

             {

                 case Resource.Styleable.GridView_header:

                     _headerId = attributes.GetResourceId(a, DEFAULT_HEADER_ID);

                     break;

             }

 

         }

     } } private void LoadHeader() {

     if (_headerId == DEFAULT_HEADER_ID) return;

     IMvxAndroidBindingContext bindingContext = MvxAndroidBindingContextHelpers.Current();

     _header = bindingContext.BindingInflate(_headerId, null); }

Now that we have a header, we can wrap our current adapter in the HeaderViewListAdapter.  As the name implies, this is the exact same adapter used by the ListView.  It handles knowing when and where to show the header.  In my sample code, I have the grid create the adapter directly, but this can work just as well if an adapter is passed in from outside.

// HeaderedGridView.cs

private IListAdapter GetAdapter() {

     var headerInfo = GetHeaders();

     ICursor cursor = ImageAdapter.CreateCursor(Context);

     IListAdapter adapter = new ImageAdapter(Context, cursor);

 

     if (headerInfo != null)

     {

         adapter = new HeaderViewListAdapter(headerInfo, null, adapter);

     }

     return adapter; }

So I don’t have to hard code sizes and so the header matches the rest of the items in the grid, I set the height and width once the grid is being laid out.  In the OnMeasure method we check if the header isn’t null, and if the ColumnWidth doesn’t match the previous column width we saw.  Caching the width and testing this prevents us from setting the layout parameters when we don’t have to; OnMeasure is called multiple times.

// HeaderedGridView.cs protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) {

     base.OnMeasure(widthMeasureSpec, heightMeasureSpec);

 

     if (_header != null && base.ColumnWidth != _cachedColumnWidth)

     {

         _cachedColumnWidth = base.ColumnWidth;

         _header.LayoutParameters = new ViewGroup.LayoutParams(_cachedColumnWidth, _cachedColumnWidth);

     } }

Now all we need to do is include our grid in a layout.   It’s almost exactly the same as the regular GridView, except we can now optionally specify a header.  This is from the FirstView layout in the sample.

<!--FirstView.axml--> <HeaderedGridView

   android:layout_width="fill_parent"

   android:layout_height="fill_parent"

   android:layout_weight="1"

   android:numColumns="3"

   android:verticalSpacing="10dp"

   android:horizontalSpacing="10dp"

   android:stretchMode="columnWidth"

   android:gravity="center"

   android:fastScrollEnabled="true"

   android:background="#000000"

   local:header="@layout/gridheader" />

The gridheader layout is just a simple layout with a single image button button.  Note that we are binding the click of the button to the ClickCommand in our view model.

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=http://schemas.android.com/apk/res/android

     xmlns:local=http://schemas.android.com/apk/res-auto

     android:orientation="vertical"

     android:layout_width="fill_parent"

     android:layout_height="fill_parent"

     android:padding="5dp">

     <ImageButton

         android:layout_width="fill_parent"

         android:layout_height="fill_parent"

         android:background="#90C53E"

         android:src="@drawable/camera"

         local:MvxBind="Click ClickCommand" /> </LinearLayout>

Firing up the sample solution and we should see something similar to:

Yarly Screen Shots 2014-04-21_184439_262000 

Future Improvements

My implementation restricts me to only being able to add one header.  While one header is currently good enough for me, if I wanted to expand it add more, I could emulate ListView a bit more.  Specifically the onFinishedInflate method which adds child layouts as a list of headers.  Of course, I could also add an explicit AddHeaderView method that could be called multiple times.

I’ve ignored footers entirely.  Again, this is just because I don’t currently have any need for a footer.  They could be added easily enough following the same pattern as headers.

Happy Coding.