如何忽略某个 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();
}
}
我创建了一个下拉小部件,但是当我触摸文本小部件或免费 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();
}
}