読者です 読者をやめる 読者になる 読者になる

SIN@SAPPOROWORKSの覚書

C#を中心に、夜な夜な試行錯誤したコードの記録です。

Xamarin.Forms ListViewでTwitter風のレイアウトを作成してみました(機種依存コードなし)

【 Xamarin 記事一覧 】


f:id:furuya02:20140807221010p:plain:w140:leftf:id:furuya02:20140807221008p:plain:w140:leftf:id:furuya02:20140807221009p:plain:w150:leftXamarin.FormsのListViewでカスタマイズセル(CellView)を使用することで、Twitter風の画面を作成してみました。(「Xamarin」というキーワード検索を表示しただけのもです)

機種依存のコードはなく、共有プロジェクトだけで書きました。

最新のXamarin.Formsにアップデート(1.2.2.6243)することで、メッセージの長さに応じて行ごとの高さを変更することもできています。

なお、サンプルコードは、GitHubにも置きました。
https://github.com/furuya02/Xamarin.Forms.ListView.Sample

1 Xamarin.Formsのアップデート

2014.08.07現在、Xamarin.Formsのバージョンは、1.1.0.6201ですが、iOSでListViewの高さ変更ができなかったり、Uri指定でのイメージ表示が一部で動作しいなど、いくつかの問題があるため最新のものにアップデートしました。

アップデートは、パッケージマネージャコンソールから簡単に行うことができます。(すべてのプロジェクトにインストールするのを忘れないでください)

PM> Install-Package Xamarin.Forms -ProjectName App1
PM> Install-Package Xamarin.Forms -ProjectName App1.iOS
PM> Install-Package Xamarin.Forms -ProjectName App1.Android
PM> Install-Package Xamarin.Forms -ProjectName App1.WinPhone
PM> Get-Package
Id                             Version              Description/Release Notes                                                      
--                             -------              -------------------------                                                      
WPtoolkit                      4.2013.08.16         Windows Phone toolkit provides a collection of controls, extension methods a...
Xamarin.Android.Support.v4     19.1.0               C# bindings for android support library v4.                                    
Xamarin.Forms                  1.2.2.6243           Build native UIs for iOS, Android, and Windows Phone from a single, shared C..

2 Linq To Twitter

f:id:furuya02:20140807223039p:plain:w250:left
Twitterのデータ取得には、LinqToTwitterを使用させて頂きました。

LINQ to Twitter – Code Plex
http://linqtotwitter.codeplex.com/

.NETでTwitterを扱うには、非常に便利にできています。

NuGetで公開されているので、こちらもパッケージマネージャコンソールで簡単にインストールが可能です。

PM> Install-Package LinqToTwitter -ProjectName App1
PM> Install-Package LinqToTwitter -ProjectName App1.iOS
PM> Install-Package LinqToTwitter -ProjectName App1.Android
PM> Install-Package LinqToTwitter -ProjectName App1.WinPhone

PM> Get-Package
Id                             Version              Description/Release Notes                                                      
--                             -------              -------------------------                                                      
LinqToTwitter                  3.0.4                Supporting Twitter API v1.1, async, and PCL! It uses standard LINQ syntax fo...
Microsoft.Bcl                  1.1.9                This packages enables projects targeting down-level platforms to use some of...
Microsoft.Bcl.Build            1.0.14               This package provides build infrastructure components so that projects refer...
Microsoft.Bcl.Compression      3.9.83               This package allows projects targeting Windows Phone Silverlight 8 directly ...
Microsoft.Net.Http             2.2.22               This package includes HttpClient for sending requests over HTTP, as well as ...
WPtoolkit                      4.2013.08.16         Windows Phone toolkit provides a collection of controls, extension methods a...
Xamarin.Android.Support.v4     19.1.0               C# bindings for android support library v4.                                    
Xamarin.Forms                  1.2.2.6243           Build native UIs for iOS, Android, and Windows Phone from a single, shared C..

f:id:furuya02:20140807223041p:plain:w250:left
なお、LinqToTwitterを使用するためには、Twitter開発者のページで、アプリを作成し、「AppKey」及び「AppSecret」を取得する必要があります。

サンプルコードは、この部分を上書きすることで動作させることが可能になります。


3 レイアウト

f:id:furuya02:20140807235702p:plain:w300:left
図は、ListViewのセルを構成する要素を表したものです。簡単に言ってしまば、ImageとLabelをStackLayoutで組み合わせただけです。

Imageは、サイズを指定して上段に寄せ、ラベルは、要素ごとにフォントサイズと色を指定しています。

4 各行の高さの変更

各行は、メッセージの行数に応じて高さが変化するようになっています。
ListViewで各行の高さを指定するには、セルのテンプレートクラス(MyCell)でOnBindingContextChangedをオーバーライドします。

サンプルでは、メッセージから改行コードと文字数で行数を算出し、そこから高さを指定しています。(日本語で27文字ぐらいが1行の収まっていたので、1行を27文字で固定しましたが、正確には、もう少しちゃんと作りこむ必要があるでしょう)

5 サンプルコード

以下が、記述したコードのすべてです。サンプルとして分かりやすいように、共有プロジェクトのApp.csだけを編集しました。

public class App {
    public static Page GetMainPage() {
        return new TweetPage();
    }
}

//1つのTweetを表現するクラス
internal class Tweet {
    public string Name {get;set;}//表示名
    public string Text {get;set;}//メッセージ
    public string ScreenName {get;set;}//アカウント名
    public string CreatedAt {get;set;}//作成日時
    public string Icon {get;set;}//アイコン
}

internal class TweetPage : ContentPage {
    //データソース(class Tweetのコレクション)
    private readonly ObservableCollection<Tweet> _tweets = new ObservableCollection<Tweet>();

    public TweetPage() {
        //パディング(iPhone用)
        Padding = new Thickness(0, Device.OnPlatform(20, 0, 0), 0, 0);
        //ListViewの生成
        var listView = new ListView{
            ItemTemplate = new DataTemplate(typeof (MyCell)),//セルの指定
            ItemsSource = _tweets,//データソースの指定
            HasUnevenRows = true,//行の高さを可変とする
        };
        //このページのコンテンツとしてListView(のみ)を指定する
        Content = new StackLayout() {
            Children = {listView,
           }
        };
        //更新 ("xamarin"という文字列を検索する)
        Refresh("xamarin");
    }

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

           //アイコン
            var icon = new Image();
            icon.WidthRequest = icon.HeightRequest = 50;//アイコンのサイズ
            icon.VerticalOptions = LayoutOptions.Start;//アイコンを行の上に詰めて表示
            icon.SetBinding(Image.SourceProperty, "Icon");
                
            //名前
            var name = new Label{Font = Font.SystemFontOfSize(12)};
            name.SetBinding(Label.TextProperty, "Name");
                
            //アカウント名
            var screenName = new Label{Font = Font.SystemFontOfSize(12)};
            screenName.SetBinding(Label.TextProperty, "ScreenName");

            //作成日時
            var createAt = new Label{Font = Font.SystemFontOfSize(8),TextColor = Color.Gray};
            createAt.SetBinding(Label.TextProperty, "CreatedAt");

            //メッセージ本文
            var text = new Label{Font = Font.SystemFontOfSize(10)};
            text.SetBinding(Label.TextProperty, "Text");

            //名前行
            var layoutName = new StackLayout {
                Orientation = StackOrientation.Horizontal, //横に並べる
                Children = { name,screenName }//名前とアカウント名を横に並べる
            };

            //サブレイアウト
            var layoutSub = new StackLayout{
                Spacing = 0,//スペースなし
                Children ={layoutName,createAt, text}//名前行、作成日時、メッセージを縦に並べる
            };

            View = new StackLayout{
                Padding = new Thickness(5),
                Orientation = StackOrientation.Horizontal, //横に並べる
                Children ={icon,layoutSub} //アイコンとサブレイアウトを横に並べる
            };
        }

        //テキストの長さに応じて行の高さを増やす
        protected override void OnBindingContextChanged() {
            base.OnBindingContextChanged();

            //メッセージ
            var text = ((Tweet)BindingContext).Text;
            //メッセージを改行で区切って、各行の最大文字数を27として行数を計算する(27文字は、日本を基準にしました)
            var row = text.Split('\n').Select(l => l.Length / 27).Select(c => c + 1).Sum();
            Height = 12 + 8 + row*10 + 20;//名前行、作成日時行、メッセージ行、パディングの合計値
            if (Height <60){
                Height = 60;//列の高さは、最低でも60とする
            }
        }
    }

    private async void Refresh(string searchString) {
        //認証
        var auth = new ApplicationOnlyAuthorizer() {
            CredentialStore = new InMemoryCredentialStore {
                ConsumerKey = "{API Keyt}",
                ConsumerSecret = "{API secret}",
            },
        };
        await auth.AuthorizeAsync();
        //コンテキストの作成
        var context = new TwitterContext(auth);
        var response = await (from search in context.Search
                        where search.Type == SearchType.Search &&
                              search.Query == searchString &&
                              search.Count == 30
                        select search).SingleOrDefaultAsync();
        //取得データの解釈
        foreach (var a in response.Statuses) {
            _tweets.Add(new Tweet {
                Text = a.Text,
                Name = a.User.Name,
                ScreenName = a.User.ScreenNameResponse,
                CreatedAt = a.CreatedAt.ToString("f"),
                Icon = a.User.ProfileImageUrl
            });
        }
    }
}


[2015.01.27追記]
@espresso3389 さんから、AOTで問題が発生するとの指摘を頂きました。

AOTの罠: Xamarin.Forms の ListView で System.MissingMethodException: Default constructor not found for type - espresso3389の日記


iOSにおけるAOTの最適化からコンストラクタの削除を避けるため、次の記述が必要とのことでした。

class PreserveAttribute : System.Attribute{
  public bool Conditional { get; set; }
}

//セル用のテンプレート
private class MyCell : ViewCell {
  [Preserve(Conditional=true)]
  public MyCell() {
...

【 Xamarin 記事一覧 】