🦦
flutterでScrollspyとposition:stickyを実装してみる
久々に投稿します。
gif早くしたかったけどちょっと面倒でした、すいません。。
Q1.横タブメニューでその要素(指定widget)にページ内遷移しつつ押下したメニューがアクティブなるのwebでなんて言うんだっけ?
Q2.あと、要素(ボタンなど)を特定の位置までは画面下に固定、特定の位置から固定させる方法なんて言うんだっけ?
から始まりました。
▪︎とりあえず成果物。
A1.Scrollspy(Bootstrap)
スクロール位置に基づいてBootstrapナビゲーションまたはリストグループコンポーネントを自動的に更新し、ビューポート内で現在どのリンクがアクティブであるかを示します。
A2.position:sticky(css)
position は CSS のプロパティで、文書内で要素がどのように配置されるかを設定します。 top, right, bottom, left の各プロパティが、配置された要素の最終的な位置を決めます。
iosでなんかメニューがスライドする時に横にびよ~んて伸びるのどうにかしたい。
【Flutter】スクロールの挙動をカスタマイズする(引っ張り時のエフェクトを無効にする
)
▪︎Code
import 'package:flutter/material.dart';
class DetailScreen extends StatefulWidget {
const DetailScreen({Key? key}) : super(key: key);
State<DetailScreen> createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
final ScrollController _scrollController = ScrollController();
final ScrollController _horizontalScrollController = ScrollController();
final List<String> _sections = [
'セクションA',
'セクションB',
'セクションC',
'セクションD',
'セクションF'
];
int _currentSectionIndex = 0;
bool _itemVisibility = true;
// GlobalKeyを個々に宣言
final GlobalKey _sectionAKey = GlobalKey();
final GlobalKey _sectionBKey = GlobalKey();
final GlobalKey _sectionCKey = GlobalKey();
final GlobalKey _sectionDKey = GlobalKey();
final GlobalKey _sectionFKey = GlobalKey();
final GlobalKey _listViewKey = GlobalKey();
final GlobalKey _buttonKey = GlobalKey();
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
super.dispose();
}
///スクロールの処理
void _onScroll() {
final appBarHeight = AppBar().preferredSize.height; // AppBarの高さを取得
final List<GlobalKey> keys = [
_sectionAKey,
_sectionBKey,
_sectionCKey,
_sectionDKey,
_sectionFKey
];
double accumulatedHeight = 0.0;
for (int i = 0; i < keys.length; i++) {
final RenderBox box =
keys[i].currentContext?.findRenderObject() as RenderBox;
final position = box.localToGlobal(Offset.zero);
// セクションの高さを累積
accumulatedHeight += box.size.height;
// スクロール位置がこのセクションの範囲内にあるか確認
if (_scrollController.offset >= position.dy - appBarHeight &&
_scrollController.offset <
position.dy - appBarHeight + accumulatedHeight) {
if (_currentSectionIndex != i) {
setState(() {
_currentSectionIndex = i;
print("現在のセクション: ${_sections[i]}");
_centerSectionTab(i);
});
}
break;
}
}
///スクロール応募ボタン位置判定
final RenderBox buttonBox =
_buttonKey.currentContext?.findRenderObject() as RenderBox;
final buttonPosition = buttonBox.localToGlobal(Offset.zero);
final buttonYPosition = buttonPosition.dy;
final screenHeight = MediaQuery.of(context).size.height;
final widgetHeight = buttonBox.size.height;
if (buttonYPosition >= screenHeight - widgetHeight) {
//画面下部に固定されたコンテナ下部固定
setState(() {
_itemVisibility = true;
});
} else {
//画面下部に固定されたコンテナ要素内固定
setState(() {
_itemVisibility = false;
});
}
}
///タブの移動処理
void _centerSectionTab(int index) {
final RenderBox? tabBox =
_listViewKey.currentContext?.findRenderObject() as RenderBox?;
if (tabBox == null) return;
//_listViewKey を使用して、リストビューのサイズを取得します。
// リストビューの幅を screenWidth として取得します。
final double screenWidth = tabBox.size.width;
//各セクションの幅を表します。これはスクロールビューを均等に分割するために、
// スクリーン幅をセクションの数で割った値です。つまり、各セクションの幅がどれくらいであるかを計算します。
final double tabWidth = screenWidth / _sections.length;
//
final double scrollPosition =
(index * tabWidth) - (screenWidth / 2) + (tabWidth / 2);
_horizontalScrollController.animateTo(
scrollPosition,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
void _scrollToSection(GlobalKey targetKey) {
final List<GlobalKey> keys = [
_sectionAKey,
_sectionBKey,
_sectionCKey,
_sectionDKey,
_sectionFKey
];
double accumulatedHeight = 0.0;
// 目的のセクションの前までの高さを累積
for (GlobalKey key in keys) {
if (key == targetKey) {
break; // 目的のセクションに到達する前にループを終了
}
final RenderBox box = key.currentContext?.findRenderObject() as RenderBox;
accumulatedHeight += box.size.height;
}
final positionToScroll = accumulatedHeight; // AppBarの高さを考慮したスクロール位置
_scrollController.animateTo(
positionToScroll,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Scroll Spy Example'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(20),
child: Flexible(
child: Container(
color: Colors.grey[200],
child: ListView.builder(
key: _listViewKey,
physics: const ClampingScrollPhysics(), //iosの横ひっぱりエフェクト回避
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
itemCount: _sections.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
switch (index) {
case 0:
_scrollToSection(_sectionAKey);
break;
case 1:
_scrollToSection(_sectionBKey);
break;
case 2:
_scrollToSection(_sectionCKey);
break;
case 3:
_scrollToSection(_sectionDKey);
break;
case 4:
_scrollToSection(_sectionFKey);
break;
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
_sections[index],
style: TextStyle(
color: _currentSectionIndex == index
? Colors.blue
: Colors.black,
),
),
),
);
},
),
),
),
),
),
body: Stack(children: [
Column(
children: <Widget>[
// タブの表示
// スクロール可能なコンテンツ
Expanded(
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: [
Container(
key: _sectionAKey,
height: 800,
color: Colors.lightBlue[100],
child: Center(child: Text('セクションA')),
),
Container(
key: _sectionBKey,
height: 900,
color: Colors.yellow,
child: Center(child: Text('セクションB')),
),
Container(
key: _sectionCKey,
height: 1000,
color: Colors.lightBlue[100],
child: Center(child: Text('セクションC')),
),
Container(
key: _sectionDKey,
height: 1100,
color: Colors.blue,
child: Center(child: Text('セクションD')),
),
Container(
key: _sectionFKey,
height: 1500,
color: Colors.red,
child: Column(
children: [
Center(child: Text('セクションF')),
Padding(padding: EdgeInsets.only(top: 250)),
Container(
key: _buttonKey,
height: 150,
color: Colors.green,
child: const Center(
child: Text(
'画面下部に固定されたコンテナ',
style: TextStyle(
color: Colors.white,
fontSize: 18.0,
),
),
),
)
],
),
),
],
),
)),
],
),
Positioned(
bottom: 0, // 画面の下部に配置
left: 0, // 左側に配置することもできます
right: 0, // 右側に配置することもできます
child: Visibility(
visible: _itemVisibility,
child: Container(
color: Colors.green,
height: 150, // 固定コンテナの高さ
// ここにコンテンツを追加
child: const Center(
child: Text(
'画面下部に固定されたコンテナ',
style: TextStyle(
color: Colors.white,
fontSize: 18.0,
),
),
),
),
),
),
]),
);
}
}
▪︎実装するにあたり検討したことや気づいた点。
・セクションの高さは全て一定ではなくAPIで情報取得した時、情報量に対して不均一だよね。
・ん?最後のセクション高さ全然なかったらスクロールしないよねー。画面半分の高さとか追加しないとね。※未実装
良かったらご利用ください!
Discussion