Flutter:在滚动时将简单对话框转换为全屏对话框
Flutter: Convert Simple Dialog To Full Screen Dialog On Scroll
我脑海中有一个 UI 布局,与 Google 地图应用中的新 menu/account 选择器基本相同。这是一个模态对话框,按下配置文件按钮时会弹出,并且可以滚动。滚动时,对话框会变成全屏对话框,反之亦然。
我的目标是使用 Material 设计兼容的方式来做到这一点,它目前只需要在 Android.
上工作
会做一些小改动,但我的问题是:这在 Flutter 中可能吗?谢谢。
嗯,在对话框上实现这种动画可能有点复杂。
您可以为此使用 animations 插件。
更多详情,您可以观看 this 视频。
您可能无法通过对话框创建动画,因此在这种情况下您可以使用 stack 并向用户显示自定义对话框。
作者在这里,
我创建了这个菜单,代码片段如下:
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import - ANOTHER PACKAGE -
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:theme_provider/theme_provider.dart';
import '../../services/authManager.dart';
import '../../services/models.dart';
import '../home.dart';
class MainMenu extends StatefulWidget {
const MainMenu({
Key key,
}) : super(key: key);
@override
_MainMenuState createState() => _MainMenuState();
}
class _MainMenuState extends State<MainMenu>
with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final mainProps = Provider.of<MainProps>(context);
final authVals = Provider.of<AuthVals>(context);
final userData = Provider.of<CustomUser>(context);
final userDataPrivate = Provider.of<CustomUserPrivate>(context);
final userDataReadOnly = Provider.of<CustomUserReadOnly>(context);
return IgnorePointer(
ignoring: !mainProps.menuOpen,
child: AnimatedOpacity(
opacity: mainProps.menuOpen ? 1 : 0,
duration: Duration(milliseconds: 150),
child: Container(
color: Colors.black.withOpacity(0.75),
child: Stack(
children: [
SafeArea(
child: Container(
width: MediaQuery.of(context).size.width,
margin: EdgeInsets.only(
top: mainProps.menuPadTop + 10,
left: mainProps.menuPadLeft,
right: mainProps.menuPadRight,
),
child: Container(
width: 100.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(mainProps.menuCorners)),
color: Theme.of(context).backgroundColor,
),
padding: EdgeInsets.only(
top: 10,
left: 10,
right: 10,
),
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification) {
if (((mainProps.menuScrollCtrl.position.pixels > 100
? 100
: mainProps
.menuScrollCtrl.position.pixels) -
100)
.abs() >=
25) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(0,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
} else if (mainProps.menuScrollCtrl.position.pixels
.abs() >
75 &&
mainProps.menuScrollCtrl.position.pixels.abs() <
100) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(100,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
}
}
return true;
},
child: Container(
height: 155,
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
(userData.public != 'Local Account'
? CircleAvatar(
radius: 20,
backgroundImage: NetworkImage(
authVals.authUser.photoURL,
),
)
: CircleAvatar(
radius: 20,
child: SvgPicture.network(
userData.photoURL,
color: Theme.of(context)
.primaryColor ==
Color(0xffff9800)
? Colors.black
: Colors.white),
backgroundColor:
Theme.of(context).backgroundColor,
)),
Column(
children: [
Text(
userData.publicExt,
style: TextStyle(
fontWeight: FontWeight.bold),
),
Text(userDataPrivate?.realName ??
'Please Wait...'),
Text(authVals.authUser.email == ''
? 'Anonymous'
: authVals.authUser.email),
Text(userDataReadOnly != null
? userDataReadOnly.joined
.toDate()
.toLocal()
.toString()
: 'Please Wait...'),
],
),
],
),
Spacer(),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
Visibility(
visible: !mainProps.signingOut,
child: OutlineButton(
onPressed: null,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.account_circle),
SizedBox(width: 15),
Text('View Profile'),
],
),
),
),
OutlineButton(
onPressed: () async {
if (!mainProps.signingOut) {
mainProps.signingOut = true;
} else {
await AuthService().signOut();
Navigator.of(context).popAndPushNamed(
-SCREEN-);
}
},
child: AnimatedContainer(
duration: Duration(milliseconds: 250),
constraints: mainProps.signingOut
? BoxConstraints(
maxWidth: MediaQuery.of(context)
.size
.width -
82)
: BoxConstraints(maxWidth: 93),
child: Row(
mainAxisSize: !mainProps.signingOut
? MainAxisSize.min
: MainAxisSize.max,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(Icons.logout,
color: mainProps.signingOut
? Colors.red
: null),
SizedBox(width: 15),
LimitedBox(
child: Text(
'Sign Out',
style: TextStyle(
color: mainProps.signingOut
? Colors.red
: null),
),
),
],
),
),
),
],
),
Spacer(),
],
),
),
),
),
),
),
),
SafeArea(
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
margin: EdgeInsets.only(
top: mainProps.menuPadTop +
(mainProps.menuScrollCtrl.hasClients
? (((mainProps.compassExpanded ? 195 : 195) / 100) *
(100 -
(mainProps.menuScrollCtrl.position.pixels >
100
? 100
: mainProps
.menuScrollCtrl.position.pixels)))
: -mainProps.menuPadTop),
left: mainProps.menuPadLeft,
right: mainProps.menuPadRight,
),
child: Container(
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(mainProps.menuCorners),
topRight: Radius.circular(mainProps.menuCorners),
),
color: Theme.of(context).backgroundColor,
),
padding: EdgeInsets.only(
top: 10,
left: 10,
right: 10,
),
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification) {
if (((mainProps.menuScrollCtrl.position.pixels > 100
? 100
: mainProps
.menuScrollCtrl.position.pixels) -
100)
.abs() >=
25) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(0,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
} else if (mainProps.menuScrollCtrl.position.pixels
.abs() >
75 &&
mainProps.menuScrollCtrl.position.pixels.abs() <
100) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(100,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
}
}
},
child: SingleChildScrollView(
child: const Text(
'hello\n\n\n\na\n\n\n\no\n\n\n\na\n\n\n\no\n\n\n\na\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\n'),
controller: mainProps.menuScrollCtrl,
),
),
),
),
),
SafeArea(
child: Container(
width: MediaQuery.of(context).size.width,
height: mainProps.compassExpanded ? 60 : 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(mainProps.menuCorners * 4),
),
),
margin: EdgeInsets.only(
top: mainProps.topMenuPadTop,
left: mainProps.menuPadLeft,
right: mainProps.menuPadRight,
),
child: Material(
borderRadius: BorderRadius.all(
Radius.circular(mainProps.menuCorners * 4),
),
elevation: 4,
color: Theme.of(context).backgroundColor,
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: IconButton(
icon: Icon(Icons.close),
onPressed: () {
mainProps.menuOpen = false;
mainProps.signingOut = false;
mainProps.menuScrollCtrl.jumpTo(0);
},
),
),
Expanded(
child: Text(
-TEXT-,
style: GoogleFonts.ubuntu(
textStyle: TextStyle(fontSize: 17),
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Stack(
children: [
Opacity(
opacity: mainProps.menuScrollCtrl.hasClients
? ((mainProps.menuScrollCtrl.position
.pixels >
100
? 0
: 100 -
mainProps.menuScrollCtrl
.position.pixels) /
100)
: 1,
child: IgnorePointer(
ignoring: mainProps.menuScrollCtrl.hasClients
? (mainProps.menuScrollCtrl.position
.pixels >=
100
? -1
: 100 -
mainProps.menuScrollCtrl
.position.pixels) <
0
: false,
child: IconButton(
icon: Icon(Icons.palette),
onPressed: () => showDialog(
context: context,
builder: (_) => ThemeConsumer(
child: ThemeDialog(
title: Row(
children: [
Icon(Icons.palette),
SizedBox(width: 15),
Text('Choose Theme'),
],
),
hasDescription: false,
),
),
),
),
),
),
Opacity(
opacity: 1.0 -
(mainProps.menuScrollCtrl.hasClients
? ((mainProps.menuScrollCtrl.position
.pixels >
100
? 0
: 100 -
mainProps.menuScrollCtrl
.position.pixels) /
100)
: 1),
child: IgnorePointer(
ignoring:
!(mainProps.menuScrollCtrl.hasClients
? (mainProps.menuScrollCtrl.position
.pixels >=
100
? -1
: 100 -
mainProps.menuScrollCtrl
.position.pixels) <
0
: false),
child: IconButton(
icon: Icon(Icons.keyboard_arrow_down),
onPressed: () =>
mainProps.menuScrollCtrl.animateTo(
0,
duration: Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
),
),
),
],
),
),
],
),
),
),
),
],
),
),
),
);
}
}
mainProps 只是我的状态管理解决方案,使用 Provider。使用 setState 执行此操作将是一场噩梦,并且可能会大大增加代码量。我认为其余代码是不言自明的。它有漂亮的动画和一些很酷的功能。
你可以看到它在这里工作:https://photos.app.goo.gl/aH6otb6CkbYbwpsr7
我正在考虑用与上述代码类似的代码创建一个包,并在 pub.dev 上分享。如果您认为这对您有帮助,请在此答案的评论中告诉我。
我脑海中有一个 UI 布局,与 Google 地图应用中的新 menu/account 选择器基本相同。这是一个模态对话框,按下配置文件按钮时会弹出,并且可以滚动。滚动时,对话框会变成全屏对话框,反之亦然。
我的目标是使用 Material 设计兼容的方式来做到这一点,它目前只需要在 Android.
上工作会做一些小改动,但我的问题是:这在 Flutter 中可能吗?谢谢。
嗯,在对话框上实现这种动画可能有点复杂。 您可以为此使用 animations 插件。 更多详情,您可以观看 this 视频。
您可能无法通过对话框创建动画,因此在这种情况下您可以使用 stack 并向用户显示自定义对话框。
作者在这里, 我创建了这个菜单,代码片段如下:
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import - ANOTHER PACKAGE -
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:theme_provider/theme_provider.dart';
import '../../services/authManager.dart';
import '../../services/models.dart';
import '../home.dart';
class MainMenu extends StatefulWidget {
const MainMenu({
Key key,
}) : super(key: key);
@override
_MainMenuState createState() => _MainMenuState();
}
class _MainMenuState extends State<MainMenu>
with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
final mainProps = Provider.of<MainProps>(context);
final authVals = Provider.of<AuthVals>(context);
final userData = Provider.of<CustomUser>(context);
final userDataPrivate = Provider.of<CustomUserPrivate>(context);
final userDataReadOnly = Provider.of<CustomUserReadOnly>(context);
return IgnorePointer(
ignoring: !mainProps.menuOpen,
child: AnimatedOpacity(
opacity: mainProps.menuOpen ? 1 : 0,
duration: Duration(milliseconds: 150),
child: Container(
color: Colors.black.withOpacity(0.75),
child: Stack(
children: [
SafeArea(
child: Container(
width: MediaQuery.of(context).size.width,
margin: EdgeInsets.only(
top: mainProps.menuPadTop + 10,
left: mainProps.menuPadLeft,
right: mainProps.menuPadRight,
),
child: Container(
width: 100.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(mainProps.menuCorners)),
color: Theme.of(context).backgroundColor,
),
padding: EdgeInsets.only(
top: 10,
left: 10,
right: 10,
),
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification) {
if (((mainProps.menuScrollCtrl.position.pixels > 100
? 100
: mainProps
.menuScrollCtrl.position.pixels) -
100)
.abs() >=
25) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(0,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
} else if (mainProps.menuScrollCtrl.position.pixels
.abs() >
75 &&
mainProps.menuScrollCtrl.position.pixels.abs() <
100) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(100,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
}
}
return true;
},
child: Container(
height: 155,
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
(userData.public != 'Local Account'
? CircleAvatar(
radius: 20,
backgroundImage: NetworkImage(
authVals.authUser.photoURL,
),
)
: CircleAvatar(
radius: 20,
child: SvgPicture.network(
userData.photoURL,
color: Theme.of(context)
.primaryColor ==
Color(0xffff9800)
? Colors.black
: Colors.white),
backgroundColor:
Theme.of(context).backgroundColor,
)),
Column(
children: [
Text(
userData.publicExt,
style: TextStyle(
fontWeight: FontWeight.bold),
),
Text(userDataPrivate?.realName ??
'Please Wait...'),
Text(authVals.authUser.email == ''
? 'Anonymous'
: authVals.authUser.email),
Text(userDataReadOnly != null
? userDataReadOnly.joined
.toDate()
.toLocal()
.toString()
: 'Please Wait...'),
],
),
],
),
Spacer(),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
Visibility(
visible: !mainProps.signingOut,
child: OutlineButton(
onPressed: null,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.account_circle),
SizedBox(width: 15),
Text('View Profile'),
],
),
),
),
OutlineButton(
onPressed: () async {
if (!mainProps.signingOut) {
mainProps.signingOut = true;
} else {
await AuthService().signOut();
Navigator.of(context).popAndPushNamed(
-SCREEN-);
}
},
child: AnimatedContainer(
duration: Duration(milliseconds: 250),
constraints: mainProps.signingOut
? BoxConstraints(
maxWidth: MediaQuery.of(context)
.size
.width -
82)
: BoxConstraints(maxWidth: 93),
child: Row(
mainAxisSize: !mainProps.signingOut
? MainAxisSize.min
: MainAxisSize.max,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(Icons.logout,
color: mainProps.signingOut
? Colors.red
: null),
SizedBox(width: 15),
LimitedBox(
child: Text(
'Sign Out',
style: TextStyle(
color: mainProps.signingOut
? Colors.red
: null),
),
),
],
),
),
),
],
),
Spacer(),
],
),
),
),
),
),
),
),
SafeArea(
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
margin: EdgeInsets.only(
top: mainProps.menuPadTop +
(mainProps.menuScrollCtrl.hasClients
? (((mainProps.compassExpanded ? 195 : 195) / 100) *
(100 -
(mainProps.menuScrollCtrl.position.pixels >
100
? 100
: mainProps
.menuScrollCtrl.position.pixels)))
: -mainProps.menuPadTop),
left: mainProps.menuPadLeft,
right: mainProps.menuPadRight,
),
child: Container(
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(mainProps.menuCorners),
topRight: Radius.circular(mainProps.menuCorners),
),
color: Theme.of(context).backgroundColor,
),
padding: EdgeInsets.only(
top: 10,
left: 10,
right: 10,
),
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification) {
if (((mainProps.menuScrollCtrl.position.pixels > 100
? 100
: mainProps
.menuScrollCtrl.position.pixels) -
100)
.abs() >=
25) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(0,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
} else if (mainProps.menuScrollCtrl.position.pixels
.abs() >
75 &&
mainProps.menuScrollCtrl.position.pixels.abs() <
100) {
WidgetsBinding.instance.addPostFrameCallback((_) {
mainProps.menuScrollCtrl.animateTo(100,
duration: Duration(milliseconds: 150),
curve: Curves.easeInOut);
});
}
}
},
child: SingleChildScrollView(
child: const Text(
'hello\n\n\n\na\n\n\n\no\n\n\n\na\n\n\n\no\n\n\n\na\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\no\n\n\n\n'),
controller: mainProps.menuScrollCtrl,
),
),
),
),
),
SafeArea(
child: Container(
width: MediaQuery.of(context).size.width,
height: mainProps.compassExpanded ? 60 : 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(mainProps.menuCorners * 4),
),
),
margin: EdgeInsets.only(
top: mainProps.topMenuPadTop,
left: mainProps.menuPadLeft,
right: mainProps.menuPadRight,
),
child: Material(
borderRadius: BorderRadius.all(
Radius.circular(mainProps.menuCorners * 4),
),
elevation: 4,
color: Theme.of(context).backgroundColor,
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: IconButton(
icon: Icon(Icons.close),
onPressed: () {
mainProps.menuOpen = false;
mainProps.signingOut = false;
mainProps.menuScrollCtrl.jumpTo(0);
},
),
),
Expanded(
child: Text(
-TEXT-,
style: GoogleFonts.ubuntu(
textStyle: TextStyle(fontSize: 17),
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Stack(
children: [
Opacity(
opacity: mainProps.menuScrollCtrl.hasClients
? ((mainProps.menuScrollCtrl.position
.pixels >
100
? 0
: 100 -
mainProps.menuScrollCtrl
.position.pixels) /
100)
: 1,
child: IgnorePointer(
ignoring: mainProps.menuScrollCtrl.hasClients
? (mainProps.menuScrollCtrl.position
.pixels >=
100
? -1
: 100 -
mainProps.menuScrollCtrl
.position.pixels) <
0
: false,
child: IconButton(
icon: Icon(Icons.palette),
onPressed: () => showDialog(
context: context,
builder: (_) => ThemeConsumer(
child: ThemeDialog(
title: Row(
children: [
Icon(Icons.palette),
SizedBox(width: 15),
Text('Choose Theme'),
],
),
hasDescription: false,
),
),
),
),
),
),
Opacity(
opacity: 1.0 -
(mainProps.menuScrollCtrl.hasClients
? ((mainProps.menuScrollCtrl.position
.pixels >
100
? 0
: 100 -
mainProps.menuScrollCtrl
.position.pixels) /
100)
: 1),
child: IgnorePointer(
ignoring:
!(mainProps.menuScrollCtrl.hasClients
? (mainProps.menuScrollCtrl.position
.pixels >=
100
? -1
: 100 -
mainProps.menuScrollCtrl
.position.pixels) <
0
: false),
child: IconButton(
icon: Icon(Icons.keyboard_arrow_down),
onPressed: () =>
mainProps.menuScrollCtrl.animateTo(
0,
duration: Duration(milliseconds: 250),
curve: Curves.easeInOut,
),
),
),
),
],
),
),
],
),
),
),
),
],
),
),
),
);
}
}
mainProps 只是我的状态管理解决方案,使用 Provider。使用 setState 执行此操作将是一场噩梦,并且可能会大大增加代码量。我认为其余代码是不言自明的。它有漂亮的动画和一些很酷的功能。
你可以看到它在这里工作:https://photos.app.goo.gl/aH6otb6CkbYbwpsr7
我正在考虑用与上述代码类似的代码创建一个包,并在 pub.dev 上分享。如果您认为这对您有帮助,请在此答案的评论中告诉我。