2024/9/14 に石川県で開催された FlutterKaigi mini #2 で LT した資料です。
💪 モチベーション
- 🔋 愛車(EV)の日々の充電データをグラフ化したい
- 📊 デザインやインタラクションにもこだわりたい
- 🤔 fl_chart 等のパッケージは UI の細かい調整が難しい印象
- 😌 Flutter で凝った UI を実装してみたいな〜
👉 📊 Bar Chart を(業務で)自作したので LT でサクっと紹介します!
iOS「ヘルスケア」アプリの Bar Chart の UI に近いものを作ったため、以降は「ヘルスケア」アプリの動作のスクリーン動画を例に、その実装例を紹介していきます。
📊 1. Bar Chart のベース部分 & スクロールによる表示内容の変化
📊 1. Bar Chart ベース部分
- 水平方向の ListView で、アイテムが項目軸(X)の各データ要素を表現
- 各データ要素は基本的な Widget の組み合わせで
-
controllerに PageController、physicsに PageScrollPhysics を使うことでスクロール終了時にアイテム(=ページ)が綺麗にフィット - スクロールでのページ変化時に HapticFeedback つけるといい感じ
ListView.builder(
controller: usePageController(...),
physics: const PageScrollPhysics(),
scrollDirection: Axis.horizontal,
...
);
📊 2. スクロールによる表示内容の変化
- NotificationListenerとScrollNotificationでスクロール状態を検知
- 表示範囲内のデータによって以下の値を変化させる
- データの期間 → ラベルと合計値
- データの最大値 → 値軸(Y)の範囲と目盛り
NotificationListener<ScrollNotification>(
onNotification: (notification) {
// 表示範囲内のデータを求めてラベルや目盛りを変更
},
child: ListView.builder(
...
)
);
📊 3. バーのアニメーション
📊 3. バーのアニメーション
- 値軸(Y)の範囲が変わるとデータ要素の高さも変わる
- AnimationController で高さの変化をアニメーションさせることで、範囲が変わったことをわかりやすくなる
final controller = useAnimationController(...);
final value = useAnimation(CurvedAnimation(parent: controller, curve: ...));
...
SizedBox(
height: barHeight * value,
child: ...,
);
📊 4. タッチ中の要素にフォーカス
📊 4. タッチ中の要素にフォーカス
- データ要素をタッチ中はその要素だけのデータを値をラベルに表示
- GestureDetector でタッチ中の要素を判定
ListView.builder(
...
itemBuilder: (context, index) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (_) => onSelectItem(index),
onTapUp: (_) => onDeselectItem(),
onLongPressStart: (_) => onDeselectItem(index),
onLongPressEnd: (_) => onDeselectItem(),
onLongPressUp: () => onDeselectItem(),
onLongPressMoveUpdate: (details) {
final touchIndex = /* details.globalPositionのある要素インデックス */
onSelectItem(touchIndex);
},
child: ...
);
}
)
📊 SNS等にシェア(オマケ)
- Bar Chart の部分を画像にして SNS 等にシェアする
- Bar Chart カードの Widget を RepaintBoundary で囲い、指定したキーを通して得られる RenderRepaintBoundary から toImage() で画像データを取得
- share_plusパッケージを使ってシェア
RepaintBoundary(
key: key, // GlobalKey
child: BarChartCard(...),
);
final boundary =
key.currentContext!.findRenderObject() as RenderRepaintBoundary;
final image = await boundary.toImage();


