shuhelohelo’s blog

Xamarin.Forms多めです.

Xamarin.Formsで上下スクロールにあわせてナビゲーションバーを表示・非表示

ShellやNavigationのナビゲーションバーを直接動かすことはできないので,Frameコントロールでナビゲーションバーにあたるものを配置して,ということです.

さて,モバイルアプリは上下にスクロールして使うUIが多いです.

上部にナビゲーションバーがあって,その下にページが配置されてスクロールするというタイプですね.

そこでおしゃれなUIによくあるのが,ページを上下にスワイプすると,ナビゲーションバーが表示されたり表示されなくなったり,とか,一定量スクロールすると一緒にスクロールアップして画面外に消えたり,とかです.

サンプル

フェードイン/アウト

これはUdemyアプリの例です.

f:id:shuhelohelo:20200309225251g:plain

最初,ナビゲーションバーは表示されていませんが,ある程度スクロールダウンするとナビゲーションバーがフェードインしてきます.

おしゃれです.

一緒にスクロールアップ/ダウン

こちらはLinkedInです.

f:id:shuhelohelo:20200309232203g:plain

ある程度スクロールアップすると一緒にスクロールして画面外に行き,スクロールダウンすると表示されるタイプです.

作る

これらをXamarin.Formsで作るのは比較的かんたんです. コントロールのアニメーション機能と,スクロールイベントを組み合わせて,調整するだけです.

フェードイン/アウト

f:id:shuhelohelo:20200309233010g:plain

StackLayoutをScrollViewを使ってスクロールさせています.

ScrollViewにはScrolledイベントがあり,イベントハンドラではY方向(またはX方向)のスクロール位置を取得できます.

このスクロール位置が指定した値を超えるか下回るかをきっかけとしてナビゲーションバーの表示,非表示を切り替えます.

表示,非表示はアニメーションで透過度を変化させています.

        private async void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
        {
            Message = e.ScrollY.ToString();

            await ToggleHeaderVisibility(e.ScrollY);
        }

        //表示,非表示が切り替わる位置
        private readonly double _headerToggleYPosition = 100;
        //アニメーションにかける時間.
        private readonly uint _animateLength = 200;
        //ナビゲーションバーの表示状態を保持
        private bool _isHeaderVisible = false;
        private async Task ToggleHeaderVisibility(double scrollYPosition)
        {
            bool prevIsHeaderVisible = _isHeaderVisible;
            if (scrollYPosition >= _headerToggleYPosition)
            {
                //指定位置までスクロールアップしたら,ナビゲーションバーを表示するフラグOn
                _isHeaderVisible = true;
            }
            else
            {
                //スクロールダウンでフラグOff
                _isHeaderVisible = false;
            }

            //指定位置をまたがない場合,アニメーションの必要は無いのでここまで.
            if (_isHeaderVisible == prevIsHeaderVisible)
            {
                return;
            }

            //ナビゲーションバーの表示・非表示のアニメーション
            if (_isHeaderVisible)
            {
                await frame_Header.FadeTo(1.0, _animateLength);
            }
            else
            {
                await frame_Header.FadeTo(0, _animateLength);
            }
        }

一緒にスクロールアップ/ダウン

f:id:shuhelohelo:20200309234408g:plain

これは指定した固定のスクロール位置からナビゲーションバーのスクロールアップ,ダウンが開始するものです.

        private readonly double _slideToggleYPosition = 110;
        private void SlideHeaderProcess(double scrollYPosition)
        {
            if (scrollYPosition >= _slideToggleYPosition)
            {
                //指定した位置より多くスクロールした分だけ,配置位置から引く
                frame_Header.TranslationY = frame_Header.Y - (scrollYPosition - _slideToggleYPosition);
            }
            else
            {
                //素早く動かすと値が飛び飛びになって,もとの位置に戻らないのでここで調整
                frame_Header.TranslationY =frame_Header.Y;
            }
        }

もう一つのパターンは,スクロールアップについては1つめと同じで決まった位置からナビゲーションバーがスクロールアップしますが,スクロールダウンのときはその位置からすぐにナビゲーションバーがスクロールダウンしてくるタイプです.

以下のような動作になります.

f:id:shuhelohelo:20200310130101g:plain

動作は期待どおりに作れましたが,コードがはずかしい感じ.

        private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
        {
            SlideHeaderProcess(e.ScrollY);
        }

        private readonly double _slideToggleYPosition_Up = 110;
        private double _slideToggleYPosition_Down = 0;

        private double _prevScrollYPosition = 0;
        private ScrollDirection _prevDirection = ScrollDirection.Down;

        private void SlideHeaderProcess(double scrollYPosition)
        {
            ScrollDirection scrlDirection = (scrollYPosition - _prevScrollYPosition) >= 0 ? ScrollDirection.Up : ScrollDirection.Down;

            //スクロールの方向が下方向に変わった
            if (scrlDirection != _prevDirection && scrlDirection == ScrollDirection.Down)
            {
                _slideToggleYPosition_Down = scrollYPosition;
            }

            //上方向にスクロールしている時
            if (scrlDirection == ScrollDirection.Up)
            {
                //上方向にスクロールするときは決まった位置から
                //ナビゲーションバーを隠す処理を開始
                if (scrollYPosition >= _slideToggleYPosition_Up)
                {
                    //前回のスクロール位置との差を計算する
                    double deltaY = Math.Abs(scrollYPosition - _prevScrollYPosition);
                    //次のTranslationYの値を計算する
                    double nextTransY = frame_Header.TranslationY - deltaY;

                    //次のTranslationYの値とナビゲーションバーのコントロールの高さを比較する.
                    //TranslationYの変化はナビゲーションバーの高さと開始位置を足したものちょうど
                    if (nextTransY < -(frame_Header.Height + frame_Header.Y))
                    {
                        nextTransY = -(frame_Header.Height + frame_Header.Y);
                    }

                    frame_Header.TranslationY = nextTransY;
                    Message = frame_Header.TranslationY.ToString();
                }
            }

            //スクロールの方向が引き続き下方向
            if (scrlDirection == ScrollDirection.Down)
            {
                double deltaY = Math.Abs(scrollYPosition - _prevScrollYPosition);
                double nextTransY = frame_Header.TranslationY + deltaY;

                if (nextTransY > 0)
                {
                    nextTransY = 0;
                }

                frame_Header.TranslationY = nextTransY;

                Message = frame_Header.TranslationY.ToString();
            }

            //スクロール方向を記録
            _prevDirection = scrlDirection;
            //スクロール位置を記録
            _prevScrollYPosition = scrollYPosition;
        }
    }

ソースコード

今回のソースコードはこちら.

github.com

追記

ソースコードにReactive Extensionsを使ったバージョンを追加しました.

記事はこちら.

shuhelohelo.hatenablog.com

おまけ

これを作りたい. f:id:shuhelohelo:20200309231702g:plain

追記

CollectionViewだとEventArgsの種類が異なりItemsViewScrolledEventArgsなので注意が必要.

この中には現在のスクロール位置(VerticalOffset,HorizontalOffset)だけではなく変化量(VerticalDeltaやHorizontalDelta)も含まれるので,変化量を計算したり,変化量の正負でスクロール方向を判断したりする必要がない.

        private readonly double _slideToggleYPosition = 110;
        private void SetHeaderBehaviorByScroll()
        {
            //ヘッダーを隠す側の動作
            var hide = Observable.FromEventPattern<ItemsViewScrolledEventArgs>(CollectionsView_Sessions, nameof(CollectionsView_Sessions.Scrolled));
            hide.Where(x => x.EventArgs.VerticalDelta >= 0 && x.EventArgs.VerticalOffset >= _slideToggleYPosition)
                .Repeat()
                .Subscribe(x =>
                {
                    double nextTransY = frame_Header.TranslationY - x.EventArgs.VerticalDelta;

                    nextTransY = Math.Max(-(frame_Header.Height + frame_Header.Y), nextTransY);
                    frame_Header.TranslationY = nextTransY;
                });

            //ヘッダーを表示する側の動作
            var show = Observable.FromEventPattern<ItemsViewScrolledEventArgs>(CollectionsView_Sessions, nameof(CollectionsView_Sessions.Scrolled));
            show.Where(x => x.EventArgs.VerticalDelta < 0)
                .Repeat()
                .Subscribe(x =>
                {
                    double nextTransY = frame_Header.TranslationY - x.EventArgs.VerticalDelta;
                    if (nextTransY > 0)
                    {
                        nextTransY = 0;
                    }

                    frame_Header.TranslationY = nextTransY;
                });
        }

こちらのソースコードはあとで載せる.