updated: 2021-07-18 10:15:21.796 url: /?p=304 categories: Flutter tags:
在Flutter中,几乎所有的对象都是一个Widget
,与原生开发中的控件 不同的是,Flutter中的widget
的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector
widget、用于应用主题数据传递的Theme
等等。由于Flutter主要就是用于构建用户界面的,所以,在大多数时候,可以认为widget就是一个控件,不必纠结于概念。
Widget的功能是“描述一个UI元素的配置数据”,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而只是显示元素的一个配置数据。实际上,Flutter中真正代表屏幕上显示元素的类是Element
,也就是说Widget只是描述Element
的一个配置。一个Widget可以对应多个Element
,这是因为同一个Widget对象可以被添加到UI树的不同部分,而真正渲染时,UI树的每一个节点都会对应一个Element
对象。
StatelessWidget
和StatefulWidget
是flutter
的基础组件,日常开发中自定义Widget
都是选择继承这两者之一。也是在往后的开放中,我们最多接触的Widget:
-StatelessWidget
:无状态的,展示信息,面向那些始终不变的UI控件;
-StatefulWidget
:有状态的,可以通过改变状态使得 UI 发生变化,可以包含用户交互(比如弹出一个 dialog)。
在实际使用中,Stateless与Stateful的选择需要取决于这个 Widget 是有状态还是无状态,简单来说看界面是否需要更新。
StatelessWidget用于不需要维护状态的场景,它通常在`build`方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。
BuildContext
表示构建widget的上下文,它是操作widget在树中位置的一个句柄,它包含了一些查找、遍历当前Widget树的一些方法。每一个widget都有一个自己的context对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import 'package:flutter/material.dart' ;void main() => runApp(StatelessApp());class StatelessApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Widget演示" , home: Scaffold( appBar: AppBar(title: Text("Widget" )), body: Text("Hello,Flutter!" ), ) ); } }
Material Design:
一种设计语言,Material Design 于2014年的 Google I/O 首次亮相,是谷歌推出的全新的设计语言。说白了,就是一种设计风格。
StatefulWidget是动态的,添加了一个新的接口`createState()`用于创建和Stateful widget相关的状态`State`,它在Stateful widget的生命周期中可能会被多次调用。
当State被改变时,可以手动调用其`setState()`方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其`build`方法重新构建widget树,从而达到更新UI的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class StatefulState extends State <StatefulApp > { int _i; @override void initState() { super .initState(); _i = 1 ; } @override Widget build(BuildContext context) { return MaterialApp( title: "Widget演示" , theme: ThemeData(), home: Scaffold( appBar: AppBar(title: Text("Widget" )), body: RaisedButton( onPressed: () { setState(() { _i++; }); }, child: Text("Hello,Flutter! $_i " ), ), )); } }
State生命周期 State类除了`build`之外还提供了很多方法能够让我们重写,这些方法会在不同的状态下由Flutter调起执行,所以这些方法我们就称之为**生命周期**方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 import 'package:flutter/material.dart' ;void main() => runApp(MyApp());class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State <MyApp > { bool isShowChild; @override void initState() { super .initState(); isShowChild = true ; debugPrint("parent initState......" ); } @override void didChangeDependencies() { super .didChangeDependencies(); debugPrint("parent didChangeDependencies......" ); } @override Widget build(BuildContext context) { debugPrint("parent build......" ); return MaterialApp( home: Scaffold( body: Center( child: RaisedButton( onPressed: () { setState(() { isShowChild = !isShowChild; }); }, child: isShowChild ? Child() : Text("演示移除Child" ), )), ), ); } @override void didUpdateWidget(MyApp oldWidget) { super .didUpdateWidget(oldWidget); debugPrint("parent didUpdateWidget......" ); } @override void deactivate() { super .deactivate(); debugPrint('parent deactivate......' ); } @override void dispose() { super .dispose(); debugPrint('parent dispose......' ); } } class Child extends StatefulWidget { @override _ChildState createState() => _ChildState(); } class _ChildState extends State <Child > { @override Widget build(BuildContext context) { debugPrint("child build......" ); return Text('lifeCycle' ); } @override void initState() { super .initState(); debugPrint("child initState......" ); } @override void didChangeDependencies() { super .didChangeDependencies(); debugPrint("child didChangeDependencies......" ); } @override void didUpdateWidget(Child oldWidget) { super .didUpdateWidget(oldWidget); debugPrint("child didUpdateWidget......" ); } @override void deactivate() { super .deactivate(); debugPrint('child deactivate......' ); } @override void dispose() { super .dispose(); debugPrint('child dispose......' ); } }
执行的输出结果显示为:
- 运行到显示
1 2 3 4 5 6 I/flutter (22218 ): parent initState...... I/flutter (22218 ): parent didChangeDependencies...... I/flutter (22218 ): parent build...... I/flutter (22218 ): child initState...... I/flutter (22218 ): child didChangeDependencies...... I/flutter (22218 ): child build......
- 点击按钮会移除Child
1 2 3 I/flutter (22218 ): parent build...... I/flutter (22218 ): child deactivate...... I/flutter (22218 ): child dispose......
- 将MyApp的代码由child: isShowChild ? Child() : Text("演示移除Child")
,改为child: Child()
,点击按钮时
1 2 3 I/flutter (22765 ): parent build...... I/flutter (22765 ): child didUpdateWidget...... I/flutter (22765 ): child build......
从这些实验中能够得出State的生命周期为:
文本显示 Text Text
是展示单一格式的文本Widget(Android TextView
)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import 'package:flutter/material.dart' ;void main() => runApp(TextApp());class TextApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Text演示" , home: Scaffold( appBar: AppBar(title: Text("Text" )), body: Text("Hello,Flutter" ), ), ); } }
在使用Text
显示文字时候,可能需要对文字设置各种不同的样式,类似Android的 android:textColor/Size
等
在Flutter中也拥有类似的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 Widget _TextBody() { return Text( "Hello,Flutter" , style: TextStyle( color: Colors.red, fontSize: 18 , fontWeight: FontWeight.w800, fontStyle: FontStyle.italic, decoration: TextDecoration.lineThrough, decorationColor: Colors.black, decorationStyle: TextDecorationStyle.wavy), ); } class TextApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Text演示" , home: Scaffold( appBar: AppBar(title: Text("Text" )), body: _TextBody(), ), ); } }
RichText 如果需要显示更为丰富样式的文本(比如一段文本中文字不同颜色),可以使用RichText
或者Text.rich
1 2 3 4 5 6 7 8 9 10 11 12 Widget _RichTextBody() { var textSpan = TextSpan( text: "Hello" , style: TextStyle(color: Colors.red), children: [ TextSpan(text: "Flu" , style: TextStyle(color: Colors.blue)), TextSpan(text: "uter" , style: TextStyle(color: Colors.yellow)), ], ); return RichText(text: textSpan); }
DefaultTextStyle 在widget树中,文本的样式默认是可以被继承的,因此,如果在widget树的某一个节点处设置一个默认的文本样式,那么该节点的子树中所有文本都会默认使用这个样式。相当于在Android中定义 Theme
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Widget _DefaultStyle(){ DefaultTextStyle( style: TextStyle( color:Colors.red, fontSize: 20.0 , ), textAlign: TextAlign.start, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text("Hello Flutter!" ), Text("Hello Flutter!" ), Text("Hello Flutter!" , style: TextStyle( inherit: false , color: Colors.grey ), ), ], ), ); }
图片显示 “图文”:有文字显示Widget,又怎么少的了图片呢。
FlutterLogo 这个Widget用于显示Flutter的logo......
1 2 3 4 5 6 7 8 9 10 11 12 Widget flutterLogo() { return FlutterLogo( size: 100 , colors: Colors.red, style: FlutterLogoStyle.stacked, textColor: Colors.blue, ); }
Icon 主要用于显示内置图标的Widget
1 2 3 4 5 6 7 8 Widget icon() { return Icon( Icons.add, size: 100 , color: Colors.red); }
Image 显示图片的Widget
。图片常用的格式主要有bmp,jpg,png,gif,webp等,Android中并不是天生支持gif和webp动图,但是这一特性在flutter中被很好的支持了。
方式
解释
Image()
使用ImageProvider提供图片,如下方法本质上也是使用的这个方法
Image.asset
加载资源图片
Image.file
加载本地图片文件
Image.network
加载网络图片
Image.memory
加载内存图片
Iamge.asset 在工程目录下创建目录,如:assets,将图片放入此目录。打开项目根目录:pubspec.yaml
1 2 3 4 5 6 7 return MaterialApp( title: "Image演示" , home: Scaffold( appBar: AppBar(title: Text("Image" )), body: Image.asset("assets/banner.jpeg" ), ), );
Image.file 在sd卡中放入一张图片。然后利用path_provider 库获取sd卡根目录(Dart库版本可以在:https://pub.dartlang.org/packages查询)。
注意权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class ImageState extends State <ImageApp > { Image image; @override void initState() { super .initState(); getExternalStorageDirectory().then((path) { setState(() { image = Image.file(File("${path.path} ${Platform.pathSeparator} banner.jpeg" )); }); }); } @override Widget build(BuildContext context) { return MaterialApp( title: "Image演示" , home: Scaffold( appBar: AppBar(title: Text("Image" )), body: image, ), ); } }
Image.network 直接给网络地址即可。
Flutter 1.0,加载https时候经常出现证书错误。必须断开AS打开app
Image.memory 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Future<List <int >> _imageByte() async { String path = (await getExternalStorageDirectory()).path; return await File("$path ${Platform.pathSeparator} banner.jpeg" ).readAsBytes(); } class ImageState extends State <ImageApp > { Image image; @override void initState() { super .initState(); _imageByte().then((bytes) { setState(() { image = Image.memory(bytes); }); }); } @override Widget build(BuildContext context) { return MaterialApp( title: "Image演示" , home: Scaffold( appBar: AppBar(title: Text("Image" )), body: image, ), ); } }
fit属性相当于android中的scaletype,定义如下:
fit
说明
效果
BoxFit.fill
填充,忽略原有的宽高比,填满为止
BoxFit.contain
包含,不改变原有比例让容器包含整个图片,容器多余部分填充背景
BoxFit.cover
覆盖,不改变原有比例,让图片充满整个容器,图片多余部分裁剪
BoxFit.fitWidth
横向图片填充
BoxFit.fitHeight
纵向图片填充
BoxFit.none
原始大小居中
BoxFit.scaleDown
图片大小小于容器事相当于none,图片大小大于容器时缩小图片大小实现contain
CircleAvatar 主要用来显示用户的头像,任何图片都会被剪切为圆形。
1 2 3 4 5 6 CircleAvatar( backgroundImage: AssetImage("assets/banner.jpeg" ), radius: 50.0 , );
FadeInImage 当使用默认Image
widget显示图片时,您可能会注意到它们在加载完成后会直接显示到屏幕上。这可能会让用户产生视觉突兀。如果最初显示一个占位符,然后在图像加载完显示时淡入,我们可以使用FadeInImage
来达到这个目的!
1 2 3 4 image = FadeInImage.memoryNetwork( placeholder: kTransparentImage, image: 'https://flutter.io/images/homepage/header-illustration.png' , );
按钮 Material widget库中提供了多种按钮Widget如RaisedButton、FlatButton、OutlineButton等,它们都是直接或间接对RawMaterialButton的包装定制,所以他们大多数属性都和RawMaterialButton
一样。所有Material 库中的按钮都有如下相同点:
按下时都会有“水波动画”。
有一个onPressed
属性来设置点击回调,当按钮按下时会执行该回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击。
“漂浮”按钮,它默认带有阴影和灰色背景
1 2 3 4 RaisedButton( child: Text("normal" ), onPressed: () => {}, )
扁平按钮,默认背景透明并不带阴影
1 2 3 4 FlatButton( child: Text("normal" ), onPressed: () => {}, )
默认有一个边框,不带阴影且背景透明。
1 2 3 4 OutlineButton( child: Text("normal" ), onPressed: () => {}, )
可点击的Icon
1 2 3 4 IconButton( icon: Icon(Icons.thumb_up), onPressed: () => {}, )
按钮外观可以通过其属性来定义,不同按钮属性大同小异
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const FlatButton({ ... @required this .onPressed, this .textColor, this .disabledTextColor, this .color, this .disabledColor, this .highlightColor, this .splashColor, this .colorBrightness, this .padding, this .shape, @required this .child, }) FlatButton( onPressed: () => {}, child: Text("Raised" ), color: Colors.blue, splashColor: Colors.yellow, colorBrightness: Brightness.dark, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50 ) ), )
而RaisedButton
,默认配置有阴影,因此在配置RaisedButton
时,拥有一系列 elevation 属性的配置
1 2 3 4 5 6 7 const RaisedButton({ ... this .elevation = 2.0 , this .highlightElevation = 8.0 , this .disabledElevation = 0.0 , ... }
输入框 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import 'package:flutter/material.dart' ;void main() => runApp(Demo1());class Demo1 extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Demo1" , home: Scaffold( appBar: AppBar( title: Text("登录" ), ), body: Column( children: <Widget>[ TextField( autofocus: true , decoration: InputDecoration( labelText: "用户名" , hintText: "用户名或邮箱" , prefixIcon: Icon(Icons.person)), ), TextField( obscureText: true , decoration: InputDecoration( labelText: "密码" , hintText: "您的登录密码" , prefixIcon: Icon(Icons.lock)), ), ], ), ), ); } }
这个效果非常的“系统”,我们可能大多数情况下需要将下划线更换为矩形边框,这时候可能就需要组合widget来完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Container( margin: EdgeInsets.all(32 ), child: TextField( keyboardType: TextInputType.emailAddress, decoration: InputDecoration( labelText: "用户名" , hintText: "用户名或邮箱" , prefixIcon: Icon(Icons.person), border: InputBorder.none )), decoration: BoxDecoration( border: Border.all(color: Colors.red[200 ], width: 1.0 ), borderRadius: BorderRadius.circular(5.0 ), ), )
焦点控制 FocusNode: 与Widget绑定,代表了这个Widget的焦点
FocusScope: 焦点控制范围
FocusScopeNode:控制焦点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 class _TextFocusState extends State <TextFocusWidget > { FocusNode focusNode1 = new FocusNode(); FocusNode focusNode2 = new FocusNode(); void _listener() { debugPrint("用户名输入框焦点:${focusNode1.hasFocus} " ); } @override void initState() { super .initState(); focusNode1.addListener(_listener); } @override void dispose() { super .dispose(); focusNode1.dispose(); focusNode2.dispose(); } @override Widget build(BuildContext context) { return Column( children: <Widget>[ TextField( autofocus: true , focusNode: focusNode1, textInputAction: TextInputAction.next, onEditingComplete: () { FocusScopeNode focusScopeNode = FocusScope.of(context); focusScopeNode.requestFocus(focusNode2); }, decoration: InputDecoration( labelText: "用户名" , hintText: "用户名或邮箱" , prefixIcon: Icon(Icons.person)), ), TextField( obscureText: true , focusNode: focusNode2, decoration: InputDecoration( labelText: "密码" , hintText: "您的登录密码" , prefixIcon: Icon(Icons.lock)), ), custom(), ], ); } }
获取输入内容 获取输入内容有两种方式:
定义两个变量,用于保存用户名和密码,然后在onChange触发时,各自保存一下输入内容。
通过controller直接获取。
onChange获得输入内容:
1 2 3 TextField( onChanged: (s) => debugPrint("ssss:$s " ), )
controller获取:
定义一个controller:
1 2 TextEditingController _unameController=new TextEditingController();
然后设置输入框controller:
1 2 3 4 TextField( controller: _unameController, ... )
通过controller获取输入框内容
1 debugPrint(_unameController.text)
TextFormField TextFormField
比TextField
多了一些属性,其中 **validator **用于设置验证回调。在单独使用时与TextField
没有太大的区别。当结合From
,利用From
可以对输入框进行分组,然后进行一些统一操作(验证)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class _TextFocusState extends State <TextFocusWidget > { GlobalKey<FormState> _key = GlobalKey<FormState>(); @override Widget build(BuildContext context) { return Form( key: _key, child: Column( children: <Widget>[ TextFormField( autofocus: true , decoration: InputDecoration( labelText: "用户名" , hintText: "用户名或邮箱" , icon: Icon(Icons.person)), validator: (v) { return v.trim().length > 0 ? null : "用户名不能为空" ; }), TextFormField( decoration: InputDecoration( labelText: "密码" , hintText: "您的登录密码" , icon: Icon(Icons.lock)), validator: (v) { return v.trim().length > 0 ? null : "密码不能为空" ; }), RaisedButton( onPressed: () { if (_key.currentState.validate()) { } }, child: Text("提交" ), ) ], )); } }