Xamarin Android GridViewのカスタムレンダラーの作成

#前書き
XamarinもAndroidも初めてで、両方の知識が必要なCustomRendererを作成するのに手こずったので書き残しました。
冗長な書き方や間違いがある可能性が高いです。

※CustomRendererはPCL側のXamlで実装できない、できたけど速度が遅い。みたいなときに各プラットフォームごとの固有の書き方をすることで改善することができる機能。（今回はAndroidなのでC#、xmlで実装していきます）

#完成形
![Screenshot_20200513-005743.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/399ee715-c2c3-6764-fb23-f7747cdee583.png)


#実装
※私のnamespaceやプロジェクト名は自分のものに置き換えてください
##PCL側
PCLとXamarin.Androidを紐づけるためのクラスを用意します。

PCLCustomGridViewRenderer.cs

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

[assembly: InternalsVisibleTo("MyProject.Android")]
namespace MyProject.Views.Renderer
{
    public class PCLCustomGridViewRenderer : ItemsView
    {
    }
}
```
↑
ItemsViewは配列を受け取るための抽象クラス。継承してやることでXamlからPCLCustomGridViewRendererに対して配列を渡すことができる。(配列はそのままAndroid側の実装で扱えます)

XamlからPCLCustomGridViewRendererを呼び出します。

```C#
<StackLayout xmlns:renderer="clr-namespace:MyProject.Views.Renderer">
    <renderer:PCLCustomGridViewRenderer ItemsSource="{Binding Items}"/>
</StackLayout>
```
※PCLCustomGridViewRendererが見つからないエラーが出る場合はプロジェクトをリビルドしてみてください。

ちなみにItems内のデータ構成はObservableCollectionになっています。

```C#
ObservableCollection<Item> Items;

class Item
{
    public string Text { get; set; }
}
```
PCL側の実装は以上です。

##Xamarin.Android側
3つほどクラスファイルを用意します。
1.Android側で配列を受け取りGridViewを生成するクラス。
2.Androidの世界で出てくるAdpterクラス。データとxmlを紐づけるためのクラス。
3.GridView内に配置する要素のxmlファイル

1.AndroidCustomGridViewRenderer.cs
エラー線が出る箇所がありますが、進めていく際に治るので、一旦無視してください。

```C#
using Android.Content;
using Android.Support.V7.View;
using Android.Widget;
using MyProject.Droid.Renderer;
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 GridViewAdapter 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 GridViewAdapter(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();
        }
    }
}
```

2.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();
        }

        // Countプロパティの数だけ呼ばれるViewを生成する関数
        public override Android.Views.View GetView(int position, Android.Views.View convertView, ViewGroup parent)
        {
            var view = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.custom_gridview_item, parent, false);
            // 要素の作成
            SetUp(view, viewModels[position]);

            return view;
        }

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

            text.Text = model.Text;
        }
    }
}
```

3.custom_gridview_item.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>
```

まだエラー線が出ている箇所があると思うので定義を追加します。
Resources/values/styles.xmlのresourcesタグの中に以下を追加します

```XML
<style name="VerticalScrollbarRecyclerView" parent="android:Widget">
  <item name="android:scrollbars">vertical</item>
</style>
```
#完成
ビルドしてみてください。


もしプロジェクトの実行の際に以下のエラーが出た場合

```
System.InvalidOperationException: 'The class, property, or method you are attempting to use ('VerifyCollectionViewFlagEnabled') is part of CollectionView; to use it, you must opt-in by calling Forms.SetFlags("CollectionView_Experimental") before calling Forms.Init()
```
MyProject.AndroidプロジェクトのMainActivity.csに１行追加してください

```C#
global::Xamarin.Forms.Forms.SetFlags("CollectionView_Experimental");　←下の行より前にこの行を追加
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);　　　　　　
```

#あとがき
今回はただテキストを表示するだけでしたが、Itemクラスを拡張したり、xmlを変えたりすることで自由な表現ができると思います。

[Xamarin Android 各要素の高さが定まっていないGridViewのCustomRenderer作成](https://qiita.com/smi/items/53114ca29b8a63e3f1ce)