SIN@SAPPOROWORKSの覚書

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

Xamarin.Forms 「付箋紙を張るやつ」を作ってみた(画面構成)

【 Xamarin 記事一覧 】

Android iOS Windows Phone
f:id:furuya02:20141116031829p:plain:w150 f:id:furuya02:20141116031828p:plain:w150 f:id:furuya02:20141116031831p:plain:w150

「ToDoアプリと何が違うのか」という突っ込みを恐れず、付箋紙アプリ(サンプル)の紹介です。
付箋紙は、BoxViewをレンダラーで拡張して表現しています。また、データはMicrosoft Azureのモバイルサービスで管理されています。
なお、今回は、主に描画まわり話です。

コードは、GirHubに置きましたので、気が向いたら見てみて下さい。

furuya02/Xamarin.Forms.PostIt.Sample · GitHub


1 付箋紙の描画

f:id:furuya02:20141116034902p:plain

Xamarin.FormsのプロジェクトでPCLにPostItViewというクラスを作成しました。PostItViewは、ContentViewから派生したカスタムビューです。
このビューは、図のようにいくつかのコントロールをAbsoluteLayoutを使用して、図中の番号の順に重ねたものです。

(1)付箋紙本体

postItBox(表面部分)②及び、shadowBox(影の部分)①を重ねて付箋紙の本体を表現しています。
この2つは、ともにBoxViewの派生クラスで、レンダラーによって図のような形に描画されています。
表面も影も、色が違うだけで形は同じなので、色のプロパティを持つ同じものを使用しています。
レンダラーでの拡張がシンプルになるように共通化が図られています。

(2)文字

本文であるテキストと、作成日時 (③) は、Labelで作成されています。

(3)透明のボタン

最後に、透明のボタン④を全体にかぶせるように配置しています。
Xamarin.FormsのBoxViewでは、タップや長押しのイベントが拾えないので、Buttonの拡張クラス(ExButton)を作成し、ExButtonのレンダラ-で各デバイスごとイベントを拾っています。

長押しのイベント処理は、下記のページを参考にさせて頂きました
http://www.buildinsider.net/mobile/xamarintips/0012

長押しで、付箋紙を削除する仕様となっています。

Android iOS Windows Phone
f:id:furuya02:20141116034937p:plain:w150 f:id:furuya02:20141116034939p:plain:w150 f:id:furuya02:20141116034940p:plain:w150

以下は、PostItViewクラスのコードです。

public class PostItView : ContentView {

    public delegate void LongTapHandler(PostItItem item);
    public event LongTapHandler LongTap;
    private readonly PostItItem _item;


    public PostItView(PostItItem item,int width){
        _item = item;
        var absoluteLayout = new AbsoluteLayout();
        var margin = width/50;

        //フォントのサイズが基準値となる
        var fontSize = width / 20;//フォントサイズ
        const int shadow = 5; //影の幅

        //文字が表示できる幅の計算
        var col = width - margin * 2 - fontSize;
        //必要行数
        var row = (item.Text.Length * fontSize) / col + 1;
        var height = (int)((row + 2) * (fontSize * 1.2) + margin*2)+fontSize/2;

        //影(塗りつぶし)の描画
        var color = Color.FromRgba(0, 0, 0,0.2);
        var turnedColor = Color.FromRgba(0, 0, 0, 0);//全透過
        var shadowBox = new PostItBox(width - margin * 2, height - margin * 2, color, turnedColor);
        absoluteLayout.Children.Add(shadowBox, new Point(margin + shadow, margin + shadow));

        //本体(塗りつぶし)の描画
        color = Color.FromRgb(255,200,200);
        turnedColor = Color.FromRgb(255, 220,220);
        var postItBox = new PostItBox(width - margin * 2, height - margin * 2, color, turnedColor);
        absoluteLayout.Children.Add(postItBox, new Point(margin,margin));

        //ラベル(本文)の描画
        var labelText = new Label {
            Text = item.Text,
            Font = Font.SystemFontOfSize(fontSize),
            TextColor = Color.Black,
            WidthRequest = col,
        };
        absoluteLayout.Children.Add(labelText, new Point(fontSize / 2 + margin, fontSize / 2 + margin));

        //ラベル(日付)の描画
        var labelDate = new Label {
            Text = string.Format("CreateAt {0}",item.CreateAt.ToString("g")),
            Font = Font.SystemFontOfSize(fontSize*0.7),
            TextColor = Color.Black,
            WidthRequest = col,
        };
        absoluteLayout.Children.Add(labelDate, new Point(fontSize / 2 + margin, height-margin-fontSize*1.5));


        //透過ボタン(長押しを取得するため透明のボタンを置く)
        var exButton = new ExButton{
            WidthRequest = width,
            HeightRequest = height,
        };
        absoluteLayout.Children.Add(exButton, new Point(0, 0));
        exButton.LongTap += (sender, args) => {
            if (LongTap != null){
                LongTap(_item);
            }
        };

        Content = absoluteLayout;
    }
}

Xamarin.Formsで表現できない部分は、当然レンダラーで書くしかないのですが、できるだけPCL側にコードを置くことで、コストと保守性を上げたいものです。


2 スクロールビュー

f:id:furuya02:20141116040339p:plain
付箋紙が配置されている部分は、クラスBordで表現されています。ここでのメインのビューは、ScrollViewです。
SorollViewのコンテキストにStackLayoutを置き、先ほどPostItView(一つの付箋紙)は、このStackLayoutのChildrenにInsertされています。

Bordクラスのコードは下記のとおりです。

class Board {
    public delegate void LongTapHandler(PostItItem item);
    public event LongTapHandler LongTap;

    readonly StackLayout _layout = new StackLayout();

    public Board() {

        _layout.Spacing = 0;
        View = new ExtendedScrollView();
        View.VerticalOptions = LayoutOptions.FillAndExpand;
        View.Content = _layout;

    }

    public ExtendedScrollView View { get; private set; }
    public int PostItWidth { private get; set; }

    public void Insert(int index, PostItItem item){
        var postItView = new PostItView(item, PostItWidth);
        postItView.HorizontalOptions = LayoutOptions.Start;
        postItView.LongTap += i =>{
            if (LongTap != null){
                LongTap(i);
            }
        };
        _layout.Children.Insert(index, postItView);

        //Androidの場合、Delayがないとスクロールできない?
        Task.Delay(1).ContinueWith(t => Device.BeginInvokeOnMainThread(() =>{
            //一番下までスクロール
            //View.Position = new Point(0, View.ContentSize.Height - View.Bounds.Height);
            //一番上までスクロール
            View.Position = new Point(0, 0);
        }));
    }

    public void Delete(int index){
        _layout.Children[index].HeightRequest = 0;
        _layout.Children.RemoveAt(index);
    }

}

付箋紙の追加時には、最新の付箋紙が見えるように、スクロールしたい所なのですが、Xamarin.Formsのスクロールには、現時点でこのイベントがありません。そこで、ここでもレンダラーの登場です。コードは、Xamarin-Forms-LabsのExtendedScrollViewを、そっくりそのまま利用させて頂きました。


Xamarin-Forms-Labs/ExtendedScrollView.cs at master · XLabs/Xamarin-Forms-Labs · GitHub



※スクロールが、なかなかうまく動作せずに悩んだのですが、StackLayoutへの追加後、コントロールの描画がまだ完了していない時点でイベントが処理されるのが原因のようでした。
Task.Delay(1).ContinueWithで描画側に処理を譲ったり、Device.BeginInvokeOnMainThreadでUIスレッドに処理を回すことで回避できそうです。








【参考にさせて頂いたページ】
Xamarin.Formsの既存のコントロールを拡張するには? - Build Insider
XLabs/Xamarin-Forms-Labs



【 Xamarin 記事一覧 】