LoginSignup
0
1

More than 3 years have passed since last update.

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

Posted at

前書き

GridViewのカスタムレンダラー化は過去記事をご覧ください。
Xamarin Android GridViewのカスタムレンダラーの作成

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

やりたいこと

具体的に私の場合は、人(要素)に対して複数件のデータが紐づいているような一覧表示をしようとしました。
【完成系】
Screenshot_20200515-022507.png

実装方法

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>

完成

ビルドしてみてください。

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1