[Flutter]サムネイル選択アニメーション

対象者

この記事は、Flutterの基本的な知識を持ち、Widgetの操作やアニメーションに興味がある初級から中級のFlutter開発者を対象としています。

  • Widgetの位置やサイズを取得したい人
  • 取得した位置により、他のWidgetの位置を調整したい人
  • サムネイル画像を選択したときの外枠をアニメーションさせたい人

はじめに

私たちの日常業務の中で、サムネイルを選択する瞬間は、一つ一つの選択が重要な意味を持ちます。
この選択プロセスを、より直感的で、視覚的に魅力的なものに変える方法として、アニメーションの使用が浮かび上がりました。特に、タップしたサムネイルを囲む外枠が動くようなアニメーションは、多くのアプリケーションで採用されており、その効果は目に見えて明らかです。このようなアニメーションをFlutterで実現する方法について、私はテストアプリを実装しました。

このブログ記事では、その知見を基に、Flutterで外枠が移動するアニメーションを実装する方法を、実際のソースコードを交えて解説します。読者の皆様がこの記事を通じて、Flutterでのアニメーション実装の技術を深め、より魅力的なユーザーインターフェースを創造できることを願っています。
Flutterでは、アプリケーションのUIを構築するために多くのウィジェットが提供されています。今回は、GlobalKeyを使用してWidgetの位置とサイズを取得し、AnimatedPositionedウィジェットを使って選択された四角形に外枠をアニメーションで表示する方法を紹介します。

Flutterで複数の四角形の中からタップされた四角形に外枠を表示し、その外枠にアニメーションを適用する方法について解説します。このプロセスを通じて、Widgetの絶対座標とサイズを取得し、取得した座標を利用する方法、そしてAnimatedPositionedウィジェットを使ったアニメーションの実装方法を学びます。

ソースコード

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _keys = [
    GlobalKey(),
    GlobalKey(),
    GlobalKey(),
  ];

  final _colors = [Colors.red, Colors.green, Colors.blue];

  var _selectedIndex = 1;

  @override
  Widget build(BuildContext context) {
    final renderBox =
        _keys[_selectedIndex].currentContext?.findRenderObject() as RenderBox?;
    final position = renderBox?.localToGlobal(Offset.zero);

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: [
          const SizedBox(height: 32),
          Stack(
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  const SizedBox(width: 8),
                  for (int i = 0; i < _keys.length; i++) buildTapBox(i),
                ],
              ),
              if (position != null)
                AnimatedPositioned(
                  left: position.dx,
                  duration: const Duration(milliseconds: 150),
                  child: Container(
                    height: renderBox?.size.height,
                    width: renderBox?.size.width,
                    decoration: BoxDecoration(
                      color: Colors.transparent,
                      border: Border.all(color: Colors.black, width: 5),
                    ),
                  ),
                ),
            ],
          ),
        ],
      ),
    );
  }

  Widget buildTapBox(int index) {
    return GestureDetector(
      onTap: () {
        setState(() {
          _selectedIndex = index;
        });
      },
      child: Container(
        key: _keys[index],
        width: 100,
        height: 100,
        color: _colors[index],
      ),
    );
  }
}

解説

位置情報の取得とAnimatedPositionedウィジェットの利用に焦点を当て、解説していきます。

GlobalKeyの使用

final _keys = [
  GlobalKey(),
  GlobalKey(),
  GlobalKey(),
];

上記のコードでは、GlobalKeyのリストを作成しています。これは、各四角形(Containerウィジェット)に一意のキーを割り当てるために使用されます。GlobalKeyを使用することで、後でこれらのウィジェットの現在の状態やコンテキストにアクセスできるようになります。これにより、特定のウィジェットの位置やサイズなどの情報を取得できるようになります。

位置情報の取得

final renderBox =
    _keys[_selectedIndex].currentContext?.findRenderObject() as RenderBox?;
final position = renderBox?.localToGlobal(Offset.zero);

ここでは、選択された四角形のGlobalKeyを使用して、そのRenderBoxオブジェクトを取得しています。RenderBoxは、Flutterのレンダリングシステムにおけるウィジェットの位置やサイズなどの情報を持つオブジェクトです。localToGlobalメソッドを使って、ウィジェットの画面上の絶対座標を取得しています。この座標は、後にAnimatedPositionedウィジェットで使用されます。

AnimatedPositionedの利用

if (position != null)
  AnimatedPositioned(
    left: position.dx,
    duration: const Duration(milliseconds: 150),
    child: Container(
      height: renderBox?.size.height,
      width: renderBox?.size.width,
      decoration: BoxDecoration(
        color: Colors.transparent,
        border: Border.all(color: Colors.black, width: 5),
      ),
    ),
  ),

この部分では、取得した位置情報をAnimatedPositionedウィジェットのleftプロパティに適用しています。AnimatedPositionedは、子ウィジェットをアニメーションで移動させるために使用されるウィジェットです。ここでは、選択された四角形の位置に外枠をアニメーションで表示するために使用しています。durationプロパティにより、アニメーションの速度を調整しています。

このWidgetは2回目移行の描画で有効になります。1回目は各四角形の場所が定まってないため、positionがnullになります。2回目の描画では四角形の場所が計算されているのでpositionが取得でき、外枠となるWidgetが表示されます。

また、leftのみの指定で、topは指定してません。position.dyはAppBarなども含めた絶対座標で取得するため、親Widgetからの相対パスで位置設定をするAnimatedPositionedではうまく動作しませんでした。

実装の流れのまとめ

  1. GlobalKeyを使用して各四角形の参照を保持します。 これにより、後で特定のウィジェットの位置やサイズを取得できるようになります。
  2. タップイベントで選択された四角形のインデックスを更新し、その四角形の位置情報を取得します。 タップされたウィジェットのGlobalKeyを使用して、そのRenderBoxから位置情報を取得します。
  3. Stackウィジェット内のAnimatedPositionedを使用して、取得した位置に外枠をアニメーションで表示します。 これにより、ユーザーがタップした四角形を視覚的に強調表示できます。

このように、GlobalKey、位置情報の取得、そしてAnimatedPositionedの利用により、Flutterで動的なUIを実現する方法を理解することができます。

Q&A

Q: GlobalKeyはどのような時に使用するべきですか?

A: Widgetの現在の状態やコンテキストにプログラムからアクセスする必要がある場合に使用します。ただし、乱用は避け、必要な場合にのみ使用してください。

Q: AnimatedPositionedのアニメーション速度を変更するには?

A: durationプロパティを調整することで、アニメーションの速度を変更できます。

まとめ

この記事では、FlutterでWidgetの絶対座標とサイズを取得し、AnimatedPositionedウィジェットを使用してアニメーションを実装する方法を学びました。この技術は、ユーザーインタラクションに応じて動的なUI変更を行いたい場合に特に有用です。Flutterの柔軟性と強力なウィジェットシステムを活用して、魅力的なユーザー体験を提供しましょう。