kamulog

xamarin.formsのネタなど

Xamarin.Formsで任意のViewにNumberPickerの機能を追加するEffect

Xamarin.Formsには標準の選択用のViewとしてPickerがありますが、Bindingに非対応だったり(もうすぐ対応されますが)、デザインが固定で使いにくかったり、Android側は選択位置を記憶してくれなかったりで個人的に正直微妙だなと思っていました。特にデザインが固定というのが個人的要件に合いません。Pickerは外観を持ったものでなくて単純にタップしたら呼び出されるだけのものであって欲しいなと思います。

またそれとは別に単純に数値だけを選択できるNumberPickerが欲しいなと思っていたのでAndroidのNumberPickerをベースとしてタップした時だけNuberPickerが出現する機能をEffectとして作成しました。

XamarinにおけるiOSでのUIPickerView、AndroidでのAlertDialogの参考になれば幸いです。

作成したもの

github.com

サンプルはこちら

nugetでも公開しています

www.nuget.org

Install-Package AiForms.Effects

nuget経由で使う場合はiOSのAppDelegate.csの
global::Xamarin.Forms.Forms.Init(); の後に
AiForms.Effects.iOS.Effects.Init();
の記述が必要です。

概要

指定したViewをタップした時にNumberPickerを呼び出して、選択して「Done/OK」するとNumberプロパティに値がセットされます。またCommandプロパティが設定されている場合は、Commandを実行します。

f:id:kamusoft:20161208175041g:plain:h400  f:id:kamusoft:20161208175146g:plain:h400

パラメータ

  • On
    • EffectのOn・Off
  • Min
    • 最小値を指定(正の整数)
  • Max
    • 最大値を指定(正の整数)
  • Number
  • Title
    • ダイアログのタイトル(任意)
    • iOSの場合長すぎると不細工に…
  • Command
    • 数値選択完了時に実行するコマンド(任意)

Xaml

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:ef="clr-namespace:AiForms.Effects;assembly=AiForms.Effects"
        xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
        prism:ViewModelLocator.AutowireViewModel="True"
        x:Class="AiEffects.Sample.Views.AddNumberPickerPage"
        Title="AddNumberPicker">
    <StackLayout>
        <Label Text="Text"
            ef:AddNumberPicker.On="true"
            ef:AddNumberPicker.Min="10"
            ef:AddNumberPicker.Max="999"
            ef:AddNumberPicker.Number="{Binding Number}"
            ef:AddNumberPicker.Title="Select your number"
            ef:AddNumberPicker.Command="{Binding SomeCommand}" />
    </StackLayout>
</ContentPage>

iOS コード補足

AddNumberPickerPlatformEffect.cs

ポイントだけ抜粋です。
iOSの場合はタップすると下からにょーんと上がってくるヤツにしました。 その場合はUIPicerViewを使い以下のようにします。

  1. UIPickerViewを生成
  2. UIToolbarを作る
  3. 2にボタンを設定、ついでに押した時のアクションも登録
  4. Pikerの起点となるViewのプロパティに以下を設定
    • InputView に1(UIPikcerView)
    • InputAccessoryView に2(UIToolbar)
  5. 1のソースとなるUIPickerViewModelを継承した何かを生成
  6. 1のModelに5を設定

ただ4の起点となるViewは入力できる部品しか無理っぽいらしく、そうなるとそのままでは任意のViewにくっつける事ができなくなるので無理やりな工夫をしました。
それはキャレットが表示されないようにしたUITextField(NoCaretField)を起点Viewのサブビューとして追加して制約で親にぴったりサイズにし、最前面にして起点ViewをタップするともれなくNoCaretFieldをタップすることになるように仕向けたことです。

とりあえずこれで期待する動きになりました。

NoCaretFieldはXamarin.FormsのInternalクラスをほぼそのままコピペして使ってます。

_entry.TranslatesAutoresizingMaskIntoConstraints = false;
これは制約まわりでの警告ログ対策です。以下の情報を参考にしました。

qiita.com

void CreatePicker()
{
    _entry = new NoCaretField();
    _entry.BorderStyle = UITextBorderStyle.None;
    _entry.BackgroundColor = UIColor.Clear;
    _view.AddSubview(_entry);

    _entry.TranslatesAutoresizingMaskIntoConstraints = false;

    _constraint = CreateConstraint(_view, _entry); //制約作成(目一杯広げる)

    _view.UserInteractionEnabled = true;
    _view.AddConstraints(_constraint);
    _view.SendSubviewToBack(_entry);

    _picker = new UIPickerView();

    var width = UIScreen.MainScreen.Bounds.Width;
    var toolbar = new UIToolbar(new CGRect(0, 0, (float)width, 44)) { BarStyle = UIBarStyle.Default, Translucent = true };

    var cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, (o, e) => {
        _entry.ResignFirstResponder();
        Select(_model.PreSelectedItem); //前の選択値に戻す
    });

    _title = new UILabel();
    var labelButton = new UIBarButtonItem(_title);

    var spacer = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace);

    var doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, (o, a) => {
        var s = (NumberPickerSource)_picker.Model;
        UpdatePickerFromModel(s);   //選択した値をbindingに反映
        _entry.ResignFirstResponder();
        _command?.Execute(s.SelectedItem);
    });

    //FlexibleSpaceを両サイドに置くことで良い感じにタイトルが真ん中にくる
    toolbar.SetItems(new[] { cancelButton, spacer, labelButton, spacer, doneButton }, false);

    _entry.InputView = _picker;
    _entry.InputAccessoryView = toolbar;

    _model = new NumberPickerSource();
    _picker.Model = _model;

}

NSLayoutConstraint[] CreateConstraint(UIView parent, UIView child)
{
    return new NSLayoutConstraint[]{
        NSLayoutConstraint.Create(
            child,
            NSLayoutAttribute.Top,
            NSLayoutRelation.Equal,
            parent,
            NSLayoutAttribute.Top,
            1,
            0
        ),
        NSLayoutConstraint.Create(
            child,
            NSLayoutAttribute.Left,
            NSLayoutRelation.Equal,
            parent,
            NSLayoutAttribute.Left,
            1,
            0
        ),
        NSLayoutConstraint.Create(
            child,
            NSLayoutAttribute.Right,
            NSLayoutRelation.Equal,
            parent,
            NSLayoutAttribute.Right,
            1,
            0
        ),
        NSLayoutConstraint.Create(
            child,
            NSLayoutAttribute.Bottom,
            NSLayoutRelation.Equal,
            parent,
            NSLayoutAttribute.Bottom,
            1,
            0
        ),
        NSLayoutConstraint.Create(
            child,
            NSLayoutAttribute.Width,
            NSLayoutRelation.Equal,
            parent,
            NSLayoutAttribute.Width,
            1,
            0
        ),
        NSLayoutConstraint.Create(
            child,
            NSLayoutAttribute.Height,
            NSLayoutRelation.Equal,
            parent,
            NSLayoutAttribute.Height,
            1,
            0
        ),
    };
}

Android コード補足

AddNumberPickerPlatformEffect.cs

AndroidではAlertDialogを使って、そこにNativeのNumberPickerを置いただけです。Xamarin.FormsのPickerのソースを参考にすると簡単でした。

using APicker = Android.Widget.NumberPicker;
//略
void CreateDialog()
{
    if (_dialog != null) return;

    var picker = new APicker(Container.Context);
    picker.MinValue = _min;
    picker.MaxValue = _max;
    picker.Value = _number;

    using (var builder = new AlertDialog.Builder(Container.Context)) {

        builder.SetTitle(AddNumberPicker.GetTitle(Element));

        Android.Widget.FrameLayout parent = new Android.Widget.FrameLayout(Container.Context);
        parent.AddView(picker, new Android.Widget.FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WrapContent,
                ViewGroup.LayoutParams.WrapContent,
               GravityFlags.Center));
        builder.SetView(parent);

        //DialogとPickerは使い捨てなのでCancel時は何もしない
        builder.SetNegativeButton(global::Android.Resource.String.Cancel, (o, args) => { });

        builder.SetPositiveButton(global::Android.Resource.String.Ok, (o, args) => {
            AddNumberPicker.SetNumber(Element, picker.Value);   //Numberプロパティに反映
            _command?.Execute(picker.Value);
        });

        _dialog = builder.Create();
    }

    _dialog.SetCanceledOnTouchOutside(true);

    _dialog.DismissEvent += (ss, ee) => {
        _dialog.Dispose();
        _dialog = null;
        picker.RemoveFromParent();
        picker.Dispose();
        picker = null;
    };

    _dialog.Show();
}

参考

終わりに

Picker関連はiOSならUIPickerViewModelのサブクラスでいろいろすれば複数カラムなのも自由に作れますし、AndroidならAlertDialogでなんとでもなりそうな感じです。

NumberPickerだけじゃなくてもっと汎用的なPickerにも応用できるので、そういうものも今後作りたいと思います。