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の基礎文法を素早くインプットできる/