簡單介紹
最近使用了Flutter的展示對(duì)話框的功能,踩了一點(diǎn)坑,順便做下總結(jié),方便各位以后少踩坑,如果有說錯(cuò)的地方,還請大家指出來。
下面將介紹對(duì)話框的幾種場景和踩坑。
- 展示普通對(duì)話框
- 展示包含列表項(xiàng)的對(duì)話框
- 對(duì)話框界面需要?jiǎng)討B(tài)刷新
- 自定義對(duì)話框。
先理解一些東西
對(duì)話框本質(zhì)上是屬于一個(gè)路由的頁面route,由Navigator進(jìn)行管理,所以控制對(duì)話框的顯示和隱藏,也是調(diào)用Navigator.of(context)的push和pop方法。
在Flutter中,對(duì)話框會(huì)有兩種風(fēng)格,調(diào)用showDialog()方法展示的是material風(fēng)格的對(duì)話框,調(diào)用showCupertinoDialog()方法展示的是ios風(fēng)格的對(duì)話框。 而這兩個(gè)方法其實(shí)都會(huì)去調(diào)用showGeneralDialog()方法,可以從源碼中看到最后是利用Navigator.of(context, rootNavigator: true).push()一個(gè)頁面。
基本要傳的參數(shù):context上下文,builder用于創(chuàng)建顯示的widget,barrierDismissible可以控制點(diǎn)擊對(duì)話框以外的區(qū)域是否隱藏對(duì)話框。
- 你會(huì)注意到,showDialog()方法返回的是一個(gè)Future對(duì)象,可以通過這個(gè)future對(duì)象來獲取對(duì)話框所傳遞的數(shù)據(jù)。 比如我們想知道想知道用戶是點(diǎn)擊了對(duì)話框的確認(rèn)按鈕還是取消按鈕,那就在退出對(duì)話框的時(shí)候,利用Navigator.of(context).pop("一些數(shù)據(jù)");
Future<T> showCupertinoDialog<T>({
@required BuildContext context,
@required WidgetBuilder builder,
});
Future<T> showDialog<T>({
@required BuildContext context,
bool barrierDismissible = true,
WidgetBuilder builder,
})
Future<T> showGeneralDialog<T>({
@required BuildContext context,
@required RoutePageBuilder pageBuilder,
bool barrierDismissible,
String barrierLabel,
Color barrierColor,
Duration transitionDuration,
RouteTransitionsBuilder transitionBuilder,
}) {
assert(pageBuilder != null);
assert(!barrierDismissible || barrierLabel != null);
return Navigator.of(context, rootNavigator: true).push<T>(_DialogRoute<T>(
pageBuilder: pageBuilder,
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel,
barrierColor: barrierColor,
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
));
}
簡單的顯示對(duì)話框
Flutter中的Dialog主要是SimpleDialog和AlertDialog。
- SimpleDialog,一般可以利用多個(gè)SimpleDialogOption為用戶提供了幾個(gè)選項(xiàng)。
- AlertDialog,警告對(duì)話框。警告對(duì)話框有一個(gè)可選標(biāo)題title和一個(gè)可選列表的actions選項(xiàng)。
展示一個(gè)簡單的SimpleDialog,代碼如下:
void showMySimpleDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return new SimpleDialog(
title: new Text("SimpleDialog"),
children: <Widget>[
new SimpleDialogOption(
child: new Text("SimpleDialogOption One"),
onPressed: () {
Navigator.of(context).pop("SimpleDialogOption One");
},
),
new SimpleDialogOption(
child: new Text("SimpleDialogOption Two"),
onPressed: () {
Navigator.of(context).pop("SimpleDialogOption Two");
},
),
new SimpleDialogOption(
child: new Text("SimpleDialogOption Three"),
onPressed: () {
Navigator.of(context).pop("SimpleDialogOption Three");
},
),
],
);
});
}
展示一個(gè)簡單的Material風(fēng)格的AlertDialog,代碼如下:
void showMyMaterialDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return new AlertDialog(
title: new Text("title"),
content: new Text("內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容"),
actions: <Widget>[
new FlatButton(
onPressed: () {
Navigator.of(context).pop();
},
child: new Text("確認(rèn)"),
),
new FlatButton(
onPressed: () {
Navigator.of(context).pop();
},
child: new Text("取消"),
),
],
);
});
}
展示一個(gè)簡單的IOS風(fēng)格的AlertDialog,代碼如下:
void showMyCupertinoDialog(BuildContext context) {
showCupertinoDialog(
context: context,
builder: (context) {
return new CupertinoAlertDialog(
title: new Text("title"),
content: new Text("內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容內(nèi)容"),
actions: <Widget>[
new FlatButton(
onPressed: () {
Navigator.of(context).pop("點(diǎn)擊了確定");
},
child: new Text("確認(rèn)"),
),
new FlatButton(
onPressed: () {
Navigator.of(context).pop("點(diǎn)擊了取消");
},
child: new Text("取消"),
),
],
);
});
}
展示列表項(xiàng)對(duì)話框(踩坑了)
構(gòu)造對(duì)話框的時(shí)候,我們都需要傳一個(gè)content對(duì)象,來構(gòu)造對(duì)話框的主要內(nèi)容。一般情況下,如果content里面只是簡單的一些內(nèi)容,那問題不大,可以正常顯示。 但是有時(shí)候,我們需要展示一個(gè)列表對(duì)話框。這個(gè)時(shí)候,如果列表項(xiàng)比較多,就會(huì)出現(xiàn)一些問題。
- 使用Column+SingleChildScrollView來顯示列表對(duì)話框。
當(dāng)Column的列表項(xiàng)數(shù)據(jù)比較多的時(shí)候,屏幕已經(jīng)放不了,就會(huì)出現(xiàn)overflow錯(cuò)誤了,所以這個(gè)時(shí)候需要在外部嵌套一個(gè)SingleChildScrollView控件,使內(nèi)部child控件可以滾動(dòng), 不會(huì)出現(xiàn)overflow錯(cuò)誤。(哈哈,可以使用下面的代碼跑一跑,然后去掉SingleChildScrollView,對(duì)比運(yùn)行結(jié)果)
void showMyDialogWithColumn(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return new AlertDialog(
title: new Text("title"),
content: new SingleChildScrollView(
child: new Column(
children: <Widget>[
new SizedBox(
height: 100,
child: new Text("1"),
),
new SizedBox(
height: 100,
child: new Text("1"),
),
new SizedBox(
height: 100,
child: new Text("1"),
),
new SizedBox(
height: 100,
child: new Text("1"),
),
new SizedBox(
height: 100,
child: new Text("1"),
),
new SizedBox(
height: 100,
child: new Text("1"),
),
new SizedBox(
height: 100,
child: new Text("1"),
),
new SizedBox(
height: 100,
child: new Text("1"),
),
new SizedBox(
height: 100,
child: new Text("1"),
),
new SizedBox(
height: 100,
child: new Text("1"),
),
new SizedBox(
height: 100,
child: new Text("1"),
),
new SizedBox(
height: 100,
child: new Text("1"),
),
],
),
),
actions: <Widget>[
new FlatButton(
onPressed: () {},
child: new Text("確認(rèn)"),
),
new FlatButton(
onPressed: () {},
child: new Text("取消"),
),
],
);
});
}
- 使用ListView+指定寬度和高度的Container來顯示對(duì)話框
要將ListView包裝在具有特定寬度和高度的Container中。 如果Container沒有定義這兩個(gè)屬性的話,會(huì)報(bào)錯(cuò),無法顯示ListView。(目前我也是這樣解決的,不知道有沒有人有其他更好的方法哈。) 報(bào)錯(cuò)如下:
void showMyDialogWithListView(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return new AlertDialog(
content: new Container(
/*
暫時(shí)的解決方法:要將ListView包裝在具有特定寬度和高度的Container中
如果Container沒有定義這兩個(gè)屬性的話,會(huì)報(bào)錯(cuò),無法顯示ListView
*/
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.9,
child: new ListView.builder(
itemBuilder: (context, index) {
return new SizedBox(
height: 100,
child: new Text("1"),
);
},
itemCount: 10,
shrinkWrap: true,
),
));
},
);
//如果直接將ListView放在dialog中,會(huì)報(bào)錯(cuò),比如
//下面這種寫法會(huì)報(bào)錯(cuò):I/flutter (10721): ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
// I/flutter (10721): The following assertion was thrown during performLayout():
// I/flutter (10721): RenderShrinkWrappingViewport does not support returning intrinsic dimensions.
// I/flutter (10721): Calculating the intrinsic dimensions would require instantiating every child of the viewport, which
// I/flutter (10721): defeats the point of viewports being lazy.
// I/flutter (10721): If you are merely trying to shrink-wrap the viewport in the main axis direction, you should be able
// I/flutter (10721): to achieve that effect by just giving the viewport loose constraints, without needing to measure its
// I/flutter (10721): intrinsic dimensions.
// I/flutter (10721):
// I/flutter (10721): When the exception was thrown, this was the stack:
// I/flutter (10721): #0 RenderShrinkWrappingViewport.debugThrowIfNotCheckingIntrinsics.<anonymous closure> (package:flutter/src/rendering/viewport.dart:1544:9)
// I/flutter (10721): #1 RenderShrinkWrappingViewport.debugThrowIfNotCheckingIntrinsics (package:flutter/src/rendering/viewport.dart:1554:6)
// I/flutter (10721): #2 RenderViewportBase.computeMaxIntrinsicWidth (package:flutter/src/rendering/viewport.dart:321:12)
// I/flutter (10721): #3 RenderBox._computeIntrinsicDimension.<anonymous closure> (package:flutter/src/rendering/box.dart:1109:23)
// I/flutter (10721): #4 __InternalLinkedHashMap&_HashVMBase&MapMixin&_LinkedHashMapMixin.putIfAbsent (dart:collection/runtime/libcompact_hash.dart:277:23)
// I/flutter (10721): #5 RenderBox._computeIntrinsicDimension (package:flutter/src/rendering/box.dart:1107:41)
// I/flutter (10721): #6 RenderBox.getMaxIntrinsicWidth (package:flutter/src/rendering/box.dart:1291:12)
// I/flutter (10721): #7 _RenderProxyBox&RenderBox&RenderObjectWithChildMixin&RenderProxyBoxMixin.computeMaxIntrinsicWidth (package:flutter/src/rendering/proxy_box.dart:81:20)
// showDialog(context: context, builder: (context) {
// return new AlertDialog(title: new Text("title"),
// content: new SingleChildScrollView(
// child: new Container(
// height: 200,
// child: new ListView.builder(
// itemBuilder: (context, index) {
// return new SizedBox(height: 100, child: new Text("1"),);
// }, itemCount: 10, shrinkWrap: true,),
// ),
// ),
// actions: <Widget>[
// new FlatButton(onPressed: () {}, child: new Text("確認(rèn)"),),
// new FlatButton(onPressed: () {}, child: new Text("取消"),),
// ],);
// });
需要?jiǎng)討B(tài)更新界面的對(duì)話框
利用StatefulBuilder來實(shí)現(xiàn)一些對(duì)話框場景,需要對(duì)話框動(dòng)態(tài)更新界面的。
比如在對(duì)話框里面顯示一個(gè)checkbox,然后點(diǎn)擊會(huì)修改checkbox的顯示狀態(tài)。如果是跟之前一樣的實(shí)現(xiàn)對(duì)話框方法, 是無法實(shí)現(xiàn)動(dòng)態(tài)去刷新對(duì)話框的界面的。
StatefulBuilder可以包含一個(gè)child,具有狀態(tài),可以調(diào)用setState刷新界面。
builder參數(shù),用于創(chuàng)建想要顯示的widget,可以調(diào)用StateSetter類型的setState參數(shù)來進(jìn)行刷新界面。
typedef StatefulWidgetBuilder = Widget Function(BuildContext context, StateSetter setState);
const StatefulBuilder({
Key key,
@required this.builder
}) : assert(builder != null),
super(key: key);
實(shí)例的代碼如下:
void showMyDialogWithStateBuilder(BuildContext context) {
showDialog(
context: context,
builder: (context) {
bool selected = false;
return new AlertDialog(
title: new Text("StatefulBuilder"),
content:
new StatefulBuilder(builder: (context, StateSetter setState) {
return Container(
child: new CheckboxListTile(
title: new Text("選項(xiàng)"),
value: selected,
onChanged: (bool) {
setState(() {
selected = !selected;
});
}),
);
}),
);
});
}
自定義Dialog
比如我想顯示一個(gè)菊花的loading加載框,那么用上面的方法都是行不通的。這個(gè)時(shí)候就需要我們?nèi)プ远x一個(gè)對(duì)話框。
首先我們可以先去看一下Dialog的源碼實(shí)現(xiàn),然后只需再照著源碼的實(shí)現(xiàn),修改一下就行了。大部分代碼是保持一致的,所以 對(duì)話框的顯示效果比如動(dòng)畫,主題都是一致的。
下面是Dialog源碼中的build方法實(shí)現(xiàn)。簡單的修改下child屬性所傳的參數(shù)就行了。
@override
Widget build(BuildContext context) {
final DialogTheme dialogTheme = DialogTheme.of(context);
return AnimatedPadding(
padding: MediaQuery.of(context).viewInsets + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
duration: insetAnimationDuration,
curve: insetAnimationCurve,
child: MediaQuery.removeViewInsets(
removeLeft: true,
removeTop: true,
removeRight: true,
removeBottom: true,
context: context,
//所以我們其實(shí)只需要修改child這個(gè)屬性了,改成我們想要展示的widget就行了。
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 280.0),
child: Material(
elevation: 24.0,
color: _getColor(context),
type: MaterialType.card,
child: child,
shape: shape ?? dialogTheme.shape ?? _defaultDialogShape,
),
),
),
),
);
}
下面是一個(gè)自定義加載框Dialog的例子,就是將AlertDialog的源碼進(jìn)行剛才所說的修改就行了。
void showMyCustomLoadingDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return new MyCustomLoadingDialog();
});
}
class MyCustomLoadingDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
Duration insetAnimationDuration = const Duration(milliseconds: 100);
Curve insetAnimationCurve = Curves.decelerate;
RoundedRectangleBorder _defaultDialogShape = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(2.0)));
return AnimatedPadding(
padding: MediaQuery.of(context).viewInsets +
const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
duration: insetAnimationDuration,
curve: insetAnimationCurve,
child: MediaQuery.removeViewInsets(
removeLeft: true,
removeTop: true,
removeRight: true,
removeBottom: true,
context: context,
child: Center(
child: SizedBox(
width: 120,
height: 120,
child: Material(
elevation: 24.0,
color: Theme.of(context).dialogBackgroundColor,
type: MaterialType.card,
//在這里修改成我們想要顯示的widget就行了,外部的屬性跟其他Dialog保持一致
child: new Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new CircularProgressIndicator(),
Padding(
padding: const EdgeInsets.only(top: 20),
child: new Text("加載中"),
),
],
),
shape: _defaultDialogShape,
),
),
),
),
);
}
}
Demo的Github地址
https://github.com/LXD312569496/flutter-learing/tree/master/dialog_learning
公眾號(hào)
最近弄了個(gè)學(xué)習(xí)Flutter的公眾號(hào)(入魔的冬瓜),一部分是搬磚一些國外文章再加些自己的理解,一部分是自己平時(shí)的總結(jié)。
希望與大家在2019一起學(xué)習(xí),共同進(jìn)步!
祝大家狗年快樂!