Xamarin.Forms expand/collapseするリスト型のビュー

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

この記事は「Xamarin Advent Calendar 2015 - Qiita」の19日目です。

複数の項目を列挙するUIとして、リスト型のビューは、非常に重宝するコントロールの一つです。 iOSでは「UITableView」、Androidでは「ListView」、そして、WindowsPhoneでは「LongListSelector」のようなイメージです。

リスト型のビューは、複数のセルを持つわけですが、すべてのセルが均一だと、その表現にも限界があります。しかし、一部のセルを動的に変化させることができるとなれば、その利用範囲はさらに大きく広がると思います。

今回は、Xamarin.Formsを使用して、この一部のセルが拡張するリスト型のUIを作成してみました。

2 選択したセルが詳細表示になるリスト

最初に、作成したサンプルの画面を見てください。

iOS Android WindowsPhone
001 002 003

これは、画面上部のテキストボックスに、任意の検索文字を入れてOKボタンを押すと、インターネット上から画像を検索してリスト型のビューに一覧表示するものです。そして、一覧の1つを選択すると、そのセルのサイズが広がり、詳細情報が表示されるようになっています。 ここでは、Xamarin.FormsのListViewでこれを表現してみました。

Xamarin.FormsのListViewは、その名の通り、リスト型のビューそのものです。

Xamarin.Formsの誕生当初、このListViewは、非常に機能が貧弱で、本当に限られた表現しかできなかったのですが、Verdsion 1.4あたりからメキメキと強化されてきて、現在では、かなり自由に利用できるようになって来ていると思います。

それでは、実装の状況を見ていきましょう。

(1) ListView

ListViewは、テンプレートを自前のオリジナルなもの(ここでは、MyCellクラス)としています。また、セルの高さを動的に変更するためには、HasUnevenRowsプロパティをtrueに設定する必要があります。

    var listView = new ListView();
    listView.ItemTemplate = new DataTemplate(typeof(MyCell)); // <= テンプレートはMyCellを指定
    listView.HasUnevenRows = true; // セルの高さを動的に変更する

(2) データクラス

個々のセルに表示されるデータは、次のようになっており、データ自体に、現在、詳細表示中かどうかのフラグを持っています。

class OneData {
    public string Title { get; set; } // タイトル
    public string MediaUrl { get; set; } // 画像のURL
    
    // ・・・省略・・・

    public OneData() {
        Expand = false; // <= 詳細表示中かどうかのフラグ
    }
}

(3) 選択時の処理

そして、リストが選択された際に、データのExpandプロパティを変更します


//現在、展開しているセルの行番号
int _index = -1;

// セルが選択された時のイベント処理
listView.ItemSelected += (s, e) => {
    OneData tmp;
     if (_index != -1) { // 既にセルが選択されている場合
          tmp = data.ar[_index];
          tmp.Expand = false; // Expandプロパティをfalse(デフォルト)に変更する
          data.ar[_index] = tmp; // データを差し替えて、プロパティが変化したことをListViewに伝える
     }

     // 新たに選択されたセル(行)を取得
     _index = data.ar.IndexOf(e.SelectedItem as OneData);
     // 選択されている場合
     if (_index != -1) {
         tmp = data.ar[_index];
         tmp.Expand = true; // Expandプロパティをtrue(詳細表示中)に変更する
         data.ar[_index] = tmp; // データを差し替えて、プロパティが変化したことをListViewに伝える
     }
};

(4) データテンプレート

データテンプレートは次のようになっています。 まずは、コンストラクタでデフォルトのセルを設計し、プロパティが変更れた時に呼び出される「OnBindingContextChanged」の中で、表示対象のデータが拡張状態の場合に、セルの高さを大きくして、詳細表示用のビューに差し替えています。

//セル用のテンプレート
class MyCell : ViewCell {


    public MyCell() {

        // ・・・省略・・・
        
        // コンストラクタ
        View = new StackLayout() {

            // 画像のアイコン表示とタイトルだけを横に並べたデフォルトのビュー
            // ・・・省略・・・

        };
    }

    // 拡張時に使用されるビュー
    View CreateExpandView() {

        // ・・・省略・・・

        return new StackLayout() {
            // 大きな画像やタイトル・サイズなどの詳細情報を並べたビュー
            // ・・・省略・・・
        };
    }

    // プロパティが変更された時に呼ばれる
    protected override void OnBindingContextChanged() {

        // ・・・省略・・・

        // 表示対象のデータを取得
        var data = (OneData)BindingContext;
        
       // 表示対象のデータが拡張状態の場合の処理
        if (data.Expand) {
            Height = 150; // セルの高さを変更
            View = CreateExpandView(); //ビューを拡張時のものに差し替える
        }
    }
}

単純に、対象データによってビューを差し替えているだけなので、コンストラクタで分岐すればリソースの節約になりそうに見えるのですが、実は、対象データを取得するためのBindingContextプロパティが、コンストラクタではnullとなっているため、このような実装になっています。

上記のコードは骨格のみになっています。詳しくはサンプルコードをご確認下さい。

github サンプルコード(http://github.com/furuya02/Xamarin.Forms.ExpandListView2)

※画像検索には「Bing Search API」を使用していますが、公開コードには、アプリケーションキーは含まれておりません。

画像検索については、本稿の対象外とさせて頂いております。下記のリンク等をご参照ください。
Bing Search APIを使用して画像検索するには @garicchi
Bing Search APIを使ってWeb検索を行うには(Json編) @garicchi

3 選択するとセルが拡張して別のビューを表示するリスト

次に紹介するのは、iOSでUITableViewのコンテンツタイプを「Static Cells」にして設計する、オプション設定画面のようなイメージです。

次のサンプルは、上から、氏名・誕生日・既婚/未婚・好きなスポーツなどの項目リストが表示されており、選択すると、その下が拡張して、それぞれの入力に応じたビューが表示されるようになっています。

iOS Android WindowsPhone
004 005 006

Xamarin.Formsでは、このような画面設計はListViewよりもStackLayoutを使用する方が簡単に設計することができます。

拡張時に表示されるビューは、全部最初からStackLayoutに積んでおき、拡張されるまでは、その高さを0にしたり、Visibleプロパティをfalseにすることで、見かけ上存在しないように見せています。

また、全体のStackLayoutが拡張した時に、画面サイズに依存せず縦に伸びることができるように、スクロールビューの上に置いています。

この状態を、簡略に表現すると、概ね次のような絵になります

実際の実装では、グループ単位にさらに階層化したりしていますが、単純にStackLayoutがネストされているだけです。

009

上図における、各種の入力ビュー(ExpandView)は、次のようなクラスとなっています。

Expandと言うプロパティがあり、これを設定することで、高さを0にしたり、サブビューのVisibleをfalseに変更することで、全く見えなくなるようになっています。

 

class ExpandView :ContentView{
    protected int _height = 100; // 拡張時の高さ(デフォルトで100)
    private bool _expand; // 拡張状態かどうかのフラグ

    protected List<View> subViews = new List<View>(); // サブビューの一覧

    public bool Expand { 
        set { // 拡張状態のフラグがセットされた際の処理
            HeightRequest = value ? _height : 0; //ビューの高さを0または_heightに設定する
            _expand = value;
            foreach (var s in subViews) {
                s.IsVisible = _expand; //すべてのサブビューのVisibleを変更する
            }
        }
        get {
            return _expand;
        }
     }

    public ExpandView() {
        Expand = false;
    }
}

このExpandViewクラスを継承して、各種の入力に応じたビューを設計して、最初に示したようにStackLayoutに積み上げていくのです。

それでは、ExpandViewの一例として、氏名入力用のビューを紹介しておきます。

FillName.csでは、2つのEntryビューをStackLayoutに積んでいるだけです。そして、FirstNameとLastNameと言うプロパティで、その入力内容を読み書きできるようにしています。ビューの高さは、ここのEntryビューの高さの2倍になるようにオーバーライドされています。

class FullNameView : ExpandView {

        public string FirstName {
            get {
                return _firstName.Text;
            }
            set {
                _firstName.Text = value;
            }
        }
        public string LastName {
            get {
                return _lastName.Text;
            }
            set {
                _lastName.Text = value;
            }
        }

        private Entry _firstName = new Entry();
        private Entry _lastName = new Entry();

        public FullNameView() {

            _height = Device.OnPlatform(100, 100, 200);

            subViews.Add(_firstName);
            subViews.Add(_lastName);

            _firstName.Placeholder = "Firstname";
            _firstName.HeightRequest = _height/2;
            _lastName.Placeholder = "Lastname";
            _lastName.HeightRequest = _height/2;


            Content = new StackLayout {
                Spacing = 0,
                Children = { _firstName, _lastName }
            };
        }
    }

その他のExpandViewに関しては、サンプルコードをご確認ください。

なお、誕生日を入力するビューでは、基本的にXamarin.FormsのDatePickerを使用しているのですが、iOSの場合だけ、入力ダイアログが画面の最下部の表示されるため、リストが拡張するイメージから考えると、ちょっと違和感あるUIとなってしまいます。

007

そこで、iOSだけは、日付入力のための独自の日付入力ビュー(ここでは、ExDatePickerとした)を設定し、これをレンダラーで拡張することにしました。

// iOS の場合は、下に表示されるインターフェースなので、セルの中に収めるために
// レンダラーでUIDatePickerを表示する
public class ExDatePicker : ContentView {
    public static readonly BindableProperty DateProperty =
      BindableProperty.Create<ExDatePicker, DateTime>
        (p => p.Date, DateTime.Now);

    public DateTime Date {
        get {
            return (DateTime)GetValue(DateProperty);
        }
        set { SetValue(DateProperty, value); }
    }
}

レンダラー側のコードは次のとおりで、UIDatePicker自体をビューとして使用しています。

[assembly: ExportRenderer(typeof(ExDatePicker),typeof(ExCalendarRenderer))]
namespace ExpandListView1.iOS {
    class ExCalendarRenderer: ViewRenderer<ExDatePicker, UIDatePicker>  {

        protected override void OnElementChanged(ElementChangedEventArgs<ExDatePicker> e) {

            //・・・省略・・・

            SetNativeControl(new UIDatePicker(Frame) {
                Mode = UIDatePickerMode.Date,
            });

            // Control(UIDatePickerの値が変化した際に、ExCarenderのCurrentDate プロパティを変化させる
            Control.AddTarget((s,a)=> {
                if (Element != null) {
                    Element.Date = Control.Date.ToDateTime();
                }
            }, UIControlEvent.ValueChanged);
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) {
            
            //・・・省略・・・

            // ExCalendarのCurrentDateプロパティが変更された時、Control(UIDatePicker)のDateを変更する
            if (e.PropertyName == ExDatePicker.DateProperty.PropertyName) {
                Control.Date = Element.Date.ToNSDate();
            }
        }
    }
}

github サンプルコード(http://github.com/furuya02/Xamarin.Forms.ExpandListView1)

4 まとめ

今回は、Xamarin.Formsによる、セルが拡張するリスト型のUIを2つ紹介しました。 一部、見た目を良くするためにボタンのレンダラー拡張などを利用しましたが、思ったよりの多くの部分が共通コードとなっています。うまく設計できれば、モデルの共有だけでなく、UIのかなりの部分の共有も可能かも知れません。

5 参考リンク


Dynamic ViewCell in ListView
Dynamic ViewCell in ListView
Bing Search APIを使用して画像検索するには @garicchi
How To Create an Expandable Table View in iOS
Xamarin記事一覧(SAPPOROWORKSの覚書)

  • shinriyo

    おそらく、OneDataには
    public bool Expand { get; set; } // 詳細表示中かどうかのフラグ
    を追加する必要がありそうですよ。