【Flutter練習帳】GestureDetectorをつかってボタンを作る


最近、Flutterを始めました。

練習と備忘録をかねて記事にまとめています。


実際に動かしてみたいけどFlutterの開発環境がない人は、
https://dartpad.dev/
にペタっとコピペして試してみてください。


結果だけ興味があるかたは、下までスクロールしてください！

今回は、`GestureDetector`を使って、ボタンを作る練習と、
カスタムウィジット(クラス)をつくる練習をしています。

ボタンやったらButton系のクラスを使えばええやん？
そう思いますが、そうはいかないときや、ボタンではないものをクリックしたいとき、
タップ以外の操作でもイベントを起こしたいときに`GestureDetector`は使えます。


<details><summary>サンプルコード　基本ボタン</summary>

```dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter button custom',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter button custom'),
    );
  }
}

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

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        // 初期のコード
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            // 区切り
            const SizedBox(
              height: 10,
            ),

            GestureDetector(
              onTap: () {
                // 実行したい処理を入れる
                print('tap');
              },
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Align(
                    alignment: Alignment.center,
                    child: Container(
                      padding: const EdgeInsets.all(12),
                      height: 50,
                      width: 100,
                      decoration: BoxDecoration(
                        color: Colors.blue,
                        border: Border.all(color: Colors.red),
                        borderRadius: BorderRadius.circular(10),
                      ),
                      child: const Text('ボタンだよ'),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), 
    );
  }
}

```
</details>

シンプルですね
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922656/7abafcde-ca7d-318f-558d-09ccacc904ab.png)


べた書きしているコードを、カスタムウィジットとして抽出します。

VSCodeでは書き出したいウィジットをクリック（カーソルがあればいい）＞電球？マークをクリックで簡単にできます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922656/68a1c25e-7a39-4bde-3fc1-7e4e009b10e9.png)

android studioでは Flutter OutLineをクリック＞書き出したいウィジットを右クリックで簡単にできます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922656/c5032324-7f64-7064-e31f-4d5c2f6cbda1.png)



<details><summary>カスタムウィジットと並べてみる</summary><div>

```dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter button custom',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter button custom'),
    );
  }
}

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

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        // 初期のコード
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            // 区切り
            const SizedBox(
              height: 10,
            ),

            GestureDetector(
              onTap: () {
                // 実行したい処理を入れる
                print('tap');
              },
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Align(
                    alignment: Alignment.center,
                    child: Container(
                      padding: const EdgeInsets.all(12),
                      height: 50,
                      width: 100,
                      decoration: BoxDecoration(
                        color: Colors.blue,
                        border: Border.all(color: Colors.red),
                        borderRadius: BorderRadius.circular(10),
                      ),
                      child: const Text('ボタンだよ'),
                    ),
                  ),
                ],
              ),
            ),

            // 区切り
            const SizedBox(
              height: 10,
            ),

            // カスタムウィジットとして切り出し
            const CustomButton(),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), 
    );
  }
}

// 切り出したカスタムウィジット
class CustomButton extends StatelessWidget {
  const CustomButton({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        // 実行したい処理を入れる
        print('カスタム');
      },
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Align(
            alignment: Alignment.center,
            child: Container(
              padding: const EdgeInsets.all(12),
              height: 50,
              width: 100,
              decoration: BoxDecoration(
                color: Colors.blue,
                border: Border.all(color: Colors.red),
                borderRadius: BorderRadius.circular(10),
              ),
              child: const Text('ボタンだよ'),
            ),
          ),
        ],
      ),
    );
  }
}

```
</div></details>


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922656/37a596d3-7807-e879-e7e9-bcc8fb3a06b7.png)

同じようなウィジットが並ぶレイアウトを作成したい場合、コードがさっぱりします。
このようにFlutterでは、ウィジットを駆使して画面を作成していきます。

<details><summary>さらに汎用的なカスタムウィジット</summary><div>

```dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter button custom',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter button custom'),
    );
  }
}

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

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        // 初期のコード
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            // 区切り
            const SizedBox(
              height: 10,
            ),

            GestureDetector(
              onTap: () {
                // 実行したい処理を入れる
                print('tap');
              },
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Align(
                    alignment: Alignment.center,
                    child: Container(
                      padding: const EdgeInsets.all(12),
                      height: 50,
                      width: 100,
                      decoration: BoxDecoration(
                        color: Colors.blue,
                        border: Border.all(color: Colors.red),
                        borderRadius: BorderRadius.circular(10),
                      ),
                      child: const Text('ボタンだよ'),
                    ),
                  ),
                ],
              ),
            ),

            // 区切り
            const SizedBox(
              height: 10,
            ),

            // カスタムウィジットとして切り出し
            const CustomButton(),

            // 区切り
            const SizedBox(
              height: 10,
            ),

            // パラメータを使用したカスタムウィジット
            const CustomButton2(
              text: 'ボタンだよ',
              borderColor: Colors.red,
              backgroundColor: Colors.blue,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

// 切り出したカスタムウィジット
class CustomButton extends StatelessWidget {
  const CustomButton({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        // 実行したい処理を入れる
        print('カスタム');
      },
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Align(
            alignment: Alignment.center,
            child: Container(
              padding: const EdgeInsets.all(12),
              height: 50,
              width: 100,
              decoration: BoxDecoration(
                color: Colors.blue,
                border: Border.all(color: Colors.red),
                borderRadius: BorderRadius.circular(10),
              ),
              child: const Text('ボタンだよ'),
            ),
          ),
        ],
      ),
    );
  }
}

// パラメータを使用したカスタムウィジット
class CustomButton2 extends StatelessWidget {
  // プロパティ
  final String text;
  final Color backgroundColor;
  final Color borderColor;

  const CustomButton2({
    super.key,
    // コンストラクタ
    required this.text,
    required this.backgroundColor,
    required this.borderColor,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        // 実行したい処理を入れる
        print('プロパティ');
      },
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Align(
            alignment: Alignment.center,
            child: Container(
              padding: const EdgeInsets.all(12),
              height: 50,
              width: 100,
              decoration: BoxDecoration(
                color: backgroundColor,
                border: Border.all(color: borderColor),
                borderRadius: BorderRadius.circular(10),
              ),
              child: Text(text),
            ),
          ),
        ],
      ),
    );
  }
}

```
</div></details>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922656/2ecffb8f-3467-08a6-0d49-f1b4c82fcfe4.png)

プロパティを指定できるようにしました。
これにより、同じウィジットを使って別の表現ができます。

今回は、この方法でいろいろ試してみました。


<details><summary>いろいろ試した結果</summary><div>

```dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter button custom',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter button custom'),
    );
  }
}

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

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        padding: const EdgeInsets.all(20),
        child: Center(
          // 初期のコード
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '$_counter',
                style: Theme.of(context).textTheme.headlineMedium,
              ),
              // 区切り
              const SizedBox(
                height: 10,
              ),

              GestureDetector(
                onTap: () {
                  // 実行したい処理を入れる
                  print('tap');
                },
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Align(
                      alignment: Alignment.center,
                      child: Container(
                        padding: const EdgeInsets.all(12),
                        height: 50,
                        width: 100,
                        decoration: BoxDecoration(
                          color: Colors.blue,
                          border: Border.all(color: Colors.red),
                          borderRadius: BorderRadius.circular(10),
                        ),
                        child: const Text('ボタンだよ'),
                      ),
                    ),
                  ],
                ),
              ),

              // 区切り
              const SizedBox(
                height: 10,
              ),

              // プロパティを反映したカスタムウィジット
              CustomButton2(
                text: 'ボタンだよ',
                borderColor: Colors.blue, 
                backgroundColor: Colors.white, 
                onTap: () {
                  print('aの処理をする');
                },
                icon: const Icon(Icons.insert_chart),
                subText: "ぼたんらしいよ",
                isHot: false,
                shadow: true,
                gradation: false,
              ),

              // 区切り
              const SizedBox(
                height: 10,
              ),

              // プロパティを反映したカスタムウィジット
              CustomButton2(
                text: 'ボタンだよ',
                borderColor: Colors.blue, 
                backgroundColor: Colors.white, 
                onTap: () {
                  print('bの処理をする');
                },
                icon: const Icon(Icons.pets),
                subText: "ぼたんらしいよ",
                isHot: true,
                shadow: false,
                gradation: true,
                backgroundColorSecond: Colors.indigo,
              ),

              // 区切り
              const SizedBox(
                height: 10,
              ),

              // プロパティを反映したカスタムウィジット
              CustomButton2(
                text: 'ボタンだよ',
                borderColor: Colors.white,
                backgroundColor: Colors.blue,
                onTap: () {
                  print('cの処理をする');
                },
                icon: const Icon(Icons.abc),
                subText: "ぼたんらしいよ",
                isHot: false,
                shadow: false,
                gradation: false,
              ),

              // 区切り
              const SizedBox(
                height: 10,
              ),

              // プロパティを反映したカスタムウィジット
              CustomButton2(
                text: 'ボタンだよ', 
                borderColor: Colors.white,
                backgroundColor: Colors.blue, 
                onTap: () {
                  print('dの処理をする');
                },
                icon: const Icon(Icons.create),
                subText: "ぼたんらしいよ",
                isHot: false,
                shadow: false,
                gradation: true,
                backgroundColorSecond: Colors.yellow,
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

// プロパティを使用したカスタムウィジット
class CustomButton2 extends StatelessWidget {
  // プロパティ
  final String text; 
  final Color backgroundColor; 
  final Color borderColor; 
  final VoidCallback? onTap;
  final Icon icon;
  final String subText;
  final bool isHot;
  final bool shadow;
  final bool gradation;
  final Color? backgroundColorSecond;

  const CustomButton2({
    super.key,
    // コンストラクタ
    required this.text,
    required this.backgroundColor,
    required this.borderColor,
    this.onTap,
    required this.icon,
    required this.subText,
    required this.isHot,
    required this.shadow,
    required this.gradation,
    this.backgroundColorSecond,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Align(
            alignment: Alignment.center,
            child: Container(
              padding: const EdgeInsets.all(12),
              decoration: shadow
                  ? BoxDecoration(
                      color: backgroundColor,
                      border: Border.all(color: borderColor), 
                      borderRadius: BorderRadius.circular(50),
                      boxShadow: const [
                        BoxShadow(blurRadius: 5, color: Colors.grey),
                      ],
                    )
                  : gradation
                      ? BoxDecoration(
                          gradient: LinearGradient(
                              begin: Alignment.topLeft,
                              end: Alignment.bottomRight,
                              colors: [
                                backgroundColor,
                                backgroundColorSecond!,
                              ]), 

                          border: Border.all(color: borderColor),
                          borderRadius: BorderRadius.circular(50),
                        )
                      : BoxDecoration(
                          color: backgroundColor, 
                          border: Border.all(color: borderColor), 
                          borderRadius: BorderRadius.circular(50),
                        ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          SizedBox(
                            child: Center(child: icon),
                          ),
                          Text(text),
                          Visibility(
                              visible: isHot,
                              child: const Icon(Icons.whatshot_outlined))
                        ],
                      ),
                      Text(subText),
                    ],
                  ),
                  const SizedBox(
                    child: Center(child: Icon(Icons.arrow_forward_outlined)),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}


```
</div></details>

いろいろとイケてないところは、気にしないでください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/922656/9cc2acbd-d18e-c698-0cb7-9fa5aba74a75.png)


まだまだ、カスタマイズしたいところですが、今回はこれぐらいにしました！

最後まで見ていただいてありがとうございます！！！