Flutter:在滚动时将简单对话框转换为全屏对话框
Posted
技术标签:
【中文标题】Flutter:在滚动时将简单对话框转换为全屏对话框【英文标题】:Flutter: Convert Simple Dialog To Full Screen Dialog On Scroll 【发布时间】:2021-05-16 10:39:34 【问题描述】:我的脑海中有一个 UI 布局,与 Google 地图应用中的新菜单/帐户选择器基本相同。它是一个模态对话框,在按下配置文件按钮时弹出并且可以滚动。滚动时,对话框动画成全屏对话框,反之亦然。
我的目标是使用与 Material Design 兼容的方式来做这件事,它目前只需要在 android 上工作。
会进行一些小的更改,但我的问题是:这在 Flutter 中可行吗?谢谢。
【问题讨论】:
【参考方案1】:嗯,在对话框上实现这种动画可能有点复杂。 您可以为此使用animations 插件。 更多详情可以看this视频。
您可能无法使用对话框创建动画,因此在这种情况下,您可以使用 stack 并向用户显示自定义对话框。
【讨论】:
谢谢,我会看看这个。【参考方案2】:作者在这里, 我已经创建了这个菜单,代码sn-p如下:
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 上共享它。如果您认为这对您有帮助,请在此答案的 cmets 中告诉我。
【讨论】:
以上是关于Flutter:在滚动时将简单对话框转换为全屏对话框的主要内容,如果未能解决你的问题,请参考以下文章
Android开发 - 设置DialogFragment全屏显示
即使设置了 CGRect,UIScrollView 中的 UIPageViewController 仍会显示为全屏 - 用户滚动后立即返回正确的帧值