Xamarin Android 各要素の高さが定まっていないGridViewのCustomRenderer作成

#前書き
GridViewのカスタムレンダラー化は過去記事をご覧ください。
[Xamarin Android GridViewのカスタムレンダラーの作成](https://qiita.com/smi/items/c5ee5821d8dae52c41eb)

今回は上記の記事で既にカスタムレンダラー化されたGridViewがある前提で、
そのGridViewを各要素の高さが定まっていないGridViewにカスタマイズしていきます。

#やりたいこと
具体的に私の場合は、人(要素)に対して複数件のデータが紐づいているような一覧表示をしようとしました。
【完成系】
![Screenshot_20200515-022507.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/543a7327-e389-056a-f6b2-3623e73ed823.png)



#実装方法
GridViewは高さの違う要素がある状態でスクロールするとGridViewが消えるようなので（実装中発見しました）
要素の高さを、行の中で一番高いものにリサイズする実装を選びました。

#実装

※私のnamespaceやプロジェクト名は自分のものに置き換えてください

##PLC側

```C#
using System.Runtime.CompilerServices;
using Xamarin.Forms;

[assembly: InternalsVisibleTo("MyProject.Android")]
namespace MyProject.Views.Renderer
{
    public class PCLCustomGridViewRenderer : ItemsView
    {
    }
}
```

XamlからPCLCustomGridViewRendererを呼び出します

```C#
<StackLayout xmlns:renderer="clr-namespace:MyProject.Views.Renderer">
    <renderer:PCLCustomGridViewRenderer ItemsSource="{Binding Items}"/>
</StackLayout>
```
バインドしてるItemsの中身です。

```C#
IList<Item> Items;

public class Item
{
    public string Name { get; set; }
    public ObservableCollection<ItemDetail> Details { get; set; }
}

public class ItemDetail
{
   public string Text { get; set; }
}
```

##Xamarin.Android側

###AndroidCustomGridViewRenderer.cs

```C#
using Android.Content;
using Android.Support.V7.View;
using Android.Widget;
using MyProject.Droid.Renderer;
using MyProject.Droid.Views;
using MyProject.Droid.Views.Adapter;
using MyProject.Views.Renderer;
using System.ComponentModel;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

//互いに参照できるよう定義
[assembly: ExportRenderer(typeof(PCLCustomGridViewRenderer), typeof(AndroidCustomGridViewRenderer))]
namespace MyProject.Droid.Renderer
{
    public class AndroidCustomGridViewRenderer : ViewRenderer<PCLCustomGridViewRenderer, GridView>
    {
        private AndroidCustomGridViewAdapter adapter;
        public AndroidCustomGridViewRenderer(Context context) : base(context) { }

        // 生成時一度だけ呼ばれるイベント
        protected override void OnElementChanged(ElementChangedEventArgs<PCLCustomGridViewRenderer> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null)
            {
                if (adapter != null)
                    adapter.Element = null;
            }

            if (e.NewElement != null)
            {
                if (Control == null)
                {
                    // Adpterの生成
                    adapter = new AndroidCustomGridViewAdapter(Context);

                    // GridViewの生成
                    var gridView = new CustomGridView(new ContextThemeWrapper(Context, Resource.Style.VerticalScrollbarRecyclerView));
                    // GridViewのパラメータを設定(親サイズに追従する設定)
                    gridView.LayoutParameters = new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent);
                    // カラム数
                    gridView.NumColumns = 3;
                    // Viewに対しAdpterを設定
                    gridView.Adapter = adapter;

                    //多分コントロールを実際に生成
                    SetNativeControl(gridView);
                }

                if (adapter != null)
                    // NewElementにはIList<Person>が入ってきます
                    // ElementはAdpterクラスが私が定義しました。
                    adapter.Element = e.NewElement;
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == ItemsView.ItemsSourceProperty.PropertyName)
            {
                adapter?.UpdateItems();
            }
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
            adapter.Dispose();
        }
    }
}
```

###CustomGridView.cs
描画前に各要素に高さの測定を指示します

```C#
using Android.Content;
using Android.Util;
using Android.Widget;
using MyProject.Droid.Views.Adapter;

namespace MyProject.Droid.Views
{
    class CustomGridView : GridView
    {
        public CustomGridView(Context context) : base(context)
        {
        }

        public CustomGridView(Context context, IAttributeSet attrs) : base(context, attrs)
        {
        }

        public CustomGridView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle)
        {
        }

        protected override void OnLayout(bool changed, int l, int t, int r, int b)
        {
            // グリッドビューの表示時に、各要素の高さを図る
            AndroidCustomGridViewAdapter adapter = (AndroidCustomGridViewAdapter)Adapter;
            // 列数と、要素数の設定
            CustomGridViewLinearLayout.initItemLayout(NumColumns, adapter.Count);
            int columnWidth = MeasuredWidth / NumColumns;
            // 各要素の高さを測定
            adapter.measureItems(columnWidth);
            base.OnLayout(changed, l, t, r, b);
        }
    }
}
```

###AndroidCustomGridViewAdapter.cs 

```C#
using System.Collections.ObjectModel;
using System.Linq;
using Android.Content;
using Android.Views;
using Android.Widget;
using MyProject.Models;
using MyProject.Views.Renderer;

namespace MyProject.Droid.Views.Adapter
{
    class AndroidCustomGridViewAdapter : BaseAdapter
    {
        private PCLCustomGridViewRenderer element;
        public PCLCustomGridViewRenderer Element
        {
            get => element;
            set
            {
                element = value;
                UpdateItems();
            }
        }

        Context context;

        private ObservableCollection<Item> viewModels = new ObservableCollection<Item>();

        public AndroidCustomGridViewAdapter(Context c)
        {
            context = c;
        }

        public override int Count
        {
            get { return viewModels.Count(); }
        }

        public override Java.Lang.Object GetItem(int position)
        {
            return null;
        }

        public override long GetItemId(int position)
        {
            return 0;
        }
        public void UpdateItems()
        {
            viewModels.Clear();

            if (Element?.ItemsSource != null)
            {
                foreach (var item in Element.ItemsSource)
                {
                    if (item is Item model)
                    {
                        viewModels.Add(model);
                    }
                }
            }

            NotifyDataSetChanged();
        }

        public override Android.Views.View GetView(int position, Android.Views.View convertView, ViewGroup parent)
        {
            var view = (CustomGridViewLinearLayout)LayoutInflater.From(parent.Context).Inflate(Resource.Layout.custom_gridview_item, parent, false);
            view.setPosition(position);
            // 要素の作成
            SetUp(view, viewModels[position]);

            return view;
        }

        private void SetUp(Android.Views.View view, Item model)
        {
            var name = view.FindViewById<TextView>(Resource.Id.name);
            var details = view.FindViewById<LinearLayout>(Resource.Id.details);

            name.Text = model.Name;
            details.RemoveAllViews();
            foreach (var detail in model.Details)
            {
                var textView = new TextView(view.Context);
                textView.Text = detail.Text;
                details.AddView(textView);
            }
        }

        /// <summary>
        /// 描画の前に各要素の高さを図ります
        /// </summary>
        /// <param name="columnWidth">列一つ分の幅</param>
        public void measureItems(int columnWidth)
        {
            LayoutInflater inflater = (LayoutInflater)context.GetSystemService(Context.LayoutInflaterService);
            CustomGridViewLinearLayout itemView = (CustomGridViewLinearLayout)inflater.Inflate(Resource.Layout.custom_gridview_item, null);

            int widthMeasureSpec = Android.Views.View.MeasureSpec.MakeMeasureSpec(columnWidth, MeasureSpecMode.Exactly);
            int heightMeasureSpec = Android.Views.View.MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified);

            for (int index = 0; index < viewModels.Count; index++)
            {
                Item item = viewModels[index];

                // 測定用のViewを設定
                itemView.setPosition(index);
                SetUp(itemView, item);

                // 強制的にViewのOnMeasureイベントを発火
                itemView.RequestLayout();
                itemView.Measure(widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
```

###CustomGridViewLinearLayout.cs
GridView内の１要素

```C#
using Android.Content;
using Android.Util;
using Android.Widget;

namespace MyProject.Droid.Views
{
    class CustomGridViewLinearLayout : LinearLayout
    {
        private static int[] maxRowHeight;

        private static int numColumns;

        private int position;

        public CustomGridViewLinearLayout(Context context) : base(context)
        {
        }

        public CustomGridViewLinearLayout(Context context, IAttributeSet attrs) : base(context, attrs)
        {
        }

        public void setPosition(int position)
        {
            this.position = position;
        }

        public static void initItemLayout(int numColumns, int itemCount)
        {
            CustomGridViewLinearLayout.numColumns = numColumns;
            maxRowHeight = new int[itemCount];
        }

        /// <summary>
        /// 自分の高さを計測、同じ行に自分より高い要素があれば、高さを上書き
        /// </summary>
        /// <param name="widthMeasureSpec"></param>
        /// <param name="heightMeasureSpec"></param>
        protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
        {
            base.OnMeasure(widthMeasureSpec, heightMeasureSpec);
            if (numColumns <= 1 || maxRowHeight == null || maxRowHeight.Length == 0)
            {
                return;
            }

            int rowIndex = position / numColumns;
            int measuredHeight = MeasuredHeight;
            if (measuredHeight > maxRowHeight[rowIndex])
            {
                maxRowHeight[rowIndex] = measuredHeight;
            }
            SetMeasuredDimension(MeasuredWidth, maxRowHeight[rowIndex]);
        }
    }
}
```

###custom_gridview_item.xml
Resources/layoutフォルダに配置してください

```XML
<MyProject.Droid.Views.CustomGridViewLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:background="@drawable/gridview_item_frame"
    android:padding="7dp"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

        <TextView
            android:id="@+id/name"
            android:singleLine="true"
            android:text="aaaa"
            android:textSize="18dp"
            android:textColor="#464646"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <LinearLayout
            android:id="@+id/details"
            android:layout_marginTop="10dp"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

</MyProject.Droid.Views.CustomGridViewLinearLayout>
```

###gridview_item_frame.xml
Resources/drawbleフォルダに配置してください。
要素に枠を追加します。

```XML
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">

  <stroke android:width="1dp" android:color="#e8d689"/>
  <solid android:color="@android:color/white" />
  <corners android:radius="1dp" />
</shape>

```
###custom_gridview_item_detail.xml
Resources/layoutフォルダに配置してください

```XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/text"
        android:text="ああああああああああああああああ"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"    
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

```

#完成
ビルドしてみてください。

コード量が多くなってしまいましたが、ジェネリクスを使えば、もっと汎用的なクラスにできると思います。