前書き
GridViewのカスタムレンダラー化は過去記事をご覧ください。
Xamarin Android GridViewのカスタムレンダラーの作成
今回は上記の記事で既にカスタムレンダラー化されたGridViewがある前提で、
そのGridViewを各要素の高さが定まっていないGridViewにカスタマイズしていきます。
やりたいこと
具体的に私の場合は、人(要素)に対して複数件のデータが紐づいているような一覧表示をしようとしました。
【完成系】
実装方法
GridViewは高さの違う要素がある状態でスクロールするとGridViewが消えるようなので(実装中発見しました)
要素の高さを、行の中で一番高いものにリサイズする実装を選びました。
実装
※私のnamespaceやプロジェクト名は自分のものに置き換えてください
PLC側
using System.Runtime.CompilerServices;
using Xamarin.Forms;
[assembly: InternalsVisibleTo("MyProject.Android")]
namespace MyProject.Views.Renderer
{
public class PCLCustomGridViewRenderer : ItemsView
{
}
}
XamlからPCLCustomGridViewRendererを呼び出します
<StackLayout xmlns:renderer="clr-namespace:MyProject.Views.Renderer">
<renderer:PCLCustomGridViewRenderer ItemsSource="{Binding Items}"/>
</StackLayout>
バインドしてるItemsの中身です。
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
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
描画前に各要素に高さの測定を指示します
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
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内の1要素
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フォルダに配置してください
<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フォルダに配置してください。
要素に枠を追加します。
<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 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>
完成
ビルドしてみてください。
コード量が多くなってしまいましたが、ジェネリクスを使えば、もっと汎用的なクラスにできると思います。