Flutter備忘録その3

# はじめに
Flutterについての備忘録です。
もろもろコードや文法などを記載していきます
なにか間違っていたり、より「こっちのほうがいいよ！」って場合はコメント等お願いします。

:::note warn

備忘録なので解説が雑な部分があります。ご注意ください。
:::

なお以下の記事の続編です
①

https://qiita.com/nanashi39/items/0a7a65feeaaecec9b56d

②

https://qiita.com/nanashi39/items/e09b08da960b93afcd28

③

https://qiita.com/nanashi39/items/a8a2c01b597f0154e77c

## アジェンダ
1. [StatelessWidgetとStatefulWidget](#statelesswidgetとstatefulwiget)
2. [InteractiveViewerについて](#interactiveviewerについて)
3. [Stackについて](#stackについて)
## StatelessWidgetとStatefulWiget


今回は最序章でも軽く触れていたStateless,Statefulについてです。


### **StatelessWiget**
- **状態を持たないウィジェット**
- 一度描画される(**初期化**)されると見た目が変わらない
- 例えば固定テキストや、アイコンや画像etc...


### **StatefulWiget**
- **状態を持つウィジェット**
- ユーザの操作や経過などによって状態が変化するものであり**動的**なもの
- 例としてテキストボックス、タイマーやカウンターetc...

--- 
#### 2つのウィジェットの基本構文

親クラスである``StatelessWidget``を継承して**状態を持たない**「**Stateless**」

**基本構文 (StatelessWidget)**
```dart
class クラス名 extends StatelessWidget{
 const クラス名({Key?key}) : super(key:key); // コンストラクタ

 @override
 Widget build(BuildeContext context){
     return const Scaffold();
 }
}
```

- ①この部分は**コンストラクタとなりクラスのメンバ変数の初期化**を行う
```dart
const クラス名({Key?key}) : super(key:key); // コンストラクタ
```
- ② この部分はBuild(画面の描画,画面構築)

```dart
@override
 Widget build(BuildeContext context){
     return const Widget( // UIを記述);
 }
```



---

**基本構文(StatefulWidget)**

- ①　**シンプルにStatefulを継承して動的なクラスの管理ができるクラスと明示する**
```dart
class クラス名 extends StatefulWidget{
 const クラス名({Key?key}) : super(key:key);

 @override
  State<クラス名> createState() => _クラス名State();
 }

class _クラス名State() extends State<クラス名> {
    // ここに動的に変化する関数などを記述

    @override
    Widget build(BuildContext context){
     return Widget();
 } 
}
```
- **②コンストラクタでメンバ変数を初期化** 
```dart
const クラス名({Key?key}) : super(key:key);
```
- **③ この書き方は関数呼び出しの書き方の省略形となっている、ここで描画を呼び出し**

**これと**
```dart
 @override
  State<クラス名> createState() => _クラス名State();
 }
```

**これは**
```dart
State<クラス名> createState(){
    return _クラス名State()
}
```

**同義です。**

**④ 画面構築**

```dart

class _クラス名State() extends State<クラス名> {
    // ここに動的に変化する関数などを記述

    @override
    Widget build(BuildContext context){
     return Widget();
 } 
```



---

画面描画タイミングが3つある
- 初回画面構築時
- 親ウィジェット更新時
- 状態更新時

#### 初回画面構築時

``コンストラクタ　→　createState() // 状態管理 → initState() // 初めて生成のとき一度のみ生成(ここに初期化を記述) →　didChangeDependencies() // 依存関係などが更新されたときに呼び出される　→ build() // 描画　``

#### 親ウィジェット更新時(新しい設定での更新時)
``コンストラクタ →　didChangeDependencies() // 依存オブジェクト変更のため　→ build() ``
#### 状態更新時(ウィジェットの状態が変更した時画面を更新したいとき)
``setState() // 変更を検知して更新を指示する特殊なメソッド　→　build()``





--- 

## InteractiveViewerについて
拡大,縮小や左右に移動できる要素(通称：パン)などを可能にするウィジェットです。
子ウィジェットを先ほどの要素などを追加できるような機能を提供してくれます。

基本的なメソッドやそれらの機能、引数については公式のリファレンスを読むとより分かりやすいかなと思います。


### 指定できる引数について(抜粋)
--- 
**``boundaryMargin``** → 子ウィジェットの周りマージンを置く。指定しておくと子ウィジェットの範囲外をスクロールを制限をかけれたりする(無限スクロールも現実可能に)

https://zenn.dev/szktty/articles/infinity-scroll-interactive-viewer

---

**``constrained``** → 画面外に出ている子ウィジェットにスクロールできるかできないかの制約を追加できる。(**引数：bool型** )
より詳しい内容(サンプルコードを動かして「true」「false」の挙動を見てみるのがおすすめ)

https://api.flutter.dev/flutter/widgets/InteractiveViewer/constrained.html

---

``maxScale``
``minScale`` →　拡大,縮小の最大値と最小値についての制約を付けられる

---

今回紹介で使ったものを使用例としてソースを残しておきます

**使用例**
```dart
    InteractiveViewer(
          boundaryMargin: EdgeInsets.all(double.infinity), // 無限スクロール可能に
          constrained: false,
          minScale: 0.5,
          maxScale: 2.0,
          child: container ( // 好きな形を定義
    )
)


```

今回の例ではそのまま`` InteractiveViewer``を使用していますが、普段は``body``
ウィジェットに入れたりするのが普通です。
また

---

## Stackについて
どこかにウィジェットを固定しておいておきたいときやウィジェット同士を重ねて遊びたいときにStackウィジェットを使用することによって実現できるらしいです

### 基礎①
最初にコンテナをチルドレンとして持たせてそれらを重ねて表示するようなプログラムを書いてみます。

**画面例①**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/40aa6376-3088-8a90-432d-03566ae008f4.png" width ="200"/>

このような形で ~~(配色が気持ち悪い)~~ ３つのコンテナを重ねて簡単に表示することができます
<details><summary><strong>コード例</strong></summary>

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

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Stack(
        children: [
          Container(
            color: Colors.black,
            width: 300,
            height: 300,
          ),
          Container(
            color: Colors.red,
            width: 200,
            height: 200,
          ),
          Container(
            color: Colors.white,
            width: 100,
            height: 100,
          )
        ], // children
      ),
    );
  }
} 

```

</details>

## 基礎②(Alignウィジェット)
AlignmentクラスでStackを中心として子ウィジェットの位置取りを制御できる。

ちなみAlignは画面にとって**相対的な位置**を指定するようになっている

前回の例も含め``Stack``は``body``内にある。すなわちbody内の右端に生成されるということ

**イメージ**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/865c405f-245e-f99b-ed63-60214783c96b.png
" width = "200"/>

今回は黒コンテナと赤のコンテナを子ウィジェットとして
赤コンテナを右下に表示するようなものを作成してみました。
黒はディフォルトです

**例①右下に赤を配置**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/23dcbf7a-3a6e-4deb-26a9-f0922703f3ff.png" width="400"/>

<details><summary><strong>コード例</strong></summary>

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

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Stack(children: [
        Container(
          color: Colors.black,
          width: 300,
          height: 300,
        ),
        Align(
          alignment: Alignment.bottomRight,
          child: Container(
            color: Colors.red,
            width: 200,
            height: 200,
          ), // Container
        ), // Align
      ]), // Stack
    );// Scaffold
  }
}

```

</details>

## 基礎③(Positionedウィジェット)
ここではPositionedウィジェットを利用して描画の優先(前に表示とか後ろに表示)などを定義してみたいと思います。

また``Positioned``は画面にとって **``絶対的な位置``** を指定するようになっている。

**優先度について**
<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/c367db3b-a068-a958-6506-1e757e679dd5.png
" width ="200"/>

先に記述されたものが下に行きます。
また``Positioned``には子ウィジェットを持つことができ、そこに制約を与えることができます(位置などの)

以上のような例で実際に作ってみましょう。

**今回出力する画面例**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/2785f7fe-724a-8a65-df87-8e57b375977a.png
" width = "200" />

<details><summary><strong>コード例</strong></summary>

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

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Stack(children: [
        Positioned(
          top: 50,
          left: 20,
          height: 250,
          width: 250,
          child: Container(
            width: 150,
            height: 150,
            color: Colors.red,
            child: const Text("①"),
          ),
        ),
        Positioned(
          top: 130,
          left: 50,
          height: 250,
          width: 250,
          child: Container(
            width: 150,
            height: 150,
            color: Colors.green,
            child: const Text("②"),
          ),
        ),
        Positioned(
          top: 210,
          left: 80,
          height: 250,
          width: 250,
          child: Container(
            width: 150,
            height: 150,
            color: Colors.purple,
            child: const Text("③"),
          ),
        ),
      ]),
    );
  }
}

```

</details>

このように先に書いた ~~(配色がきれいな)~~ コンテナ達が並んでますよね

## 応用例？
スクロールや画面をサイズを変更しても固定で同じ場所に表示し続けるやり方を紹介します。

以下のイメージ図のようにどれだけ画面を拡大などしてもコンテナを画面の固定の場所に表示し続ける方法をご紹介します。

**イメージ図**
| 通常 | 画面を大きくしたとき |
|--------|--------|
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/555bd51e-a63f-1665-8b11-fff4d33c8883.png" width="300"/> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/c6ec1bae-67f7-b3d5-4c79-8a681337ce11.png" width="300"/> |

ここで勘が良い方はお気づきかもしれないですが、
実はめちゃくちゃ簡単で **``画面に取っての相対位置を指定``** で済むのです
つまり
``Align``を利用すればいいだけです。


**実際の実行画面**
| 通常 | 画面を大きくしたとき |
|--------|--------|
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/a2bafc86-1937-cd7e-6321-99a73aba1670.png" width="300"/> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/849e9d79-98e2-4688-791d-8faf17e289cc.png" width="300"/> |

このように相対的な位置なので画面に対して右端を指定できていることがわかります。
<details><summary><strong>コード例</strong></summary>

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

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Stack(children: [
        Align(
          alignment: Alignment(1, 0),
          child: Container(
            color: Colors.purple,
            width: 100,
            height: 100,
            child: const Center(
              child: Text("コンテナ"),
            ),
          ),
        ),
      ]),
    );
  }
}

```

</details>



## GestureDetectorウィジェット
ボタンを使用せずに好きなコンテナなどをボタンのように扱うためのウィジェットです。

**今回作成したい機能のイメージ図**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/707435a0-c4ff-bd48-1003-c8725f960957.png" width="200"/>

という感じで``Stack``の子ウィジェットに``GestureDetector``を入れてその子ウィジェットのコンテナをボタン化したいというものです。



| ボタン押下前 | ボタン押下後 |
|--------|--------|
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/5758dd4f-7ae2-98c2-197d-db36511a9554.png" width="300"/> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/3b35aeda-958b-09a2-d683-8d1f10f44488.png" width="300"/> |

※ 前紹介したものと同じようにコンテナは相対的に真ん中に表示されるソースコードです(画像の切り取り方の問題)

<details><summary><strong>コード例</strong></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 Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  // 状態を保持する変数
  String tapText = "タップして";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Stack(children: [
        GestureDetector(
          onTap: () {
            setState(() {
              // テキストを更新
              tapText = "タップ済み";
            });
          },
          child: Align(
            alignment: const Alignment(1, 0),
            child: Container(
              color: Colors.purple,
              width: 100,
              height: 100,
              child: Center(
                child: Text(
                  tapText,
                  style: const TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
        ),
      ]),
    );
  }
}

```
</details>

解説は深掘りしませんが、ほぼボタンと同じでコンテナが押された際にメンバ変数をsetStateで更新している形になっています。

## Visibilityウィジェットについて
ボタンをや指定のコンテナをタップしたらそのコンテナやボタンを非表示にできるウィジェットを紹介します。

今回作成するもののイメージ図


| ボタン押下前 | ボタン押下後 |
|--------|--------|
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/2b7e6f99-22d2-24df-e2c4-3e77c4d2b62d.png" width="300"/> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/246fa16a-543c-e856-f49a-a00d6b1f3583.png" width="300"/> |

実際の実行例
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/2211d3ec-a56c-c57b-07e9-88bf67b72eb4.gif" width="300"/>


コード例の前に解説。
今回はこれまで``Stack``の子ウィジェットとして色々機能を載せてきたコンテナを利用します。これも実はすごく簡単で、``Visibility``の子ウィジェットとしてStackを子ウィジェットにすればいいだけです。
もしかしたらもっといい方法もあるのかも？

<details><summary><strong>コード例</strong></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 Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  // 状態を保持する変数
  String tapText = "タップして";
  bool _isButtonVisible = true; // ボタンの状態
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Visibility(
        visible: _isButtonVisible,
        child: Stack(children: [
          GestureDetector(
            onTap: () {
              setState(() {
                // テキストを更新
                tapText = "タップ済み";
                _isButtonVisible = false;
              });
            },
            child: Align(
              alignment: const Alignment(1, 0),
              child: Container(
                color: Colors.purple,
                width: 100,
                height: 100,
                child: Center(
                  child: Text(
                    tapText,
                    style: const TextStyle(color: Colors.white),
                  ),
                ),
              ),
            ),
          ),
        ]),
      ),
    );
  }
}

```

</details>


## 応用例①(StackとGestureDetectorの組み合わせ)

今回上の基礎2つを組み合わせて ~~(3つ必要だと思ったけど考えてみれば2つでいいやんってなった)~~ コンテナ①を表示して,コンテナ②をタップしたらコンテナ②を表示をするような感じの表示非表示の切り替えをしてみたいと思います。

今回作成する画面のイメージ図


| コンテナ①押下前 | ボタン押下後 |
|--------|--------|
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/e892e88f-8799-faea-81b4-49f848f13984.png" width="300"/> | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/acab1640-be0b-7ed2-d9a6-eaf92aeb11cd.png" width="300"/> |

みたいな感じの設計です。

今回の考え方として
**コンテナ①のフラグを管理する**ことが一番大事です。

フローチャートにするほどでもないですが簡単にフローチャートにしてみました。

:::note warn
**今回わかりやすさ優先でフローチャートにしたため実際の描画タイミングやif文,更新のタイミングが異なります。公式リファレンスや描画の記事などをご参照ください。**
:::
~~すごい雑ですが勘弁してください~~
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/44e91370-2cb1-cd89-4375-7f39d69ddefd.png"width ="400"/>

では実際の動作例を見てみましょう
**動作例**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/e2f1d3e5-0c48-e7fe-1955-c8f55a97d660.gif" width = "300"/>

<details><summary><strong>コード例</strong></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 Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  // 状態を保持する変数
  String tapText1 = "タップして";
  String tapText2 = "コンテナ②";
  bool isContainer1Visible = true; // コンテナ①の表示状態

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Stack(
        children: [
          // もしコンテナ1のフラグが立っていたらコンテナ①を表示処理
          if (isContainer1Visible)
            GestureDetector(
              onTap: () {
                // コンテナがタップされたら
                setState(() {
                  // コンテナ②を表示する(フラグを下げるとコンテナ②が表示される)
                  isContainer1Visible = false;
                });
              },
              child: Align(
                alignment: const Alignment(1, 0),
                child: Container(
                  color: Colors.purple,
                  width: 100,
                  height: 100,
                  child: Center(
                    child: Text(
                      tapText1,
                      style: const TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              ),
            ),

          // コンテナ①のフラグが立っていなかったら(コンテナ②の表示処理)
          if (!isContainer1Visible)
            GestureDetector(
              onTap: () {
                setState(() {
                  // コンテナ①を表示する
                  isContainer1Visible = true;
                });
              },
              child: Align(
                alignment: const Alignment(1, 0),
                child: Container(
                  color: Colors.green,
                  width: 100,
                  height: 300,
                  child: Center(
                    child: Text(
                      tapText2,
                      style: const TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }
}

```

</details>

## 応用②(InteractiveViewerとの組み合わせ)
ここでは``Stack``の下にひょうじされるコンテナを無限スクロール可能として応用①と組み合わせをしてみようと思います。

イメージ図
<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/8f127856-ae65-ad5b-0487-3e5e417dd7e8.png" width="300" />

このようなものを作ります。

基本的に``Stack``の考え方で
『 **先に書いたものは下に来る** 』 性質を利用して棲み分けを定義していきます。

これにより先にスクロールできる領域のコンテナを``InteractiveViewer``で定義して
後で応用①で利用したコードを定義します。

**実際の実行例**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628177/f88b9435-1ab8-6f7f-cb10-12661c2629a0.gif"width = "300" />

<details><summary><strong>コード例</strong></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 Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  bool isContainer1Visible = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Stack(
        children: [
          // インタラクティブエリア
          InteractiveViewer(
            boundaryMargin: const EdgeInsets.all(500), // 十分に余裕を持たせる
            minScale: 0.5,
            maxScale: 2.0,
            child: Stack(
              children: [
                // 背景
                Container(
                  color: Colors.grey[600],
                  width: 2000,
                  height: 2000,
                ),
              ],
            ),
          ),

          // コンテナ①
          if (isContainer1Visible)
            GestureDetector(
              onTap: () {
                setState(() {
                  isContainer1Visible = false;
                });
              },
              child: Align(
                alignment: const Alignment(1, 0),
                child: Container(
                  color: Colors.purple,
                  width: 100,
                  height: 100,
                  child: const Center(
                    child: Text(
                      "タップして",
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              ),
            ),

          // コンテナ②
          if (!isContainer1Visible)
            GestureDetector(
              onTap: () {
                setState(() {
                  isContainer1Visible = true;
                });
              },
              child: Align(
                alignment: const Alignment(1, 0),
                child: Container(
                  color: Colors.green,
                  width: 100,
                  height: 300,
                  child: const Center(
                    child: Text(
                      "コンテナ②",
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }
}

```
</details>


# 参考記事等

https://www.udemy.com/course/nzigen-flutter_beginner/?couponCode=NEWYEARCAREERJP

https://flutter.salon/widget/stack/

https://zenn.dev/faucon/articles/03e0bb753710b1


https://zenn.dev/yuta_ktd/articles/3b662ab21caf46

https://flutter.salon/widget/gesturedetector/

https://zenn.dev/yuta_ktd/articles/3b662ab21caf46