kamulog

xamarin.formsのネタなど

Xamarin.Formsでほぼ最小構成のListViewを作ってみた(2.3.5.256-pre6を使用)

iOSのUITableView / AndroidのListViewの基本的な動きを覚えたくてXamarin.Formsでの最小構成でのCustomRendererを作ってみました。 諸事情によりpreを使うことになりましたが、とりあえず無事動作しました。

これをベースとすれば、いらんことをしないオレオレ仕様のListViewに育てることができると思います。

リポジトリ

github.com

f:id:kamusoft:20170818213149p:plain

仕様

  • FormsはListViewのサブクラスを使う
  • NativeのRendererはListViewRendererは使わずViewRendererを使う
  • ItemsSourceとItemTemplateを指定して表示するだけ
  • CachingStrategyはRetain固定
  • ViewCellに対応

Forms側 実装

public class MinimumListView : ListView
{
    //Retain固定
    public MinimumListView() : base(ListViewCachingStrategy.RetainElement){}
}

以上ですw
CachingStrategyに関してはRecycleが何やってるのかさっぱりなのでRetain固定です。

iOS実装

public class MinimumListViewRenderer : ViewRenderer<MinimumListView, UITableView>
{
    protected override void OnElementChanged(ElementChangedEventArgs<MinimumListView> e)
    {
        base.OnElementChanged(e);

        if (e.NewElement != null) {
            var tableView = new UITableView();
            tableView.ScrollEnabled = true;
            tableView.RowHeight = 44;
            tableView.Source = new MyTableSource(Element);

            SetNativeControl(tableView);
        }
    }
}

public class MyTableSource : UITableViewSource
{
    MinimumListView _listview;
    IList _source;

    public MyTableSource(MinimumListView listview)
    {
        _listview = listview;
        _source = _listview.ItemsSource as IList;
    }

    public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
    {
        //FormsCell取得 TemplatedItemsはEditorBrowsableState.Neverなのでインテリセンスに出てこない
        var cell = _listview.TemplatedItems[indexPath.Row];
        //var cell = (Cell)_listview.ItemTemplate.CreateContent(); //XF2.3.4まででもこの方法で取れるがViewCellがうまく表示されない
       
        var id = cell.GetType().FullName;
        var renderer = (CellRenderer)Xamarin.Forms.Internals.Registrar.Registered.GetHandler<IRegisterable>(cell.GetType());

        //リサイクルCell取得
        var reusableCell = tableView.DequeueReusableCell(id);
        //NativeCell取得
        var nativeCell = renderer.GetCell(cell, reusableCell, tableView);

        return nativeCell;
    }

    //セクションの数
    public override nint NumberOfSections(UITableView tableView)
    {
        return 1;  
    }

    //1セクションの行数
    public override nint RowsInSection(UITableView tableview, nint section)
    {
        return _source.Count;
    }
}

ここの肝はGetCellのTemplatedItemsからFormsのCellを取得しているところですね。コメントにも書いてますがXamarin.Forms.Internalsのメンバーの多くはEditorBrowsable属性でNeverが指定されているのでインテリセンスに出てこないので注意です。直打ちかコピペで入力するしかないです。
この方法で取得したCellはViewCellでもちゃんと表示できる状態になっています。

コメントアウトされている方のCreateContentで取得した場合は何か設定が足りないCellが返ってきてViewCellが正しく表示されません。ViewCell以外の組み込みCellの場合は問題なく表示できるので、ViewCellが必要なければよりシンプルにできますね。

さらに言うと、FormsのCellを使わずにここに直接NativeCellを組み込んだら、よりシンプルかつパフォーマンスの良いものになると思います。

ここではCellを使うものとしてXFのソースから拝借したコードを使ってリサイクルNativeCellを取得して、それをCellRendererに渡してそっちのGetCellの結果をもらって、それを返す動きにしています。
「リサイクルだったらこれで、新規だったらNewして云々」の処理はCellRenderに任せます。

グループは使わないのでNumberOfSectionsは常に1を返し、総行数をRowsInSectionで返すようにしています。

Android実装

public class MinimumListViewRenderer : ViewRenderer<MinimumListView, Android.Widget.ListView>
{
    protected override void OnElementChanged(ElementChangedEventArgs<MinimumListView> e)
    {
        base.OnElementChanged(e);

        if (e.NewElement != null) {
            var listview = new Android.Widget.ListView(Context);
            listview.Focusable = false;
            listview.DescendantFocusability = DescendantFocusability.AfterDescendants;
            listview.Adapter = new MyListViewAdapter(Context, Element);
            AutoPackage = false;
            SetNativeControl(listview);
        }
    }
}

public class MyListViewAdapter : BaseAdapter<object>
{
    MinimumListView _listview;
    IList _source;
    Context _context;

    public MyListViewAdapter(Context context, MinimumListView listview)
    {
        _context = context;
        _listview = listview;
        _source = _listview.ItemsSource as IList;
    }

    public override Android.Views.View GetView(int position, Android.Views.View convertView, ViewGroup parent)
    {
        //FormsCell取得 TemplatedItemsはEditorBrowsableState.Neverなのでインテリセンスに出てこない
        var formsCell = _listview.TemplatedItems[position];
       
        //NativeCell取得(AndroidはCellFactoryなる便利なものがあったのでありがたく使わせてもらう)
        var nativeCell = CellFactory.GetCell(formsCell, convertView, parent, _context, _listview);

        //セルの高さ設定
        //(ViewCellはこの指定では反映されないのでListViewのRowHeightプロパティで設定する)
        nativeCell.SetMinimumHeight((int)_context.ToPixels(44));

        return nativeCell;
    }

    //データソースのItemを返すインデクサ
    public override object this[int position] {
        get {
            return _source[position];
        }
    }

    //Listの全行数
    public override int Count {
        get {
            return _source.Count;
        }
    }

    //Idを返す(特に無いのでpositionを返しておく)
    public override long GetItemId(int position)
    {
        return position;
    }
}

まぁiOSとだいたい同じで肝はやっぱりGetViewです。 ただAndroidの方はCellFactoryという素敵なクラスが用意されていて、iOSの方でやっていたrendererを呼び出してリサイクルCellを取得してそれを渡して…とかのダルい処理を全部やってくれます。

SetMinimumHeightでセルの高さを設定するんですが、これが有効なのはViewCell以外の組み込みCellで、ViewCellの場合はこれが有効になりません。 というのもViewCellはそのRenderer内で高さを云々する処理をしているのでそっちが優先されてしまうようです。
なので、ViewCellの高さを反映させるには親のListViewのRowHeightを指定してやる必要があります。

iOSと同様、ここでFormsのCellを介さずに直接NativeCellを使えば、よりパフォーマンスが良くなると思います。AndroidのNativeCell作成方法に関しては必要であれば

kamusoft.hatenablog.jp

こちらの記事を参考にしてください。

preを使った理由

最初はForms側もListView継承じゃなくて普通にViewを継承するようにして作っていったのですが、これだとどうしてもViewCellがちゃんと表示されませんでした。

ソースを追って行くとListView.TemplatedItemsからCellを取得するようにすれば良さそうでしたので、早速ListViewのサブクラスを使うように変えました。

しかしTemplatedItemsは、stableではまだinternalで隠されていたのでオープンになる2.3.5を使うしかなかったということでした。

もしViewCellに対応しないのであれば、ListViewを継承せずとも、またpreを使わずとも実装できたと思います。 それかTemplatedItemsで何が行われているのかをもっと読めば分かるかもしれません。

追記

その後、もう少しソースを読み進めているとAndroidの方の

Xamarin.Forms/ViewCellRenderer.cs at 78426257f4d5e835c79177121e9343747191d068 · xamarin/Xamarin.Forms · GitHub

このあたりに is TableViewとか is ListViewで親のタイプを確認してそれで値を設定している箇所があって、ここを通らないとCannot null valueとなって落ちてしまうことが判明したので、現時点では独自のListViewはForms側はオリジナルのListViewを継承するしかなさそうです。

終わりに

FormsのListViewで何が問題かと言えば、何か独自処理が必要になってCustomRendererで解決しようにもTableSourceやAdapterの内部に入れないと結局何も解決できないことが多いことです。

このサンプルのようにTableSource/Adapterを自由にいじれるようなものを用意しておけば、あとは各プラットフォームについてググった情報どおりに移植してやれば、Nativeで出来ることは何でも出来るようになると思います。

私もこれからこれを育てていこうかと思いますw