Xamarin.FormsでCustomRendererでContentPageをカスタマイズしてみる
はじめに
今回はContentPageのRendererを継承したクラスを作成してContentPageをカスタマイズしてみたいと思います。
完成品はこちら
コードはこちら
IsBusyプロパティに連動したローディング表現を実装してみます。
以前はに書いた記事ではNavigationPageでIsBusyがTrueになった時に単純にインジケータを動かすだけでしたが
IsBusyがTrueになった時にインジケータの表示に加えてページをスライドアウトさせて非表示にしIsBusyがFalseになった時にはページがスライドインしてくるような動作をAndroid/iOSで書いてみました。
更にIsBusyがTrueになってから1秒のDelay後にローディングが行われるようにしました。
ページの実装
XamarinFormsLoadingPage/MainPage.xaml.cs at master · yuka1984/XamarinFormsLoadingPage · GitHub
Pageでの実装は今回もボタンをクリックするとIsBusyが変化するだけのものです。
ボタンをクリックするとIsBusyがTrueになります。
2500msec後にIsBusyはfalseになります。
その途中にボタンをクリックした場合にはIsBusyはfalseになります。
Androidの実装解説
XamarinFormsLoadingPage/LoadingPageRenderer.cs at master · yuka1984/XamarinFormsLoadingPage · GitHub
PageRendererクラスを継承して実装を行います。
AndroidのPageRendererはVisualElementRenderer
部品(View or ViewGroup)を組み合わせて画面を構成できます。
セットアップ
まずはクラスセットアップとしてOnElementChangedにてAddViewを使ってAndroid.Widget.ProgressBarを追加します。
_progress = new AProgressBar(Context, null, Android.Resource.Attribute.ProgressBarStyleSmall) { Indeterminate = true }; AddView(_progress); _progress.Visibility = ViewStates.Invisible;
次にValueAnimatorの設定を行っています。
ValueAnimatorは
AndroidでもiPhoneに負けないようなアニメーションを実装してみよう - Yahoo! JAPAN Tech Blog
アニメーションを実行するために、アニメーション中の値を計算して、それらの値をターゲットオブジェクトに設定するための、簡単なタイミングエンジンを提供します。
というものでこれを使用してIsBusyが変化したときのアニメーション動作を作成ています。
if (animator == null) { animator = ValueAnimator.OfFloat(0f, 1f); animator.SetDuration(300); animator.Update += (s, a) => { var view = GetChildAt(1); var width = view.Width; var height = view.Height; var c = (float) a.Animation.AnimatedValue; view.Left = (int) (width * c); view.Right = view.Left + width; _progress.Alpha = c; System.Diagnostics.Debug.WriteLine(c); ; }; }
0fから1fまで300msecで変化して変化のたびにUpdateイベントを発行してくれるような感じです。
値を使用して画面のスライドアウトとプログレスバーのAlphaを同時に変更しています。
レイアウト
OnLayoutのoverrideにてプログレスバーの位置を調整しています。
真ん中よりちょっと上くらいに表示させます。
var width = r - l; var woffset = (width - 100) / 2; var hoffset = (b - t) / 10; _progress.Layout(l + woffset, t + hoffset, r - woffset, t + hoffset * 4);
IsBusy
OnElementPropertyChangedのoverrideにてIsBusyが変化したときの動作を実装しています。
IsBusyがTrueになった場合ValueAnimatorにStartDelayを設定しStartしています。
IsBusyがFalseになった場合にはValueAnimatorでStartDelayを0にしてReverseしています。
Reverseすることで1f -> 0fにアニメーションしていくのでIsBusyがTrueになった時の逆のアニメーションが行われます。
Pause Resuemeを使用することでアニメーション中にIsBusyの変化が起きた場合に対応します。
var view = GetChildAt(1); if (view != _progress) { if (animator.IsStarted) animator.Pause(); if (page.IsBusy) { _progress.Alpha = 0; if (!animator.IsPaused) animator.StartDelay = 1000; else animator.Resume(); animator.Start(); } else { animator.StartDelay = 0; if (animator.IsPaused) animator.Resume(); animator.Reverse(); } }
iOSの実装解説
XamarinFormsLoadingPage/LoadingPageRenderer.cs at master · yuka1984/XamarinFormsLoadingPage · GitHub
PageRendererクラスを継承して実装します。
iOSのPageRendererクラスはUIViewControllerクラスを継承して実装されているためカスタマイズする際にはUIViewControllerでできる事は全てできます。
UIViewControllreの位置づけというのはFormsのPageと近いものがあると思います。
ただXamarin.FormsでのUIViewControllerの使われ方という側面でいうと、本来のUIViewControllerの使われ方とは少し異なる感じですので注意が必要です。
セットアップ
UIActivityIndicatorViewをViewにAddSubViewで追加します。
if (_indicator == null) { _indicator = new UIActivityIndicatorView(UIActivityIndicatorViewStyle.Gray); View.AddSubview(_indicator); Indicatorconstraint(_indicator); }
レイアウトはLayoutAucherを使って設定します。
iOS 9で追加されたNSLayoutAnchor使うと簡単にわかりやすく間違えずにNSLayoutConstraint(制約)が作れます【Auto Layout】 - Qiita
Xmarin.FormsでいうRelativeLayoutに近い感じで設定できます。
私自身はこちらの方が書きやすくてよいです。
CreateAnchorした後に.Activeプロパティをtrueにしないと制約が確定しませんので注意が必要です。
よく付け忘れて制約が適用されずに悩む事をしてしまうことが多いです。
CenterX CenterYを使うことで中央より少し上くらいに表示します。
TranslatesAutoresizingMaskIntoConstraintsをfalseにしないと制約が適用されません。
UserInteractionEnable = falseとするとタップしても反応せずに背面に透過します。
protected virtual void Indicatorconstraint(UIActivityIndicatorView indicatorView) { indicatorView.CenterXAnchor.ConstraintEqualTo(View.CenterXAnchor).Active = true; indicatorView.CenterYAnchor.ConstraintEqualTo(View.CenterYAnchor, -30).Active = true; indicatorView.TranslatesAutoresizingMaskIntoConstraints = false; indicatorView.UserInteractionEnabled = false; }
IsBusy
今回はUIView.Animateを使用してみました。
ただこういうケースでは本来はUIView.Transitionを使うのが良いとは思うのですがレイアウト面がまだ使いこなせていないのでAnimateとしました。
iOSアプリ開発でアニメーションするなら押さえておきたい基礎 - Qiita
こちらの記事で学習できます。
UIView.Animateを使用して
- インジケータのAlphaを0から1fへ変更
- Pageの中身のView(PageのXAMLをiOSのUIViewに変換された様なもの)のAlphaを1fから0fへ変更
- Pageの中身のviewの位置を0fから1fの値の変化に応じて移動
というアニメーション動作を設定しています。
戻す際には逆動作となります。
if (e.PropertyName == "IsBusy") { var isbusy = (Element as Page).IsBusy; var view = View.Subviews.First(x => x != _indicator); var distance = view.Frame.Width > view.Frame.Height ? view.Frame.Width : view.Frame.Height; view.Layer.RemoveAllAnimations(); if (isbusy) { _indicator.Alpha = 0; _indicator.StartAnimating(); UIView.Animate(0.3, 1, UIViewAnimationOptions.CurveEaseIn , () => { view.Frame = new CGRect(View.Frame.X + distance, View.Frame.Y, view.Frame.Width, view.Frame.Height); view.Alpha = 0; _indicator.Alpha = 1; } , null); } else { UIView.Animate(0.3, 0, UIViewAnimationOptions.CurveEaseOut , () => { view.Frame = new CGRect(View.Frame.X, View.Frame.Y, view.Frame.Width, view.Frame.Height); view.Alpha = 1; _indicator.Alpha = 0; } , () => { _indicator.StopAnimating(); }); } }
また今回はUIViewControllerのViewからインジケータもPageのViewもRemoveSubViewせずに実装しているのでWillAnimateRotationのoverrideにてIsBusyプロパティに応じてPageのViewの調整を行っています。
このような感じでContentPageのRendererを拡張し様々な事を行うことができます。
ここからかなり詰めていけば、独自のNavigationやUXを実現することも可能です?
ではでは( `ー´)ノ