LoginSignup
5
7

More than 1 year has passed since last update.

【Flutter】tutorial_coach_mark の使い方

Posted at

初めに

tutorial_coach_mark とは、以下のように、特定の Widget にフォーカスして、ユーザにアプリの操作方法を理解してもらうためのチュートリアルを作成するパッケージです。

公式ページから引用

記事の対象者

  • アプリにチュートリアルを実装したい方
  • 操作が難しいアプリを開発している方
  • アプリに対して「操作がわかりにくい」とフィードバックがあった方

準備

パッケージの追加

まずは tutorial_coach_mark パッケージ を「 pubspeck.yaml 」に記述します。
パッケージのバージョンは、特に制約がなければ最新のバージョンで問題ありません。

pubspeck.yaml
dependencies:
  flutter:
    sdk: flutter

  tutorial_coach_mark: ^1.2.4

Pub get をしてパッケージの準備は完了です。

完成イメージ

tutorial_coach_mark.gif

簡単な入力フォームを作成し、その入力方法を説明するためのチュートリアルを作成します。
本来であれば、入力フォームにチュートリアルを設けることはないかと思いますが、今回は一例として実装します。

全体コード

tutorial_coach_mark_example.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';

class TutorialCoachMarkExample extends ConsumerStatefulWidget {
  const TutorialCoachMarkExample({Key? key}) : super(key: key);

  @override
  TutorialCoachMarkExampleState createState() =>
      TutorialCoachMarkExampleState();
}

class TutorialCoachMarkExampleState extends ConsumerState<TutorialCoachMarkExample> {
  late TutorialCoachMark tutorialCoachMark;
  List<TargetFocus> targets = [];

  final GlobalKey _key = GlobalKey();
  final GlobalKey _key1 = GlobalKey();
  final GlobalKey _key2 = GlobalKey();
  final GlobalKey _key3 = GlobalKey();

  @override
  void initState() {
    initTargets();
    WidgetsBinding.instance.addPostFrameCallback(_layout);
    super.initState();
  }

  void _layout(_) {
    Future.delayed(const Duration(milliseconds: 100));
    showTutorial();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: const Text("新規登録"),
      ),
      body: Center(
        child: Column(
          children: [
            const SizedBox(
              height: 30,
            ),
            Image.asset(
              "assets/flutter_logo.png",
            ),
            const SizedBox(
              height: 30,
            ),
            const Text("登録を完了させましょう!"),
            const SizedBox(
              height: 30,
            ),
            SizedBox(
              key: _key,
              width: MediaQuery.of(context).size.width * 0.8,
              child: const TextField(
                decoration: InputDecoration(
                    border: OutlineInputBorder(), hintText: "ニックネーム"),
              ),
            ),
            const SizedBox(
              height: 30,
            ),
            SizedBox(
              key: _key1,
              width: MediaQuery.of(context).size.width * 0.8,
              child: const TextField(
                decoration: InputDecoration(
                    border: OutlineInputBorder(), hintText: "メールアドレス"),
              ),
            ),
            const SizedBox(
              height: 30,
            ),
            SizedBox(
              key: _key2,
              width: MediaQuery.of(context).size.width * 0.8,
              child: const TextField(
                obscureText: true,
                decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "パスワード",
                ),
              ),
            ),
            const SizedBox(
              height: 30,
            ),
            ElevatedButton(
              key: _key3,
              style: ElevatedButton.styleFrom(primary: Colors.blue[40]),
              onPressed: () {},
              child: const Padding(
                padding: EdgeInsets.all(10.0),
                child: Text("登録"),
              ),
            ),
          ],
        ),
      ),
    );
  }

  void initTargets() {
    targets.add(
      TargetFocus(
        identify: "Target 0",
        keyTarget: _key,
        contents: [
          TargetContent(
            align: ContentAlign.bottom,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: const <Widget>[
                Text(
                  "ニックネームを入力してください。",
                  style: TextStyle(color: Colors.white, fontSize: 18.0),
                ),
              ],
            ),
          )
        ],
        shape: ShapeLightFocus.RRect,
        radius: 5,
      ),
    );
    targets.add(
      TargetFocus(
        identify: "Target 1",
        keyTarget: _key1,
        contents: [
          TargetContent(
              align: ContentAlign.bottom,
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisAlignment: MainAxisAlignment.center,
                children: const <Widget>[
                  Text(
                    "メールアドレスを入力してください。",
                    style: TextStyle(color: Colors.white, fontSize: 18.0),
                  ),
                ],
              ))
        ],
        shape: ShapeLightFocus.RRect,
        radius: 5,
      ),
    );

    targets.add(TargetFocus(
      identify: "Target 2",
      keyTarget: _key2,
      contents: [
        TargetContent(
            align: ContentAlign.top,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: const <Widget>[
                Padding(
                  padding: EdgeInsets.only(bottom: 20.0),
                  child: Text(
                    "パスワードを入力してください。",
                    style: TextStyle(color: Colors.white, fontSize: 18.0),
                  ),
                ),
              ],
            )),
        TargetContent(
            align: ContentAlign.bottom,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: const <Widget>[
                Padding(
                  padding: EdgeInsets.only(bottom: 20.0),
                  child: Text(
                    "パスワードは安全に保管してください。",
                    style: TextStyle(color: Colors.white, fontSize: 14.0),
                  ),
                ),
              ],
            ),
        ),
      ],
      shape: ShapeLightFocus.RRect,
      radius: 5,
    ));

    targets.add(TargetFocus(
      identify: "Target 3",
      keyTarget: _key3,
      // color: Colors.red,
      contents: [
        TargetContent(
            align: ContentAlign.top,
            child: Column(
              children: <Widget>[
                Padding(
                  padding: const EdgeInsets.all(10.0),
                  child: Image.asset(
                    "assets/icon_flutter.png",
                    fit: BoxFit.contain,
                    height: 120,
                  ),
                ),
                const Padding(
                  padding: EdgeInsets.symmetric(vertical: 20.0),
                  child: Text(
                    "三つの項目が入力できたらこのボタンを押して登録完了です!",
                    style: TextStyle(color: Colors.white, fontSize: 18.0),
                  ),
                ),
              ],
            ))
      ],
      shape: ShapeLightFocus.RRect,
      radius: 10
    ));
  }

  void showTutorial() {
    tutorialCoachMark = TutorialCoachMark(
      targets: targets,
      textSkip: "SKIP",
      paddingFocus: 10,
      opacityShadow: 0.8,
    )..show(context: context);
  }
}

実装

それぞれ解説していきます。なお、今回は tutorial_coach_mark の使い方を解説することがこの記事の趣旨であるため、フォームの実装方法については解説しません。

tutorial_coach_mark_example.dart
  List<TargetFocus> targets = [];

  final GlobalKey _key = GlobalKey();
  final GlobalKey _key1 = GlobalKey();
  final GlobalKey _key2 = GlobalKey();
  final GlobalKey _key3 = GlobalKey();

このコードでは TargetFocus のリストを targets という変数に代入し、初期値を空のリストにしています。

また、 _key から _key4 までの変数にグローバルキーを代入しています。このグローバルキーはチュートリアルでどの要素にフォーカスするかを指定する時に必要になります。

tutorial_coach_mark_example.dart
  @override
  void initState() {
    initTargets();
    WidgetsBinding.instance.addPostFrameCallback(_layout);
    super.initState();
  }

このコードでは initState() でページの初期状態を指定しています。
initTargets() は、先ほど作成した TargetFocus の空のリストに TargetFocus を追加していく処理になります。詳しくは後述します。

WidgetsBinding.instance.addPostFrameCallback()
この関数は全ての Widget のビルドが終わった時に実行する処理を記述するための関数です。
そしてこの関数は最初にページが読み込まれたとき、一度しか発火しません。
したがって、ページが読み込まれた瞬間にチュートリアルを実行する必要がある際に最適なメソッドと言えます。

上のコードでは、addPostFrameCallback() 関数の引数に _layout が入れられています。
この _layout は自作のメソッドで、チュートリアルを表示させるメソッドが含まれています。

tutorial_coach_mark_example.dart
  void _layout(_) {
    Future.delayed(const Duration(milliseconds: 100));
    showTutorial();
  }

このコードは先述した _layout() の内容を記述したコードです。
Future.delayed() メソッドではチュートリアルを表示させる時間を遅らせています。

showTutorial() メソッドは、チュートリアルを表示させるメソッドで、全ての Widget のビルドが完了し、_layout() メソッドが実行された時に実行されます。
詳しい内容は後述します。

tutorial_coach_mark_example.dart
SizedBox(
  key: _key,
  width: MediaQuery.of(context).size.width * 0.8,
  child: const TextField(
    decoration: InputDecoration(
      border: OutlineInputBorder(), hintText: "ニックネーム"
    ),
  ),
),

このコードでは SizedBox の子要素に TextField を指定することで大きさを調整して表示させています。

key: _key,
このようにすることで、 SizedBox にグローバルキーが割り当てられます。
このキーがあることで、それぞれのチュートリアルの場面で異なる Widget にフォーカスを当てたり、表示させる内容を変更したりすることができるようになります。

このコード以下の SizedBox にそれぞれ別のグローバルキーを割り当てることで、独自の内容を表示させています。グローバルキーの割り当ては全く同じなので、その他の要素に関しては省略します。

tutorial_coach_mark_example.dart
  void initTargets() {
    targets.add(
      TargetFocus(
        identify: "Target 0",
        keyTarget: _key,
        contents: [
          TargetContent(
            align: ContentAlign.bottom,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: const <Widget>[
                Text(
                  "ニックネームを入力してください。",
                  style: TextStyle(color: Colors.white, fontSize: 18.0),
                ),
              ],
            ),
          )
        ],
        shape: ShapeLightFocus.RRect,
        radius: 5,
      ),
    );

このコードでは targts のリストに TargetFocus を追加しています。
以下で詳しく解説していきます。

tutorial_coach_mark_example.dart
identify: "Target 0",

identifyTargetFocus がどの要素に当てられているものか、何を説明するためのものかなど、 TargetFocus の説明などを String 型で指定するためのプロパティです。

公式ページでも使用方法が厳しく限定されているわけではないため、 TargetFocus の説明を簡潔にまとめれば自分にもわかりやすくなります。

tutorial_coach_mark_example.dart
keyTarget: _key,

このコードでフォーカスさせたい要素のグローバルキーを指定しています。
keyTarget がないとフォーカスする場所がないため、エラーになります。

tutorial_coach_mark_example.dart
contents: [
  TargetContent(
    align: ContentAlign.bottom,
    child: Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: const <Widget>[
        Text(
          "ニックネームを入力してください。",
          style: TextStyle(color: Colors.white, fontSize: 18.0),
        ),
      ],
    ),
  )
],

contents ではチュートリアルで、特定の Widget にフォーカスが当たった際に表示される要素を指定しています。
表示させる要素は TargetContent() で囲む必要があります。
align でコンテンツを表示させる位置を指定します。
child で表示させる Widget を指定します。上のコードではテキストを表示させています。
テキスト以外にも画像などを表示させることも可能です。

tutorial_coach_mark_example.dart
shape: ShapeLightFocus.RRect,
radius: 5,

このコードではフォーカスの形を指定しています。
上のようにすると四角のフォーカスが当たります。

shape: ShapeLightFocus.Circle,
このようにすると円形のフォーカスが当たります。

radius では、フォーカスを四角にしたときのみ、その角を調整することができます。

tutorial_coach_mark_example.dart
  void initTargets() {
    targets.add(
      TargetFocus(
        identify: "Target 0",
        keyTarget: _key,
        contents: [
          TargetContent(
            align: ContentAlign.bottom,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: const <Widget>[
                Text(
                  "ニックネームを入力してください。",
                  style: TextStyle(color: Colors.white, fontSize: 18.0),
                ),
              ],
            ),
          )
        ],
        shape: ShapeLightFocus.RRect,
        radius: 5,
+       color: Colors.red,
      ),
    );

color を指定すると、以下のように特定のフォーカスだけ背景色を変更することができます。
tutorial_coach_nark_red.gif

tutorial_coach_mark_example.dart
    targets.add(TargetFocus(
      // identify: "Target 2",
      keyTarget: _key2,
      contents: [
        TargetContent(
            align: ContentAlign.top,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: const <Widget>[
                Padding(
                  padding: EdgeInsets.only(bottom: 20.0),
                  child: Text(
                    "パスワードを入力してください。",
                    style: TextStyle(color: Colors.white, fontSize: 18.0),
                  ),
                ),
              ],
            )),
        TargetContent(
            align: ContentAlign.bottom,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: const <Widget>[
                Padding(
                  padding: EdgeInsets.only(bottom: 20.0),
                  child: Text(
                    "パスワードは安全に保管してください。",
                    style: TextStyle(color: Colors.white, fontSize: 14.0),
                  ),
                ),
              ],
            ),
        ),
      ],
      shape: ShapeLightFocus.RRect,
      radius: 5,
+     focusAnimationDuration: Duration(seconds: 2),
    ));

focusAnimationDurationDuration を2秒に指定すると、2秒かけてフォーカスするようになるため、他の要素へのフォーカスと比較して以下のようにフォーカスする際の速度が遅くなります。
duartion_sample.gif
パスワードの TextField にフォーカスする際にのみフォーカスの速度が遅くなっているのが分かるかと思います。

tutorial_coach_mark_example.dart
  void showTutorial() {
    tutorialCoachMark = TutorialCoachMark(
      targets: targets,
      textSkip: "SKIP",
      paddingFocus: 10,
      opacityShadow: 0.8,
    )..show(context: context);
  }

このコードはチュートリアルを表示させるためのメソッドを記述しています。

targets: targets,
この記述で、チュートリアルのターゲットに TargetFocus のリストが代入された targets が指定されています。

textSkip: "SKIP",
この記述で、スキップボタンのテキストを変更しています。
なお、何も指定していないデフォルトの状態では「SKIP」というテキストになります。

paddingFocus: 10,
この記述で特定の要素にフォーカスした際のその要素との空白を指定しています。以下の画像では薄い赤色の部分になります。

opacityShadow: 0.8,
この記述でフォーカスされている部分以外の透明度を指定しています。
この値を変更すると以下のようになります。

opacityShadow: 0.8,


opacityShadow: 0.3,


TutorialCoachMark() には show() メソッドがあり、これを実行することでチュートリアルが表示されます。

以上です。

あとがき

最後まで読んでいただきありがとうございました。

参考にしていただければ幸いです。
誤っている箇所があればご指摘いただければ幸いです。

参考にしたサイト

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7