眠いしお腹すいたし(´・ω・`)

C#関連を主に書きます。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Xamarin.FormsでCustomRendererでContentPageをカスタマイズしてみる

はじめに

今回はContentPageのRendererを継承したクラスを作成してContentPageをカスタマイズしてみたいと思います。

完成品はこちら

f:id:tamafuyou:20170513004320g:plain

コードはこちら

github.com

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を継承しているわけですがVisualElementRendererはAndroidのViewGroupを継承しています。

ビューとビューグループ - Android入門

部品(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でできる事は全てできます。

UIViewControllerまとめ - Qiita

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のXAMLiOSの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を実現することも可能です?

ではでは( `ー´)ノ