Xamarin.Formsで上下スクロールにあわせてナビゲーションバーを表示・非表示
ShellやNavigationのナビゲーションバーを直接動かすことはできないので,Frameコントロールでナビゲーションバーにあたるものを配置して,ということです.
さて,モバイルアプリは上下にスクロールして使うUIが多いです.
上部にナビゲーションバーがあって,その下にページが配置されてスクロールするというタイプですね.
そこでおしゃれなUIによくあるのが,ページを上下にスワイプすると,ナビゲーションバーが表示されたり表示されなくなったり,とか,一定量スクロールすると一緒にスクロールアップして画面外に消えたり,とかです.
サンプル
フェードイン/アウト
これはUdemyアプリの例です.
最初,ナビゲーションバーは表示されていませんが,ある程度スクロールダウンするとナビゲーションバーがフェードインしてきます.
おしゃれです.
一緒にスクロールアップ/ダウン
こちらはLinkedInです.
ある程度スクロールアップすると一緒にスクロールして画面外に行き,スクロールダウンすると表示されるタイプです.
作る
これらをXamarin.Formsで作るのは比較的かんたんです. コントロールのアニメーション機能と,スクロールイベントを組み合わせて,調整するだけです.
フェードイン/アウト
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); } }
一緒にスクロールアップ/ダウン
これは指定した固定のスクロール位置からナビゲーションバーのスクロールアップ,ダウンが開始するものです.
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つめと同じで決まった位置からナビゲーションバーがスクロールアップしますが,スクロールダウンのときはその位置からすぐにナビゲーションバーがスクロールダウンしてくるタイプです.
以下のような動作になります.
動作は期待どおりに作れましたが,コードがはずかしい感じ.
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; } }
ソースコード
今回のソースコードはこちら.
追記
ソースコードにReactive Extensionsを使ったバージョンを追加しました.
記事はこちら.
おまけ
これを作りたい.
追記
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; }); }
こちらのソースコードはあとで載せる.