如何忽略某个 GestureDetector 小部件的触摸并检测 Flutter 中的外部触摸?

How to ignore touches for a certain GestureDetector widget and detect outside touches in Flutter?

我创建了一个下拉小部件,但是当我触摸文本小部件或免费 space 等小部件时,它会下拉高度跳转到触摸位置。如何忽略这个触摸? 我使用了 IgnorePointer 小部件,但它也禁用了 Switch 小部件。 另外,如何检测外部触摸以关闭下拉小部件?

import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:movie_god/MyApp.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget{
  @override
  State<StatefulWidget> createState() => MyAppState();
}

class MyAppState extends State<MyApp>{
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter!'),
        ),
        body: Stack(
          children: <Widget>[
            Container(
              color: Colors.blueGrey[200],
              child: Center(
                child: Text('Widgets'),
              ),
            ),
            BottomFilter()
          ],
        ),
      ),
    );
  }

}

class BottomFilter extends StatefulWidget{
  @override
  State<StatefulWidget> createState() => BottomFilterState();
}

class BottomFilterState extends State<BottomFilter> with SingleTickerProviderStateMixin{
  double _minHeight = 20;
  double _height;
  double _maxHeight = 200;
  double _transparentHeight = 30;
  AnimationController _controller;
  Animation _animation;
  Map<String,dynamic> _switches = {
    'switch1' : false,
    'switch2' : false,
    'switch3' : false,
    'switch4' : false,
    'option' : null
  };
  List<String> _options = <String>[];

  @override
  void initState() {
    _controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500));
    _animation = Tween(begin: _minHeight+_transparentHeight, end: _maxHeight).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    Size _size = MediaQuery.of(context).size;

    return GestureDetector(
      onVerticalDragUpdate: (drag){
        setState(() {
          _controller.reset();
          double _postion = drag.globalPosition.dy-kToolbarHeight-_minHeight-_transparentHeight;
          print(_postion.toString());
          if(_postion<0){
            _height=_minHeight+_transparentHeight;
          } else if(_postion>_maxHeight){
            double _newHeight = _maxHeight+_transparentHeight+_minHeight + ((_size.height-_postion)/_size.height)*((_postion-_maxHeight));
            _height < _newHeight ? _height = _newHeight: null;
          } else{
            _height = _postion+_transparentHeight+_minHeight;
          }
          _animation = Tween(begin: _height, end: _maxHeight).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
        });
      },
      onVerticalDragEnd: (drag){
        if(_height>_maxHeight || _height>=_maxHeight/2){
          _animation = Tween(begin: _height, end: _maxHeight).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
          _controller.forward();
        }else if(_height<_maxHeight/2){
          _animation = Tween(begin: _height, end: _minHeight+_transparentHeight).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
          _controller.forward();
        }
      },
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context,Widget child){
          return Stack(
            children: <Widget>[
              Positioned(
                bottom: _size.height - (kToolbarHeight + 20 + _animation.value),
                child: child,
              )
            ],
          );
        },
        child: Container(
            height: 400,
            width: _size.width,
            color: Colors.transparent,
            child: Padding(
              padding: EdgeInsets.only(bottom: _transparentHeight),
              child: Container(
                height: 300,
                alignment: Alignment.bottomCenter,
                width: _size.width,
                decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.vertical(bottom: Radius.circular(20))
                ),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: <Widget>[
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: <Widget>[
                        Text('switch1',style: TextStyle(color: Colors.blueGrey[800]),),
                        Switch.adaptive(
                          inactiveThumbColor: Colors.blue,
                          value: _switches['switch1'],
                          onChanged: (value){
                            setState(() {
                              _switches['switch1'] = value;
                            });
                          },
                        ),
                        Text('switch2',style: TextStyle(color: Colors.blueGrey[800]),),
                        Switch.adaptive(
                          value: _switches['switch2'],
                          onChanged: (value){
                            setState(() {
                              _switches['switch2'] = value;
                            });
                          },
                        ),
                        Text('switch3',style: TextStyle(color: Colors.blueGrey[800]),),
                        Switch.adaptive(
                          value: _switches['switch3'],
                          onChanged: (value){
                            setState(() {
                              _switches['switch3'] = value;
                            });
                          },
                        ),
                      ],
                    ),
                    Row(
                      mainAxisSize: MainAxisSize.min,
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: <Widget>[
                        Theme(
                          data: Theme.of(context).copyWith(
                              canvasColor: Colors.white,
                              brightness: Brightness.light
                          ),
                          child: Row(
                            children: <Widget>[
                              DropdownButton<String>(
                                value: _switches['option'],
                                hint: Text('sample1',style: TextStyle(color: Colors.blueGrey[800]),),
                                style: TextStyle(
                                    color: Colors.blueGrey[800]
                                ),
                                onChanged: (String value){
                                  if(value != null){
                                    setState(() {
                                      _switches['option'] = value;
                                      print(_switches['option']);
                                    });
                                  }
                                },
                                items: <String>['option1','option2','option3','option4','option5','option6'].map<DropdownMenuItem<String>>((value){
                                  return DropdownMenuItem<String>(
                                      value: value,
                                      child : Align(child: Text(value),alignment: Alignment(1, 0),)
                                  );
                                }).toList(),
                              ),
                            ],
                          ),
                        ),
                        Theme(
                          data: Theme.of(context).copyWith(
                              canvasColor: Colors.white,
                              brightness: Brightness.light
                          ),
                          child: Row(
                            children: <Widget>[
                              DropdownButton<String>(
                                hint: Text('sample2',style: TextStyle(color: Colors.blueGrey[800]),),
                                style: TextStyle(
                                    color: Colors.blueGrey[800]
                                ),
                                onChanged: (String value){
                                  if(value != null){
                                    _options.indexOf(value)<0?
                                    setState(() {
                                      _options.add(value);
                                    }) : null;
                                  }
                                },
                                items: <String>['option1','option2','option3','option4','option5','option6'].map<DropdownMenuItem<String>>((value){
                                  return DropdownMenuItem<String>(
                                      value: value,
                                      child : Align(child: Text(value),alignment: Alignment(1, 0),)
                                  );
                                }).toList(),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                    SizedBox(
                      height: 50,
                      child: ListView(
                        shrinkWrap: false,
                        scrollDirection: Axis.horizontal,
                        children: _genresGenerator(),
                      ),
                    ),
                    Align(
                      alignment: Alignment.bottomCenter,
                      child: Column(
                        children: <Widget>[
                          Padding(
                            padding: EdgeInsets.symmetric(horizontal: 40),
                            child: Divider(
                              color: Colors.blueGrey[500],
                              height: 10,
                              indent: 5,
                            ),
                          ),
                          Icon(FontAwesomeIcons.angleDoubleDown,size: 15,color: Colors.blueGrey[500],)
                        ],
                      ),
                    )
                  ],
                ),
              ),
            )
        ),
      ),
    );

  }

  List<Widget> _genresGenerator() {
    List<Widget> _optionsWidgets = _options.map<Widget>((String name) {
      return InputChip(
          key: ValueKey<String>(name),
          label: Text(name),
          onDeleted: () {
            setState(() {
              _removeTool(name);
            });
          });
    }).toList();

    return _optionsWidgets;
  }

  void _removeTool(String name) {
    _options.remove(name);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

}

要从中折叠抽屉,您可以从父 Widget 向子 Widget 发送命令。在 BottomFilter 中配置一个 Stream 以在抽屉应该收回时监听命令。

class BottomFilter extends StatefulWidget {
  BottomFilter({Key? key, required Stream<bool> stream})
      : stream = stream,
        super(key: key);
  final Stream<bool> stream;

  @override
  State<StatefulWidget> createState() => BottomFilterState();
}

BottomFilterState 中,配置一个执行折叠动画的函数。

void close() {
  setState(() {
    _animation = Tween(begin: _height, end: 50)
        .animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
  });
}

然后在 initState() 中设置 Stream 侦听器

@override
void initState() {
  ...
  widget.stream.listen((bool isExpand) {
    /// Collapse widget if [isExpand] is false 
    if(!isExpand) close();
  });
  super.initState();
}

MyAppState 中,初始化您的 StreamController。

class MyAppState extends State<MyApp> {
  var _expandStreamController = StreamController<bool>();

  @override
  void dispose() {
    // Close the Stream when done
    _expandStreamController.close();
    super.dispose();
  }
  ...
}

在屏幕上添加 GestureDetector 以检测会提示折叠小部件的触摸。

Stack(
  children: <Widget>[
    GestureDetector(
      onTap: () {
        /// Collapse the widget
        _expandStreamController.add(false);
      },
      child: Container(
        color: Colors.blueGrey[200],
        child: Center(
          child: Text('Widgets'),
        ),
      ),
    ),
    BottomFilter(
      stream: _expandStreamController.stream,
    ),
  ],
),

完整代码,根据提供的重现更新。

import 'dart:async';

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  var _expandStreamController = StreamController<bool>();

  @override
  void dispose() {
    _expandStreamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter '),
        ),
        body: Stack(
          children: <Widget>[
            GestureDetector(
              onTap: () {
                debugPrint('Close Drawer');
                _expandStreamController.add(false);
              },
              child: Container(
                color: Colors.blueGrey[200],
                child: Center(
                  child: Text('Widgets'),
                ),
              ),
            ),
            BottomFilter(
              stream: _expandStreamController.stream,
            )
          ],
        ),
      ),
    );
  }
}

class BottomFilter extends StatefulWidget {
  BottomFilter({Key? key, required Stream<bool> stream})
      : stream = stream,
        super(key: key);
  final Stream<bool> stream;

  @override
  State<StatefulWidget> createState() => BottomFilterState();
}

class BottomFilterState extends State<BottomFilter>
    with SingleTickerProviderStateMixin {
  double _minHeight = 20;
  late double _height;
  double _maxHeight = 200;
  double _transparentHeight = 30;
  late AnimationController _controller;
  late Animation _animation;
  Map<String, dynamic> _switches = {
    'switch1': false,
    'switch2': false,
    'switch3': false,
    'switch4': false,
    'option': null
  };
  List<String> _options = <String>[];

  void close() {
    setState(() {
      _animation = Tween(begin: _height, end: 50)
          .animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
    });
  }

  @override
  void initState() {
    _controller =
        AnimationController(vsync: this, duration: Duration(milliseconds: 500));
    _animation = Tween(begin: _minHeight + _transparentHeight, end: _maxHeight)
        .animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));

    widget.stream.listen((bool isExpand) {
      if(!isExpand) close();
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    Size _size = MediaQuery.of(context).size;

    return GestureDetector(
      onTap: () {
        debugPrint(
            'onTap\nheight: $_height\nminHeight: $_minHeight\nmaxHeight: $_maxHeight');
        close();
      },
      onVerticalDragUpdate: (drag) {
        setState(() {
          _controller.reset();
          double _position = drag.globalPosition.dy -
              kToolbarHeight -
              _minHeight -
              _transparentHeight;
          print(_position.toString());
          if (_position < 0) {
            _height = _minHeight + _transparentHeight;
          } else if (_position > _maxHeight) {
            double _newHeight = _maxHeight +
                _transparentHeight +
                _minHeight +
                ((_size.height - _position) / _size.height) *
                    ((_position - _maxHeight));
            _height < _newHeight ? _height = _newHeight : null;
          } else {
            _height = _position + _transparentHeight + _minHeight;
          }
          _animation = Tween(begin: _height, end: _maxHeight).animate(
              CurvedAnimation(parent: _controller, curve: Curves.easeOut));
        });
      },
      onVerticalDragEnd: (drag) {
        if (_height > _maxHeight || _height >= _maxHeight / 2) {
          _animation = Tween(begin: _height, end: _maxHeight).animate(
              CurvedAnimation(parent: _controller, curve: Curves.easeOut));
          _controller.forward();
        } else if (_height < _maxHeight / 2) {
          _animation = Tween(
                  begin: _height, end: _minHeight + _transparentHeight)
              .animate(
                  CurvedAnimation(parent: _controller, curve: Curves.easeOut));
          _controller.forward();
        }
      },
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, Widget? child) {
          return Stack(
            children: <Widget>[
              Positioned(
                bottom: _size.height - (kToolbarHeight + 20 + _animation.value),
                child: child!,
              )
            ],
          );
        },
        child: Container(
            height: 400,
            width: _size.width,
            color: Colors.transparent,
            child: Padding(
              padding: EdgeInsets.only(bottom: _transparentHeight),
              child: Container(
                height: 300,
                alignment: Alignment.bottomCenter,
                width: _size.width,
                decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius:
                        BorderRadius.vertical(bottom: Radius.circular(20))),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: <Widget>[
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: <Widget>[
                        Text(
                          'switch1',
                          style: TextStyle(color: Colors.blueGrey[800]),
                        ),
                        Switch.adaptive(
                          inactiveThumbColor: Colors.blue,
                          value: _switches['switch1'],
                          onChanged: (value) {
                            setState(() {
                              _switches['switch1'] = value;
                            });
                          },
                        ),
                        Text(
                          'switch2',
                          style: TextStyle(color: Colors.blueGrey[800]),
                        ),
                        Switch.adaptive(
                          value: _switches['switch2'],
                          onChanged: (value) {
                            setState(() {
                              _switches['switch2'] = value;
                            });
                          },
                        ),
                        Text(
                          'switch3',
                          style: TextStyle(color: Colors.blueGrey[800]),
                        ),
                        Switch.adaptive(
                          value: _switches['switch3'],
                          onChanged: (value) {
                            setState(() {
                              _switches['switch3'] = value;
                            });
                          },
                        ),
                      ],
                    ),
                    Row(
                      mainAxisSize: MainAxisSize.min,
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: <Widget>[
                        Theme(
                          data: Theme.of(context).copyWith(
                              canvasColor: Colors.white,
                              brightness: Brightness.light),
                          child: Row(
                            children: <Widget>[
                              DropdownButton<String>(
                                value: _switches['option'],
                                hint: Text(
                                  'sample1',
                                  style: TextStyle(color: Colors.blueGrey[800]),
                                ),
                                style: TextStyle(color: Colors.blueGrey[800]),
                                onChanged: (String? value) {
                                  if (value == null) {
                                    setState(() {
                                      _switches['option'] = value;
                                      print(_switches['option']);
                                    });
                                  }
                                },
                                items: <String>[
                                  'option1',
                                  'option2',
                                  'option3',
                                  'option4',
                                  'option5',
                                  'option6'
                                ].map<DropdownMenuItem<String>>((value) {
                                  return DropdownMenuItem<String>(
                                      value: value,
                                      child: Align(
                                        child: Text(value),
                                        alignment: Alignment(1, 0),
                                      ));
                                }).toList(),
                              ),
                            ],
                          ),
                        ),
                        Theme(
                          data: Theme.of(context).copyWith(
                              canvasColor: Colors.white,
                              brightness: Brightness.light),
                          child: Row(
                            children: <Widget>[
                              DropdownButton<String>(
                                hint: Text(
                                  'sample2',
                                  style: TextStyle(color: Colors.blueGrey[800]),
                                ),
                                style: TextStyle(color: Colors.blueGrey[800]),
                                // onChanged: (String? value) {
                                //   if (value == null) {
                                //     _options.indexOf(value) < 0
                                //         ? setState(() {
                                //             _options.add(value);
                                //           })
                                //         : null;
                                //   }
                                // },
                                items: <String>[
                                  'option1',
                                  'option2',
                                  'option3',
                                  'option4',
                                  'option5',
                                  'option6'
                                ].map<DropdownMenuItem<String>>((value) {
                                  return DropdownMenuItem<String>(
                                      value: value,
                                      child: Align(
                                        child: Text(value),
                                        alignment: Alignment(1, 0),
                                      ));
                                }).toList(),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                    SizedBox(
                      height: 50,
                      child: ListView(
                        shrinkWrap: false,
                        scrollDirection: Axis.horizontal,
                        children: _genresGenerator(),
                      ),
                    ),
                    Align(
                      alignment: Alignment.bottomCenter,
                      child: Column(
                        children: <Widget>[
                          Padding(
                            padding: EdgeInsets.symmetric(horizontal: 40),
                            child: Divider(
                              color: Colors.blueGrey[500],
                              height: 10,
                              indent: 5,
                            ),
                          ),
                          Icon(
                            Icons.arrow_drop_down,
                            size: 15,
                            color: Colors.blueGrey[500],
                          )
                        ],
                      ),
                    )
                  ],
                ),
              ),
            )),
      ),
    );
  }

  List<Widget> _genresGenerator() {
    List<Widget> _optionsWidgets = _options.map<Widget>((String name) {
      return InputChip(
          key: ValueKey<String>(name),
          label: Text(name),
          onDeleted: () {
            setState(() {
              _removeTool(name);
            });
          });
    }).toList();

    return _optionsWidgets;
  }

  void _removeTool(String name) {
    _options.remove(name);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}