Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Xamarin

A Custom Grid-List Adapter

4.66/5 (20 votes)
29 Apr 2014CPOL2 min read 29.2K   1K  
Making a custom ListAdapter

Introduction

In this article, I will explain how I made my custom list-adapter for a list activity, to take benefits if you are interested in.

I needed a grid that looks different from all classic ones, I wanted two columns to load my Card Items but with no row alignment:

Image 1

Background

We will use the BaseAdapter and ListView, to make the hybrid look of the Cards gridView.

Using the Code

I. CardItem

First of all, let's define the Card Item sample.
It is composed of:

  • Title
  • Subtitle
  • Logo
C#
internal class CardItem
 {
     public string Title { get; set; }
     public string SubTitle { get; set; }
     public int ResId { get; set; }
 }

Its corresponding layout is as follows:

Image 2

C#
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_margin="16dp"
    android:layout_height="wrap_content"
    android:background="@drawable/card_background">
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:paddingTop="4dp"
        android:paddingRight="4dp"
        android:paddingLeft="4dp"
        android:text="Title"
        android:textSize="18dp"
        android:textColor="@android:color/black"
        android:id="@+id/card_title" />
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:paddingRight="6dp"
        android:paddingLeft="6dp"
        android:text="Subtitle"
        android:textSize="14dp"
        android:id="@+id/card_subtitle"
        android:textColor="#6D7B8D" />
    <ImageView
        android:id="@+id/card_logo"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="4dp"
        android:layout_width="fill_parent"
        android:layout_height="match_parent"
        android:background="@drawable/custom_background"
        android:layout_gravity="center"
        android:src="@drawable/android"
        android:scaleType="centerInside"
        android:padding="5dp" />
</LinearLayout>

II. Card Item BaseAdapter

The next step is to implement our BaseAdapter<IList<CardItem>> in our custom usage.

The custom view is similar to a gridView layout.

I created a RowView layout containing 4 cards layout items, and each card of them will have its random defined height. this screen shot will explain:

Image 3

Cards Layouts will have a single random width/height at creation, and will be stored, to get them back on scroll events.

This row layout is loaded dynamically in the CardsViewMaker.cs class, GenerateView(int position) will generate the cardItemsArray[position] layout.

C#
// Generate empty raw layout with no sizing and data..
public View GenerateView(int position)
        {
            //Call the inflater to inflate CardItem.axml Layout later and get their View..
            var inflater = _context.LayoutInflater;

            /* Our Row Layout: Horizontal Layout containing 2 Vertical Layout
               [   E1   ][   E2  ]
               [   E3   ][   E4  ]
            */
            // It will be the root view element, Linear Horizontal Layout

            var rootView = new LinearLayout(_context)
            {
                Orientation = Orientation.Horizontal
            };
       
            var rootParams = new AbsListView.LayoutParams
            (ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.WrapContent);
            rootView.LayoutParameters = rootParams;
            var card1 = (LinearLayout)inflater.Inflate(Resource.Layout.CardItem, null);
            var card2 = (LinearLayout)inflater.Inflate(Resource.Layout.CardItem, null);
            var card3 = (LinearLayout)inflater.Inflate(Resource.Layout.CardItem, null);
            var card4 = (LinearLayout)inflater.Inflate(Resource.Layout.CardItem, null);
            card1.Id = 1;
            card2.Id = 2;
            card3.Id = 3;
            card4.Id = 4;
            var leftView = new LinearLayout(_context)
            {
                Orientation = Orientation.Vertical
            };
            var leftParams = new AbsListView.LayoutParams
            (ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent);
            leftView.LayoutParameters = leftParams;
            leftView.AddView(card1);
            leftView.AddView(card2);
            var rightView = new LinearLayout(_context)
            {
                Orientation = Orientation.Vertical
            };
            var rightParams = new AbsListView.LayoutParams
            (ViewGroup.LayoutParams.WrapContent, ViewGroup.LayoutParams.WrapContent);
            rightView.LayoutParameters = rightParams;
            rightView.AddView(card3);
            rightView.AddView(card4);
            rootView.AddView(leftView);
            rootView.AddView(rightView);
            rootView.SetBackgroundColor(Android.Graphics.Color.ParseColor("#FFE5E5E5"));
            return rootView;
        }   

Now once the row (view) is generated (or recycled), it's ready to give it its new random size if it's newly created, or give it back its original size on generation, it's in the ReSize(View view, int position) and SetViewInfo(View view, int id, int position) functions:

C#
//used to store the generated sizes for each card item 
private readonly Dictionary<int, string> _sizes = new Dictionary<int, string>();  
//check if the card is alread fetched before, to give it back original generated size
public void ReSize(View view, int position)
        {
            if (_sizes.ContainsKey(position))
            {
                var gens = _sizes[position].Split(' ').Select(int.Parse);
                var enumerable = gens as int[] ?? gens.ToArray();
                SetSize(view, enumerable.ElementAt(0), enumerable.ElementAt(1));
            }
            else
            {//random values use to generate the CardItemLayout random size...
                var leftrnd = _gen.Next(30, 70);
                var rightrnd = _gen.Next(30, 70);
                SetSize(view, leftrnd, rightrnd);//Sizing function
                _sizes.Add(position, string.Format("{0} {1}", leftrnd, rightrnd));
            }

            var id = 4*position;

            SetViewInfo(view, 1, id);
            SetViewInfo(view, 3, id+1);
            SetViewInfo(view, 2, id+2);
            SetViewInfo(view, 4, id+3);
        } 

//leftRand and rightRand are random numbers used to generate cards height
public void SetSize(View view, int leftRand, int rightRand)
        {
            //get the screen width to divide half half cards with minus padding...
            var width = _context.WindowManager.DefaultDisplay.Width; 
            var card1 = view.FindViewById<LinearLayout>(1);
            var card2 = view.FindViewById<LinearLayout>(2);
            var card3 = view.FindViewById<LinearLayout>(3);
            var card4 = view.FindViewById<LinearLayout>(4);
            var bh = DefaultHeight + Pad;
            var bh1 = (int)(bh * leftRand / 100.0f - Pad / 2.0f);
            var bh2 = (int)(bh * (1 - leftRand / 100.0f) - Pad / 2.0f);
       
            //defining layout parameters for our cards
            var params1 = new LinearLayout.LayoutParams(width / 2 - 3 * Pad / 4, bh1);
            var params2 = new LinearLayout.LayoutParams(width / 2 - 3 * Pad / 4, bh2);
            
            bh1 = (int)(bh * rightRand / 100.0f - Pad / 2.0f);
            bh2 = (int)(bh * (1 - rightRand / 100.0f) - Pad / 2.0f);
            
            var params3 = new LinearLayout.LayoutParams(width / 2 - 3 * Pad / 4, bh1);
            var params4 = new LinearLayout.LayoutParams(width / 2 - 3 * Pad / 4, bh2);
            
            //Set the right geometric margin dimensions...
            params1.SetMargins(Pad / 2, Pad / 4, 0, Pad / 4);
            params2.SetMargins(Pad / 2, Pad / 4, 0, 0);
            params3.SetMargins(Pad / 2, Pad / 2, 0, Pad / 4);
            params4.SetMargins(Pad / 2, Pad / 4, 0, 0);
            //setting our layout parameters
            card1.LayoutParameters = params1;
            card2.LayoutParameters = params2;
            card3.LayoutParameters = params3;
            card4.LayoutParameters = params4;
        }
//used to fill our cards with some data
private void SetViewInfo(View view, int id, int position)
        { 
            //if the position of resultant card is higher than the data items count
           // do not fill the card, hide it.
             if (position >= _items.Count)
                view.FindViewById(id).Visibility = ViewStates.Invisible;
            else 
            { // setting title, subtitle and logo 
                var card = _items[position];
                view.FindViewById(id).FindViewById<TextView>
                (Resource.Id.card_title).Text = card.Title; 
                view.FindViewById(id).FindViewById<TextView>
                (Resource.Id.card_subtitle).Text = card.SubTitle; 
                view.FindViewById(id).FindViewById<ImageView>
                (Resource.Id.card_logo).SetImageResource(card.ResId);
                view.FindViewById(id).Visibility = ViewStates.Visible;
            } 
        }       

Now let's dig into the CardItemsAdapter that implements the BaseAdapter.

It is important to know that override int Count must returns the rows count for the listview.

So if we have "X" card elements, we will have at least X/4 rows if X is a multiple of 4, else we'll have 1 + X/4 rows.

The override override View GetView(int position, View convertView, ViewGroup parent) will return the row view layout to the listview when scrolling or fetching rows data.
We must recycle fetched views, and reuse them for better performance and memory usage.

C#
internal class CardItemsAdapter : BaseAdapter<CardItem>
   {
        private readonly IList<CardItem> _values;
        private readonly CardsViewMaker _gen;
        public CardItemsAdapter(Activity context, IList<CardItem> values)
        {
            _gen = new CardsViewMaker(context,values);
            _values = values;
        }
        public CardItemsAdapter(Activity context,int height, int spacing, IList<CardItem> values)
        {
            _gen = new CardsViewMaker(context,height, spacing, values);
            _values = values;
        }
        public override CardItem this[int position]
        {
            get { return _values[position]; }
        }
        public override long GetItemId(int position)
        {
            return position;
        }
        public override View GetView(int position, View convertView, ViewGroup parent)
        {
       //check if convertView is null => Generate a new View
       // if it's not null, recycle, reuse it..
           var view = convertView ?? _gen.GenerateView(position);
            
          // tell the CardViewMaker _gen to Resize and fill the card data
          _gen.ReSize(view, position); 
            return view;
        }
        public override int Count
        {
            get
            {
                return _values.Count / 4 + (_values.Count % 4 == 0 ? 0 : 1);
            }
        }
    }    

Finally, let's test the Cards adapter in a ListActivity:

C#
var mAdapter = CardItem.GenerateSampleCardItems();//Generate Sample Cards Data
ListAdapter = new CardItemsAdapter(this, mAdapter ); 
ListView.DividerHeight = 0; // Hide the divider between the rows       

Image 4

C#
// it's the manifest, compile with ics,15 minimum sdk, with light theme.
// remove android:theme="@android:style/Theme.DeviceDefault.Light" for a lower version.. 
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
android:versionCode="1" android:versionName="1.0" package="CustomAdapter.CustomAdapter">
    <uses-sdk android:targetSdkVersion="15" android:minSdkVersion="15" />
    <application android:theme="@android:style/Theme.DeviceDefault.Light" 
android:label="CustomAdapter" android:icon="@drawable/icon"></application>
</manifest>  

Points of Interest

Hope it's a handy work that you will use. Thanks.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)