Flutter: Stackで下層ウィジェットからはみ出た部分をタップできない
結論:下層ウィジェットを、Align、Center、Positionedいずれかでラップする
2022/8/5 Flutter エラー・バグ日記
「Stack」を使い、ウィジェットの上に、「GestureDetector」でタップ処理を入れたウィジェットを重ねたところ、下層ウィジェットからはみ出た部分がタップできない(反応しない)事に気づいた。
発生した状況

「Stack」部分のコードは以下のとおり。
body: Stack(
// ↓はみ出た部分を表示させるには、これが必要
clipBehavior: Clip.none,
children: [
/// 下層ウィジェット
Container(
width: 100,
height: 100,
color: Colors.blueGrey,
),
/// 上層ウィジェット
Positioned(
top: 50,
left: 50,
child: GestureDetector(
onTap: () {
print("タップされた");
},
child: Container(
width: 100,
height: 100,
color: Colors.lightBlueAccent.withOpacity(0.5),
),
),
),
],
),
そもそも当初、はみ出た部分が表示自体されなかったので、調べたところ、「Stack」の「clipBehavior:」属性に、「Clip.none」を設定すれば表示できる(※)と分かった。
※この部分が分かるまで苦労した。。「Stack」では、上層ウィジェットが、下層ウィジェットの範囲をはみ出すと、その部分は表示しないのがデフォルトらしい。
タップできない問題については、調べたところ、多くの情報があった。
1つ目の記事では、解決方法が提示されているものの、既存の「Stack」の拡張クラスを自作するという方法で、かなり難解な印象。
さらに調べると、GitHubにもイシューがあったが、オープン中の別のイシューに引き継がれ、まだ解決していない模様(若干論点が変わってしまっている気もするが、、)。
下層を「Center」でラップしたら解消
自分なりに試行錯誤したところ、下層のウィジェットを「Center」でラップすると、「clipBehavior: Clip.none」を設定しなくても、はみ出た部分が表示されることに気づいた。
そして、はみ出た部分のタップも普通に機能していた。
「Center」クラスは、「Align」クラスの拡張クラスなので、今度は下層のウィジェットを「Align」でラップして、「alignment: Alignment(-1, -1)」(画面左上の位置)を設定したところ、やはりはみ出た部分のタップが有効になった。

タップできるようになった理由
「Align」クラスの公式サイト説明
を見ると、
widthFactor と heightFactor が NULL の場合、可能な限り大きくなる
と書かれていた。
同じ内容が、「Center」クラスの公式説明にも書かれていた。
「widthFactor」 と「heightFactor」は、子ウィジェットの幅を制限する係数らしい。
つまり、「widthFactor」 と「heightFactor」を設定しなければ、「Align」や「Center」のウィジェットは、画面全体に広がるため、その上にStackした(重ねた)ウィジェットは、下層に対して、はみ出た部分が無いことになり、全ての範囲がタップ可能になった、ということだと理解。
実際、「widthFactor」 と「heightFactor」を1以下の値で設定すると、下層ウィジェットの範囲外(はみ出た部分)は、タップできなくなった。
タップできる場合・できない場合(その他の方法)
他の方法も調べてみたところ、結果は、以下のとおりだった。
■下層からはみ出た部分をタップできる場合
下層ウィジェットを
- 「Align」(「Center」含む)でラップした場合
- 「Positioned」でラップした場合
※「Positioned」の公式説明を確認したが、「Align」のように画面全体にウィジェットの範囲が広がるのか否か、よく分からなかった、、、。
■下層からはみ出た部分をタップできない場合
下層ウィジェットを
- ラップしない場合
- 「Transform.translate」でラップした場合
下層を「Align」か「Positioned」でラップする
とりあえず、「Stack」しているウィジェットに「タップ処理」を入れたいときは、下層ウィジェットを「Align」か「Positioned」でラップする、ことを意識しようと思う。
個人的には、「Positioned」の方が使いやすいので、こっちを使いたいと思った。
※以下に、今回検証したコードをご参考に掲載します(コメントアウトの箇所を変更すると、違いを検証いただけます)。
// クラス名、メソッド名、プロパティ名(変数名)について、筆者が作成したもの(名前変更可のもの)
// の名前の末尾には、大文字のオー「O」をつけています
// ※ライブラリ(パッケージ)で予め決められているもの(名前の変更不可のもの)と、
// 自分で作成したもの(名前の変更可のもの)の区別をしやすくするため
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Test",
theme: ThemeData.light(),
home: SampleScreenO(),
);
}
}
class SampleScreenO extends StatefulWidget {
@override
_SampleScreenOState createState() => _SampleScreenOState();
}
class _SampleScreenOState extends State<SampleScreenO> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Stack(
clipBehavior: Clip.none,
children: [
/// 下層ウィジェット
// はみ出た部分のタップが無効になるケース(1)
// Container(
// width: 100,
// height: 100,
// color: Colors.blueGrey,
// ),
// はみ出た部分のタップが無効になるケース(2)
// Transform.translate(
// offset: Offset(0, 0),
// child: Container(
// width: 100,
// height: 100,
// color: Colors.blueGrey,
// ),
// ),
// はみ出た部分のタップが有効になるケース(1)
Positioned(
left: 0,
top: 0,
child: Container(
width: 100,
height: 100,
color: Colors.blueGrey,
),
),
// はみ出た部分のタップが有効になるケース(2)
// Align(
// alignment: Alignment(-1, -1),
// // ↓これを1以下で設定すると、はみ出た部分はタップできなくなる
// // widthFactor: 1,
// // heightFactor: 1,
// child: Container(
// width: 100,
// height: 100,
// color: Colors.blueGrey,
// ),
// ),
/// 上層ウィジェット
Positioned(
top: 50,
left: 50,
child: GestureDetector(
onTap: () {
print("タップされた");
},
child: Container(
width: 100,
height: 100,
color: Colors.lightBlueAccent.withOpacity(0.5),
),
),
),
],
),
);
}
}
\一般的なエラー対処法をまとめた記事はこちら/
リリースしたアプリ(全てFlutterで開発)
個人アプリ開発で役立ったもの
おすすめの学習教材
\超初心者向けでオススメな元Udemyの講座/
\キャンペーン時を狙えば安価で網羅的な内容が学べる(日本語訳あり)/
\Gitの基礎について無料で学べる/
おすすめの学習書籍
\実用的。image_pickerに関してかなり助けられた/
\Dartの基礎文法を素早くインプットできる/





