Flutter 学习
一、认识Fluter
几乎完全还原手机app,相当于原生app。
二、环境搭建(Windows)
Windows 7以上64位系统,磁盘空间大于3个G,因为要安装模拟虚拟机
1、java环境的安装,下载地址:java下载地址--自己现在相应的版本,下载安装完成之后,在终端中输入 java,出现选项帮助即为安装成功
2、下载安装FlutterSDK,下载地址:FlutterSDK下载地址,下载安装完成之后,在Flutter安装目录的flutter
文件下找到flutter_console.bat
,双击运行并启动flutter命令行,接下来,你就可以在Flutter命令行运行flutter命令了。
2.1、配置环境变量,整个过程需要翻墙,若要在任何地方都可以使用Flutter命令。配置Flutter SDk目录配置到系统环境变量Path中D:\Flutter\flutter\bin
2.2、进行Flutter doctor -v测试,确认安装的Flutter是否正确无误,在终端中输入flutter doctor -v,结果中有X的说明安装是有问题的,点根烟,看看窗外,不要急,进行下一步,安装Android Studio
2.3、傻瓜式安装,点击下一步,整个过程需要翻墙,Android Studio下载地址:Android Studio,找到下载点击下载就好,赶紧安吧。
整个过程持续时间比较久,男人嘛,久点好。大哥,你要是真的不会装,送你一个详细教程:不会装就看他装,OK,安装成功之后,就如插件商店Plugin,搜索Flutter插件,点中间的Search in repositories
,然后点击安装。别犹豫下载他,完成之后重启。
2.4、此时我们可以在终端中再次输入 flutter doctor -v ,发现缺少证书,我们继续安装证书。在终端中输入
flutter doctor --android-licenses
见到需要选择Y/N的,一律选择Y,直到证书安装完成,先不要高兴,下面一步进行虚拟机安装
2.5、先新建一个flutter文件,然后安装虚拟机,点击Android Studio中的上方菜单tool
-AVD Manager
选项。出现新建菜单,选择Create Virtual Device.....
,如果你一个虚拟机也没建过,这个选项在对话框的中间,选择虚拟机类型,这个你随意选就好,我选择的是Nexus 5x
。选择系统,这里尽量选择最新的,我选择了Android 9.0
系统,安装好后,点击开始按钮,运行虚拟机,虚拟机运行之后,可以点击debug按钮,你可能会遇到一些小问题,不要急,看下一步
2.6、第一步:修改掉项目下的android目录下的build.gradle
文件,把google() 和 jcenter()这两行去掉。改为阿里的链接。
maven { url 'https://maven.aliyun.com/repository/google' }maven { url 'https://maven.aliyun.com/repository/jcenter' }maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
全部代码:
buildscript { repositories { // google() // jcenter() maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/jcenter' } maven { url 'http://maven.aliyun.com/nexus/content/groups/public'} } dependencies { classpath 'com.android.tools.build:gradle:3.1.2' }}allprojects { repositories { // google() // jcenter() maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/jcenter' } maven { url 'http://maven.aliyun.com/nexus/content/groups/public' } }}rootProject.buildDir = '../build'subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}"}subprojects { project.evaluationDependsOn(':app')}task clean(type: Delete) { delete rootProject.buildDir}
注意是有两个部分进行了修改,不要只修改一处。
第二步:修改Flutter SDK包下的flutter.gradle
文件,这个目录要根据你的SDK存放的位置有所变化。比如我放在了D盘Flutter目录下,那路径就是这个。
D:\Flutter\flutter\packages\flutter_tools\gradle
打开文件进行修改,修改代码如下
buildscript { repositories { //jcenter() // maven { // url 'https://dl.google.com/dl/android/maven2' // } maven{ url 'https://maven.aliyun.com/repository/jcenter' } maven{ url 'http://maven.aliyun.com/nexus/content/groups/public' } } dependencies { classpath 'com.android.tools.build:gradle:3.1.2' }}
然后再重新Debug一下,就基本可以启动起来。
2.7、在vsCode中使用flutter,只需要下载Flutter即可,为了加快我们的开发速度,我们来写一个快速启动虚拟机的批处理文件,
- 打开
emulator.exe
这个程序,你可以巧妙利用windows的查找工具进行查找。 - 打开你设置的虚拟机,批处理时需要填写你设置的虚拟机名称。
新建一个
xxx.bat
文件到桌面,xxx的意思是,你可以自己取名字,随意叫什么都可以。我这里叫EmulatorRun.bat
.查找
emulator.exe
文件的路径,把查找到的路径放到bat文件中, 你一般会查找到两个emulator.exe文件,一个是在tools目录下,一个是在emulator目录下,我们选择emulator
目录下的这个,复制它的路径。
C:\Users\Administrator\AppData\Local\Android\Sdk\emulator\emulator.exe
3,打开Android Studio
,并查看你的AVD虚拟机名称, 如果你觉的输入不方便和怕出错,你可以点击图片后边的笔型按钮,进入编辑模式,复制这个名称。
4.然后根据你复制的名称,把bat文件输入成如下形式。
C:\Users\Administrator\AppData\Local\Android\Sdk\emulator\emulator.exe -netdelay none -netspeed full -avd Nexus_5X_API_28
参数解释:
- -netdelay none :设置模拟器的网络延迟时间,默认为none,就是没有延迟。
- -netspeed full: 设置网络加速值,full代表全速。
2.8、现在可以跑起来flutter项目了,在终端中输入 flutter run 等待启动
flutter run
三、Flutter常用组件
TextAlign属性就是文本的对齐方式,它的属性值有如下几个(详细请看视频中讲解):
- center: 文本以居中形式对齐,这个也算比较常用的了。
- left:左对齐,经常使用,让文本居左进行对齐,效果和start一样。
- right :右对齐,使用频率也不算高。
- start:以开始位置进行对齐,类似于左对齐。
- end: 以为本结尾处进行对齐,不常用。有点类似右对齐.
child:Text( 'Hello JSPang ,非常喜欢前端,并且愿意为此奋斗一生。我希望可以出1000集免费教程。', textAlign:TextAlign.left,)
设置最多显示的行数,后面直接跟数字即可
child:Text( 'Hello JSPang ,非常喜欢前端,并且愿意为此奋斗一生。我希望可以出1000集免费教程。', textAlign:TextAlign.left, maxLines: 1,)
overflow属性是用来设置文本溢出时,如何处理,它有下面几个常用的值供我们选择。
- clip:直接切断,剩下的文字就没有了,感觉不太友好,体验性不好。
- ellipsis:在后边显示省略号,体验性较好,这个在工作中经常使用。
- fade: 溢出的部分会进行一个渐变消失的效果,当然是上线的渐变,不是左右的哦。
style属性的内容比较多
child:Text( 'Hello JSPang ,非常喜欢前端,并且愿意为此奋斗一生。我希望可以出1000集免费教程。', textAlign:TextAlign.left, overflow:TextOverflow.ellipsis, maxLines: 1, style: TextStyle( fontSize:25.0, color:Color.fromARGB(255, 255, 150, 150), decoration:TextDecoration.underline,//设置下划线 decorationStyle:TextDecorationStyle.solid,//下划线属性 ),)
更详细的属性资料可以参看这个网址:https://docs.flutter.io/flutter/painting/TextStyle-class.html
Container(容器控件)在Flutter是经常使用的控件,它就相当于我们HTML里的<div>
标签
其实容器的作用就是方便我们进行布局的,这个属性针对的是Container内child的对齐方式,也就是容器子内容的对齐方式,并不是容器本身的对齐方式。
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'Text widget', home:Scaffold( body:Center( child:Container( child:new Text('Hello JSPang',style: TextStyle(fontSize: 40.0),), alignment: Alignment.center, ), ), ), ); }}
bottomCenter
:下部居中对齐。botomLeft
: 下部左对齐。bottomRight
:下部右对齐。center
:纵横双向居中对齐。centerLeft
:纵向居中横向居左对齐。centerRight
:纵向居中横向居右对齐。topLeft
:顶部左侧对齐。topCenter
:顶部居中对齐。topRight
: 顶部居左对齐
设置宽、高和颜色属性
设置宽、高和颜色属性
child:Container( child:new Text('Hello JSPang',style: TextStyle(fontSize: 40.0),), alignment: Alignment.center, width:500.0, height:400.0, color: Colors.lightBlue,),
padding属性
padding的属性就是一个内边距,它和你使用的前端技术CSS里的padding
表现形式一样
child:Container( child:new Text('Hello JSPang',style: TextStyle(fontSize: 40.0),), alignment: Alignment.topLeft, width:500.0, height:400.0, color: Colors.lightBlue, padding:const EdgeInsets.all(10.0),),
如何单独设置某一边的内边距使用** EdgeInsets.fromLTRB(value1,value2,value3,value4)
**
那我们设置上边距为30,左边距为10,就可以用下面的代码来编写。
padding:const EdgeInsets.fromLTRB(10.0,30.0,0.0,0.0),
child:Container( child:new Text('Hello JSPang',style: TextStyle(fontSize: 40.0),), alignment: Alignment.topLeft, width:500.0, height:400.0, color: Colors.lightBlue, padding:const EdgeInsets.fromLTRB(10.0,30.0,0.0,0.0), margin: const EdgeInsets.all(10.0),),
decoration
是 container 的修饰器,主要的功能是设置背景和边框。
比如你需要给背景加入一个渐变,这时候需要使用BoxDecoration这个类,代码如下(需要注意的是如果你设置了decoration,就不要再设置color属性了,因为这样会冲突)
child:Container( child:new Text('Hello JSPang',style: TextStyle(fontSize: 40.0),), alignment: Alignment.topLeft, width:500.0, height:400.0, padding:const EdgeInsets.fromLTRB(10.0,30.0,0.0,0.0), margin: const EdgeInsets.all(10.0), decoration:new BoxDecoration( gradient:const LinearGradient( colors:[Colors.lightBlue,Colors.greenAccent,Colors.purple] ) ),),
设置边框
设置边框可以在decoration里设置border属性
child:Container( child:new Text('Hello JSPang',style: TextStyle(fontSize: 40.0),), alignment: Alignment.topLeft, width:500.0, height:400.0, padding:const EdgeInsets.fromLTRB(10.0,30.0,0.0,0.0), margin: const EdgeInsets.all(10.0), decoration:new BoxDecoration( gradient:const LinearGradient( colors:[Colors.lightBlue,Colors.greenAccent,Colors.purple] ), border:Border.all(width:2.0,color:Colors.red) ),),
- Image.asset:加载资源图片,就是加载项目资源目录中的图片,加入图片后会增大打包的包体体积,用的是相对路径。
- Image.network:网络资源图片,意思就是你需要加入一段http://xxxx.xxx的这样的网络路径地址。
- Image.file:加载本地图片,就是加载本地文件中的图片,这个是一个绝对路径,跟包体无关。
- Image.memory: 加载Uint8List资源图片,这个我目前用的不是很多,所以没什么发言权。
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'Text widget', home:Scaffold( body:Center( child:Container( child:new Image.network( 'http://jspang.com/static/myimg/blogtouxiang.jpg', scale:1.0, ), width:300.0, height:200.0, color: Colors.lightBlue, ), ), ), ); }}
fit属性可以控制图片的拉伸和挤压
BoxFit.fill:全图显示,图片会被拉伸,并充满父容器。
BoxFit.contain:全图显示,显示原比例,可能会有空隙。
BoxFit.cover:显示可能拉伸,可能裁切,充满(图片要充满整个容器,还不变形)。
BoxFit.fitWidth:宽度充满(横向充满),显示可能拉伸,可能裁切。
BoxFit.fitHeight :高度充满(竖向充满),显示可能拉伸,可能裁切。
BoxFit.scaleDown:效果和contain差不多,但是此属性不允许显示超过源图片大小,可小不可大
图片混合模式(colorBlendMode)和color属性配合使用,能让图片改变颜色,里边的模式非常的多,产生的效果也是非常丰富的
child:new Image.network( 'http://jspang.com/static/myimg/blogtouxiang.jpg', color: Colors.greenAccent, colorBlendMode: BlendMode.darken,),
- color:是要混合的颜色,如果你只设置color是没有意义的。
- colorBlendMode:是混合模式,相当于我们如何混合。
ImageRepeat.repeat : 横向和纵向都进行重复,直到铺满整个画布。
ImageRepeat.repeatX: 横向重复,纵向不重复。
ImageRepeat.repeatY:纵向重复,横向不重复。
child:new Image.network( 'http://jspang.com/static/myimg/blogtouxiang.jpg', repeat: ImageRepeat.repeat,),
列表组件的知识其实是很多的
body: new ListView( children:<Widget>[ new ListTile( leading:new Icon(Icons.access_time), title:new Text('access_time') ) ]),
我们使用了ListView,然后在他的内部children
中,使用了widget
数组,因为是一个列表,所以它接受一个数组,然后有使用了listTite组件(列表瓦片),在组件中放置了图标和文字。
body: new ListView( children:<Widget>[ new ListTile( leading:new Icon(Icons.access_time), title:new Text('access_time') ), new ListTile( leading:new Icon(Icons.account_balance), title:new Text('account_balance') ), ]),
上节课学习了Image Widget
body: new ListView( children:<Widget>[ new Image.network( 'http://jspang.com/static/upload/20181111/G-wj-ZQuocWlYOHM6MT2Hbh5.jpg' ), new Image.network( 'http://jspang.com/static/upload/20181109/1bHNoNGpZjyriCNcvqdKo3s6.jpg' ), new Image.network( 'http://jspang.com/static/myimg/typescript_banner.jpg' ),new Image.network( 'http://jspang.com/static/myimg/smile-vue.jpg' ) ]),
横向列表的使用
已经对ListView有了清楚的认识,还是使用我们的ListView组件,只是在ListView组件里加一个ScrollDirection
属性。
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'Text widget', home:Scaffold( body:Center( child:Container( height:200.0, child:new ListView( scrollDirection: Axis.horizontal, children: <Widget>[ new Container( width:180.0, color: Colors.lightBlue, ), new Container( width:180.0, color: Colors.amber, ), new Container( width:180.0, color: Colors.deepOrange, ),new Container( width:180.0, color: Colors.deepPurpleAccent, ), ], ) ), ), ), ); }}
我们先是加入了Center组件,作用是让我们的横向列表可以居中到屏幕的中间位置,然后在center组件的下面加入了Container容器组件,并设置了容器组件的高是200,在容器组件里我们加入了ListView
组件,然后设置了组件的scrollDirection属性。然后再ListView的子组件里加入了Container容器组件,然后设置了不同颜色,
ListView组件的scrollDirection
属性只有两个值,一个是横向滚动,一个是纵向滚动。默认的就是垂直滚动,所以如果是垂直滚动,我们一般都不进行设置。
- Axis.horizontal:横向滚动或者叫水平方向滚动。
- Axis.vertical:纵向滚动或者叫垂直方向滚动。
代码优化,组件拆分
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'ListView widget', home:Scaffold( body:Center( child:Container( height:200.0, child:MyList() ), ), ), ); }}class MyList extends StatelessWidget{ @override Widget build(BuildContext context){ return ListView( scrollDirection: Axis.horizontal, children: <Widget>[ new Container( width:180.0, color: Colors.lightBlue, ), new Container( width:180.0, color: Colors.amber, ), new Container( width:180.0, color: Colors.deepOrange, ),new Container( width:180.0, color: Colors.deepPurpleAccent, ), ], ); }}
List是Dart的集合类型之一,其实你可以把它简单理解为数组(反正我是这么认为的),其他语言也都有这个类型。它的声明有几种方式:
var myList = List()
: 非固定长度的声明。var myList = List(2)
: 固定长度的声明。var myList= List<String>()
:固定类型的声明方式。var myList = [1,2,3]
: 对List直接赋值。
那我们这里使用的是一个List传递,然后直接用List中的generate
方法进行生产List里的元素。最后的结果是生产了一个带值的List变量。代码如下:
void main () => runApp(MyApp( items: new List<String>.generate(1000, (i)=> "Item $i")));
说明:再main
函数的runApp中调用了MyApp类,再使用类的使用传递了一个items
参数,并使用generate生成器对items
进行赋值。
generate方法传递两个参数,第一个参数是生成的个数,第二个是方法。
我们已经传递了参数,那MyApp这个类是需要接收的。
final List<String> items; MyApp({Key key, @required this.items}):super(key:key);
这是一个构造函数,除了Key,我们增加了一个必传参数,这里的@required
意思就必传。:super
如果父类没有无名无参数的默认构造函数,则子类必须手动调用一个父类构造函数。
这样我们就可以接收一个传递过来的参数了,当然我们要事先进行声明。
接受了值之后,就可以直接调用动态列表进行生成了。具体代码如下:
import 'package:flutter/material.dart';void main () => runApp(MyApp( items: new List<String>.generate(1000, (i)=> "Item $i")));class MyApp extends StatelessWidget{ final List<String> items; MyApp({Key key, @required this.items}):super(key:key); @override Widget build(BuildContext context ){ return MaterialApp( title:'ListView widget', home:Scaffold( body:new ListView.builder( itemCount:items.length, itemBuilder:(context,index){ return new ListTile( title:new Text('${items[index]}'), ); } ) ), ); }}
列表组件已经学会了,那还有一种常用的列表,叫做网格列表,网格列表经常用来显示多张图片,比如我们经常使用的手机里的相册功能,大部分形式都是网格列表。
我们先不做一个相册的应用,而是使用文字,作一个最简单的网格列表,目的是先熟悉一下GridView
的基本语法,代码如下
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'ListView widget', home:Scaffold( body:GridView.count( padding:const EdgeInsets.all(20.0), crossAxisSpacing: 10.0, crossAxisCount: 3, children: <Widget>[ const Text('I am Jspang'), const Text('I love Web'), const Text('jspang.com'), const Text('我喜欢玩游戏'), const Text('我喜欢看书'), const Text('我喜欢吃火锅') ], ) ), ); }}
我们在body属性中加入了网格组件,然后给了一些常用属性:
- padding:表示内边距,这个小伙伴们应该很熟悉。
- crossAxisSpacing:网格间的空当,相当于每个网格之间的间距。
- crossAxisCount:网格的列数,相当于一行放置的网格数量。
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'ListView widget', home:Scaffold( body:GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 2.0, crossAxisSpacing: 2.0, childAspectRatio: 0.7 ), children: <Widget>[ new Image.network('http://img5.mtime.cn/mt/2018/10/22/104316.77318635_180X260X4.jpg',fit: BoxFit.cover), new Image.network('http://img5.mtime.cn/mt/2018/10/10/112514.30587089_180X260X4.jpg',fit: BoxFit.cover), new Image.network('http://img5.mtime.cn/mt/2018/11/13/093605.61422332_180X260X4.jpg',fit: BoxFit.cover), new Image.network('http://img5.mtime.cn/mt/2018/11/07/092515.55805319_180X260X4.jpg',fit: BoxFit.cover), new Image.network('http://img5.mtime.cn/mt/2018/11/21/090246.16772408_135X190X4.jpg',fit: BoxFit.cover), new Image.network('http://img5.mtime.cn/mt/2018/11/17/162028.94879602_135X190X4.jpg',fit: BoxFit.cover), new Image.network('http://img5.mtime.cn/mt/2018/11/19/165350.52237320_135X190X4.jpg',fit: BoxFit.cover), new Image.network('http://img5.mtime.cn/mt/2018/11/16/115256.24365160_180X260X4.jpg',fit: BoxFit.cover), new Image.network('http://img5.mtime.cn/mt/2018/11/20/141608.71613590_135X190X4.jpg',fit: BoxFit.cover), ], ) ), ); }}
childAspectRatio:宽高比,这个值的意思是宽是高的多少倍,如果宽是高的2倍,那我们就写2.0,如果高是宽的2倍,我们就写0.5。
四、Flutter布局
Flutter中的row控件就是水平控件,它可以让Row里边的子元素进行水平排列。Row控件可以分为灵活排列和非灵活排列两种,这两种模式都需要熟练掌握,等经验丰富后可根据需求进行使用。
从字面上就可以理解到,不灵活就是根据Row子元素的大小,进行布局。如果子元素不足,它会留有空隙,如果子元素超出,它会警告。
比如现在我们要制作三个按钮,并让三个按钮同时在一排。我们写下了如下代码,但你会发现效果并不理想。
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'ListView widget', home:Scaffold( appBar:new AppBar( title:new Text('水平方向布局'), ), body:new Row( children: <Widget>[ new RaisedButton( onPressed: (){ }, color:Colors.redAccent, child:new Text('红色按钮') ), new RaisedButton( onPressed: (){ }, color:Colors.orangeAccent, child: new Text('黄色按钮'), ), new RaisedButton( onPressed: (){ }, color:Colors.pinkAccent, child:new Text('粉色按钮') ) ], ) ), ); }}
这时候你会发现的页面已经有了三个按钮,但这三个按钮并没有充满一行,而是出现了空隙。这就是不灵活横向排列造成的。它根据子元素的大小来进行排列。如果我们想实现充满一行的效果,就要使用灵活水平布局了。
解决上面有空隙的问题,可以使用 Expanded
来进行解决,也就是我们说的灵活布局。我们在按钮的外边加入Expanded就可以了,代码如下:
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'ListView widget', home:Scaffold( appBar:new AppBar( title:new Text('水平方向布局'), ), body:new Row( children: <Widget>[ Expanded(child:new RaisedButton( onPressed: (){ }, color:Colors.redAccent, child:new Text('红色按钮') )), Expanded(child:new RaisedButton( onPressed: (){ }, color:Colors.orangeAccent, child: new Text('黄色按钮'), ) ), Expanded(child:new RaisedButton( onPressed: (){ }, color:Colors.pinkAccent, child:new Text('粉色按钮') ) ) ], ) ), ); }}
如果这时候想让中间的按钮大,而两边的按钮保持真实大小,就可以不灵活和灵活模式进行混用,实现效果。代码和效果如下:
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'ListView widget', home:Scaffold( appBar:new AppBar( title:new Text('水平方向布局'), ), body:new Row( children: <Widget>[ new RaisedButton( onPressed: (){ }, color:Colors.redAccent, child:new Text('红色按钮') ), Expanded(child:new RaisedButton( onPressed: (){ }, color:Colors.orangeAccent, child: new Text('黄色按钮'), ) ), new RaisedButton( onPressed: (){ }, color:Colors.pinkAccent, child:new Text('粉色按钮') ) ], ) ), ); }}
Column组件即垂直布局控件,能够将子组件垂直排列。
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'ListView widget', home:Scaffold( appBar:new AppBar( title:new Text('垂直方向布局'), ), body:Column( children: <Widget>[ Text('I am JSPang'), Text('my website is jspang.com'), Text('I love coding') ], ) ), ); }}
左对齐只要在column组件下加入下面的代码,就可以让文字左对齐。
crossAxisAlignment: CrossAxisAlignment.start,
- CrossAxisAlignment.star:居左对齐。
- CrossAxisAlignment.end:居右对齐。
- CrossAxisAlignment.center:居中对齐。
在设置对齐方式的时候你会发现右mainAxisAlignment属性,意思就是主轴对齐方式,那什么是主轴,什么又是幅轴那。
main轴:如果你用column组件,那垂直就是主轴,如果你用Row组件,那水平就是主轴。
cross轴:cross轴我们称为幅轴,是和主轴垂直的方向。比如Row组件,那垂直就是幅轴,Column组件的幅轴就是水平方向的。
比如现在我们要把上面的代码,改成垂直方向居中。因为用的是Column组件,所以就是主轴方向,这时候你要用的就是主轴对齐了。
mainAxisAlignment: MainAxisAlignment.center,
现在全部的代码如下:
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'ListView widget', home:Scaffold( appBar:new AppBar( title:new Text('垂直方向布局'), ), body:Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text('I am JSPang'), Text('my website is jspang.com'), Text('I love coding') ], ) ), ); }}
让文字相对于水平方向居中,只要加入Center组件就可以轻松解决。
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return MaterialApp( title:'ListView widget', home:Scaffold( appBar:new AppBar( title:new Text('垂直方向布局'), ), body:Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Center(child:Text('I am JSPang')), Center(child:Text('my website is jspang.com')), Center(child:Text('I love coding')) ], ) ), ); }}
其实在学习水平布局的时候我们对Expanded有了深刻的理解,它就是灵活布局。比如我们想让中间区域变大,而头部区域和底部根据文字所占空间进行显示。
body:Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Center(child:Text('I am JSPang')), Expanded(child:Center(child:Text('my website is jspang.com'))), Center(child:Text('I love coding')) ],)
水平布局和垂直布局确实很好用,但是有一种情况是无法完成的,比如放入一个图片,图片上再写一些字或者放入容器,这时候Row和Column就力不从心了。Flutter为这种情况准备了Stack层叠布局
alignment属性是控制层叠的位置的,建议在两个内容进行层叠时使用。它有两个值X轴距离和Y轴距离,值是从0到1的,都是从上层容器的左上角开始算起的
CircleAvatar
这个经常用来作头像的,组件里边有个radius
的值可以设置图片的弧度。
现在我们准备放入一个图像,然后把弧度设置成100,形成一个漂亮的圆形,代码如下:
new CircleAvatar( backgroundImage: new NetworkImage('http://jspang.com/static//myimg/blogtouxiang.jpg'), radius: 100.0,),
想布局出这个效果还是比较容易的,代码如下:
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ var stack = new Stack( alignment: const FractionalOffset(0.5, 0.8), children: <Widget>[ new CircleAvatar( backgroundImage: new NetworkImage('http://jspang.com/static//myimg/blogtouxiang.jpg'), radius: 100.0, ), new Container( decoration: new BoxDecoration( color: Colors.lightBlue, ), padding: EdgeInsets.all(5.0), child: new Text('JSPang 技术胖'), ) ], ); return MaterialApp( title:'ListView widget', home:Scaffold( appBar:new AppBar( title:new Text('垂直方向布局'), ), body:Center(child:stack), ), ); }}
上节课已经学习了Stack
组件,并且进行了两个组件的层叠布局,但是如果是超过两个组件的层叠该如何进行定位那?这就是我们加今天要学的主角Positioned
组件了。这个组件也叫做层叠定位组件。
- bottom: 距离层叠组件下边的距离
- left:距离层叠组件左边的距离
- top:距离层叠组件上边的距离
- right:距离层叠组件右边的距离
- width: 层叠定位组件的宽度
- height: 层叠定位组件的高度
实现图片中的布局,代码如下:
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ var stack = new Stack( children: <Widget>[ new CircleAvatar( backgroundImage: new NetworkImage('http://jspang.com/static//myimg/blogtouxiang.jpg'), radius: 100.0, ), new Positioned( top:10.0, left:10.0, child: new Text('JSPang.com'), ), new Positioned( bottom:10.0, right:10.0, child: new Text('技术胖'), ) ], ); return MaterialApp( title:'ListView widget', home:Scaffold( appBar:new AppBar( title:new Text('层叠布局'), ), body:Center(child:stack), ), ); }}
Flutter还有一种比较比较酷炫的布局方式,我称 它为卡片式布局
比如我们现在要开发一个类似收获地址的列表,并且列表外部使用一个卡片式布局。
卡片式布局默认是撑满整个外部容器的,如果你想设置卡片的宽高,需要在外部容器就进行制定
代码中使用了一个垂直布局组件Column组件,然后利用了ListTile
实现内部列表,这里需要说明的是ListTile不光可以使用在ListView组件中,然后容器组件其实都可以使用。代码如下.
import 'package:flutter/material.dart';void main () => runApp(MyApp());class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ var card = new Card( child: Column( children: <Widget>[ ListTile( title:new Text('吉林省吉林市昌邑区',style: TextStyle(fontWeight: FontWeight.w500),), subtitle: new Text('技术胖:1513938888'), leading: new Icon(Icons.account_box,color: Colors.lightBlue,), ), new Divider(), ListTile( title:new Text('北京市海淀区中国科技大学',style: TextStyle(fontWeight: FontWeight.w500),), subtitle: new Text('胜宏宇:1513938888'), leading: new Icon(Icons.account_box,color: Colors.lightBlue,), ), new Divider(), ListTile( title:new Text('河南省濮阳市百姓办公楼',style: TextStyle(fontWeight: FontWeight.w500),), subtitle: new Text('JSPang:1513938888'), leading: new Icon(Icons.account_box,color: Colors.lightBlue,), ), new Divider(), ], ), ); return MaterialApp( title:'ListView widget', home:Scaffold( appBar:new AppBar( title:new Text('卡片布局'), ), body:Center(child:card), ), ); }}
五、页面导航和其他知识
它有两个最基本的属性:
- child:可以放入容器,图标,文字。让你构建多彩的按钮。
- onPressed:点击事件的相应,一般会调用
Navigator
组件。
Navigator.push 和 Navigator.pop
Navigator.push
:是跳转到下一个页面,它要接受两个参数一个是上下文context
,另一个是要跳转的函数。Navigator.pop
:是返回到上一个页面,使用时传递一个context(上下文)参数,使用时要注意的是,你必须是有上级页面的,也就是说上级页面使用了Navigator.push
。
我们现在就来作一个简单的案例,我们打开一个页面,页面上只有一个简单的按钮,按钮写着“查看商品详情页面”,然后点击后进入下一个页面,页面有一个按钮,可以直接返回。
import 'package:flutter/material.dart';void main(){ runApp(MaterialApp( title:'导航演示1', home:new FirstScreen() ));}class FirstScreen extends StatelessWidget{ @override Widget build(BuildContext context){ return new Scaffold( appBar: AppBar(title:Text('导航页面')), body:Center( child:RaisedButton( child:Text('查看商品详情页面'), onPressed: (){ Navigator.push(context,new MaterialPageRoute( builder:(context) =>new SecondScreen()) ); }, ) ) ); }}class SecondScreen extends StatelessWidget{ @override Widget build(BuildContext context){ return Scaffold( appBar:AppBar(title:Text('技术胖商品详情页')), body:Center(child:RaisedButton( child:RaisedButton( child:Text('返回'), onPressed: (){ Navigator.pop(context); }, ) )) ); }}
Dart中可以使用类来抽象一个数据,比如我们模仿一个商品信息,有商品标题和商品描述。我们定义了一个Product类,里边有两个字符型变量,title和description。
- title:是商品标题。
- description: 商品详情描述
代码如下:
class Product{ final String title; //商品标题 final String description; //商品描述 Product(this.title,this.description);}
作一个商品的列表,这里我们采用动态的构造方法,在主方法里传递一个商品列表(List)到自定义的Widget中。
先来看看主方法的编写代码:
void main(){ runApp(MaterialApp( title:'数据传递案例', home:ProductList( products:List.generate( 20, (i)=>Product('商品 $i','这是一个商品详情,编号为:$i') ), ) ));}
上面的代码是主路口文件,主要是在home属性中,我们使用了ProductList,这个自定义组件,而且时候会报错,因为我们缺少这个组件。这个组件我们传递了一个products参数,也就是商品的列表数据,这个数据是我们用List.generate
生成的。并且这个生成的List原型就是我们刚开始定义的Product这个类(抽象数据)。
ProductList自定义组件的代码:
class ProductList extends StatelessWidget{ final List<Product> products; ProductList({Key key,@required this.products}):super(key:key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title:Text('商品列表')), body:ListView.builder( itemCount:products.length, itemBuilder: (context,index){ return ListTile( title:Text(products[index].title), onTap:(){ } ); }, ) ); }}
先接受了主方法传递过来的参数,接受后用ListView.builder
方法,作了一个根据传递参数数据形成的动态列表
我们还是使用Navigator
组件,然后使用路由MaterialPageRoute
传递参数,具体代码如下。
Navigator.push( context, MaterialPageRoute( builder:(context)=>new ProductDetail(product:products[index]) ));
现在需要声明ProductDetail
这个类(组件),先要作的就是接受参数,具体代码如下。
class ProductDetail extends StatelessWidget { final Product product; ProductDetail({Key key ,@required this.product}):super(key:key); @override Widget build(BuildContext context) { return new Scaffold( appBar: AppBar( title:Text('${product.title}'), ), body:Center(child: Text('${product.description}'),) ); }}
为了更好的帮助大家学习,我把所有的传递参数和接受参数的代码附在了下面。
import 'package:flutter/material.dart';//传递的数据结构,也可以理解为对商品数据的抽象class Product{ final String title; //商品标题 final String description; //商品描述 Product(this.title,this.description);}void main(){ runApp(MaterialApp( title:'数据传递案例', home:ProductList( products:List.generate( 20, (i)=>Product('商品 $i','这是一个商品详情,编号为:$i') ), ) ));}class ProductList extends StatelessWidget{ final List<Product> products; ProductList({Key key,@required this.products}):super(key:key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title:Text('商品列表')), body:ListView.builder( itemCount:products.length, itemBuilder: (context,index){ return ListTile( title:Text(products[index].title), onTap:(){ Navigator.push( context, MaterialPageRoute( builder:(context)=>new ProductDetail(product:products[index]) ) ); } ); }, ) ); }}class ProductDetail extends StatelessWidget { final Product product; ProductDetail({Key key ,@required this.product}):super(key:key); @override Widget build(BuildContext context) { return new Scaffold( appBar: AppBar( title:Text('${product.title}'), ), body:Center(child: Text('${product.description}'),) ); }}
这节课学一下页面跳转后,这节课学一下页面跳转后,当我们返回页面时返回结果到上一个页面(也就是父页面)。这样的场景经常用于,我们去子页面选择了一项选项,然后把选择的结果返回给父级页面。
Dart中的异步请求和等待和ES6中的方法很像,直接使用async...await就可以实现。比如下面作了一个找小姐姐的方法,然后进行跳转,注意这时候是异步的。等待结果回来之后,我们再显示出来内容。具体代码如下
_navigateToXiaoJieJie(BuildContext context) async{ //async是启用异步方法 final result = await Navigator.push(//等待 context, MaterialPageRoute(builder: (context)=> XiaoJieJie()) ); Scaffold.of(context).showSnackBar(SnackBar(content:Text('$result'))); }}
SnackBar
是用户操作后,显示提示信息的一个控件,类似Tost
,会自动隐藏。SnackBar
是以Scaffold
的showSnackBar
方法来进行显示的。
Scaffold.of(context).showSnackBar(SnackBar(content:Text('$result')));
返回数据其实是特别容易的,只要在返回时带第二个参数就可以了。
Navigator.pop(context,'xxxx'); //xxx就是返回的参数
import 'package:flutter/material.dart';void main(){ runApp(MaterialApp( title:'页面跳转返回数据', home:FirstPage() ));}class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar:AppBar(title:Text("找小姐姐要电话")), body:Center( child: RouteButton(), ) ); }}//跳转的Buttonclass RouteButton extends StatelessWidget { @override Widget build(BuildContext context) { return RaisedButton( onPressed:(){ _navigateToXiaoJieJie(context); }, child: Text('去找小姐姐'), ); } _navigateToXiaoJieJie(BuildContext context) async{ //async是启用异步方法 final result = await Navigator.push(//等待 context, MaterialPageRoute(builder: (context)=> XiaoJieJie()) ); Scaffold.of(context).showSnackBar(SnackBar(content:Text('$result'))); }}class XiaoJieJie extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar:AppBar( title:Text('我是小姐姐') ), body:Center( child:Column( children: <Widget>[ RaisedButton( child: Text('大长腿小姐姐'), onPressed: (){ Navigator.pop(context,'大长腿:1511008888'); }, ) , RaisedButton( child: Text('小蛮腰小姐姐'), onPressed: (){ Navigator.pop(context,'大长腿:1511009999'); }, ) , ], ) ) , ); }}
如果想配置项目资源文件,就需要使用pubspec.yaml
文件,需要把资源文件在这里声明。
比如在项目根目录下新建了一个images
文件夹,文件夹下面放了一个图片,图片的名称叫做blogtouxiang.jpg
,那我们在pubspec.yaml
文件里就要写如下代码进行声明。
assets: - images/blogtouxiang.jpg
有了声明后,我们就可以直接在项目中引用这个文件了。这里使用最简单的代码结构,只用了一张图片。代码如下:
import 'package:flutter/material.dart';void main()=>runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return Container( child: Image.asset('images/blogtouxiang.jpg'), ); }}
到现在为止学Android客户端如何打包apk。
想配置APP的图片,你需要找到下面的目录:
项目根目录/android/app/src/main/res/
进入之后你会看到很多mipmap-为前缀命名的文件夹,后边的是像素密度,可以看出图标的分辨率。
- mdpi (中) ~160dpi
- hdpi (高) ~240dip
- xhdpi (超高) ~320dip
- xxhdpi (超超高) ~480dip
- xxxhdpi (超超超高) ~640dip
将对应像素密度的图片放入对应的文件夹中,图片记得用png格式,记得名字要统一,才能一次性进行配置。
这个文件主要用来配置APP的名称、图标和系统权限,所在的目录在:
项目根目录/android/app/src/main/AndroidManifest.xml
android:label="flutter_app" //配置APP的名称,支持中文android:icon="@mipmap/ic_launcher" //APP图标的文件名称
这里的坑挺多的
使用这个命令:这时候可以用下面的命令找到keytool.exe的位置。
flutter doctor -v
这时候你直接拷贝命令并进行输入,但这里也有个坑,就是如果文件夹中间带有空空,你需要用带引号扩上。
D:\Program\Android\'Android Studio'\jre\bin\keytool -genkey -v -keystore D:\key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
这时候就可以创建成功了。你的D盘下面就会有一个Jks的文件,记住这个文件不能共享给任何人。
有了这个key.jks文件后,可以到项目目录下的android
文件夹下,创建一个名为key.properties的文件,并打开粘贴下面的代码
storePassword=<password from previous step> //输入上一步创建KEY时输入的 密钥库 密码keyPassword=<password from previous step> //输入上一步创建KEY时输入的 密钥 密码keyAlias=keystoreFile=<E:/key.jks> //key.jks的存放路径
我的文件最后是这样的:
storePassword=123123keyPassword=123123keyAlias=keystoreFile=D:/key.jks
key生成好后,需要在build.gradle文件中进行配置。这个过程其实很简单,就是粘贴复制一些东西,你是不需要知道这些文件的具体用处的。
第一项:
进入项目目录的/android/app/build.gradle文件,在android{
这一行前面,加入如下代码:
def keystorePropertiesFile = rootProject.file("key.properties")def keystoreProperties = new Properties()keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
把如下代码进行替换
buildTypes { release { signingConfig signingConfigs.debug }}
替换成的代码:
signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] }}buildTypes { release { signingConfig signingConfigs.release }}
生成apk
直接在终端中输入:
flutter build apk
这时候就打包成功了
如何进行真机调试:
使用数据线连接电脑,打开开发者模式,USB调试模式,此时Android Studio会自动识别,使用flutter devices 查看是否有真机存在,有的时候可能是驱动没有安装成功,在手机上安装一个豌豆荚即可,如果正确连接真机,此时运行flutter run 等待完全运行成功之后,app就安装到手机上了。
20个flutter实例
底部导航栏制作
app必备之功能:
首先我们先写一个主入口文件,这个文件只是简单的APP通用结构,最主要的是要引入自定义的BottomNavigationWidget
组件。
import 'package:flutter/material.dart';import 'bottom_navigation_widget.dart';void main()=> runApp(new MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title:'Flutter bottomNavigationBar', theme:ThemeData.light(), home:BottomNavigationWidget() ); }.}
此时BottomNaivgationWidget组件还没编写,此时会报错
在编写BottomNaivgationWidget
组件前,我们需要简单了解一下什么是StatefulWidget
. StatefulWidget
具有可变状态(state)的窗口组件(widget)。使用这个要根据变化状态,调整State值。
在lib目录下,新建一个bottom_navigation_widget.dart
文件
它的初始化和以前使用的StatelessWidget
不同,我们在VSCode中直接使用快捷方式生成代码(直接在VSCode中输入stful):
class name extends StatefulWidget { _nameState createState() => _nameState();}class _nameState extends State<name> { @override Widget build(BuildContext context) { return Container( child: child, ); }}
上面的代码可以清楚的看到,使用StatefulWidget
分为两个部分,第一个部分是继承与StatefullWidget
,第二个部分是继承于State
.其实State
部分才是我们的重点,主要的代码都会写在State
中。
接下来我们就要创建BottomNaivgationWidget
这个Widget了,只是建立一个底部导航。
import 'package:flutter/material.dart';class BottomNavigationWidget extends StatefulWidget { _BottomNavigationWidgetState createState() => _BottomNavigationWidgetState();}class _BottomNavigationWidgetState extends State<BottomNavigationWidget> { final _BottomNavigationColor = Colors.blue; @override Widget build(BuildContext context) { return Scaffold( bottomNavigationBar: BottomNavigationBar( items: [ BottomNavigationBarItem( icon:Icon( Icons.home, color:_BottomNavigationColor, ), title:Text( 'Home', style:TextStyle(color:_BottomNavigationColor) ) ), BottomNavigationBarItem( icon:Icon( Icons.email, color:_BottomNavigationColor, ), title:Text( 'Email', style:TextStyle(color:_BottomNavigationColor) ) ), BottomNavigationBarItem( icon:Icon( Icons.pages, color:_BottomNavigationColor, ), title:Text( 'Pages', style:TextStyle(color:_BottomNavigationColor) ) ), BottomNavigationBarItem( icon:Icon( Icons.airplay, color:_BottomNavigationColor, ), title:Text( 'AipPlay', style:TextStyle(color:_BottomNavigationColor) ) ), ], type:BottomNavigationBarType.fixed ), ); }}
此时,运行flutter run 进行查看,此时在底部已经出现导航栏了,但是点击还没有效果,下面我们继续来实现
子页面我们就采用最简单的编写了,只放入一个AppBar
和一个Center
,然后用Text Widget表明即可
先来写一个HomeScreen组件,新建一个pages目录,然后在目录下面新建home_screen.dart
文件
import 'package:flutter/material.dart';class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar:AppBar( title: Text('HOME'), ), body:Center( child: Text('HOME'), ) ); }}
有了这个文件剩下的文件就可以复制粘贴,然后改少量的代码来完成了。
分别建立:
- email_screen.dart
- pages_screen.dart
- airplay_screen.dart
这些都是导航要用的子页面,有了这些页面,我们才能继续编写代码。
我们要重新initState()
方法,把刚才做好的页面进行初始化到一个Widget数组中。有了数组就可以根据数组的索引来切换不同的页面了。这是现在几乎所有的APP采用的方式。
代码如下:
List<Widget> list = List(); @override void initState(){ list ..add(HomeScreen()) ..add(EmailScreen()) ..add(PagesScreen()) ..add(AirplayScreen()); super.initState(); }
这里的..add()
是Dart语言的..语法,如果你学过编程模式,你一定听说过建造者模式,简单来说就是返回调用者本身。这里list后用了..add(),还会返回list,然后就一直使用..语法,能一直想list里增加widget元素。 最后我们调用了一些父类的initState()
方法。
BottomNavigationBar
组件里提供了一个相应事件onTap
,这个事件自带一个索引值index
,通过索引值我们就可以和我们list里的索引值相对应了。
onTap:(int index){ setState((){ _currentIndex= index; }); },
现在给出全部的bottom_navigation_widget.dart
的全部代码,讲解我会在视频中一行一行讲解。
import 'package:flutter/material.dart';import 'pages/home_screen.dart';import 'pages/email_screen.dart';import 'pages/pages_screen.dart';import 'pages/airplay_screen.dart';class BottomNavigationWidget extends StatefulWidget { _BottomNavigationWidgetState createState() => _BottomNavigationWidgetState();}class _BottomNavigationWidgetState extends State<BottomNavigationWidget> { final _BottomNavigationColor = Colors.blue; int _currentIndex = 0; List<Widget> list = List(); @override void initState(){ list ..add(HomeScreen()) ..add(EmailScreen()) ..add(PagesScreen()) ..add(AirplayScreen()); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( body: list[_currentIndex], bottomNavigationBar: BottomNavigationBar( items: [ BottomNavigationBarItem( icon:Icon( Icons.home, color:_BottomNavigationColor, ), title:Text( 'Home', style:TextStyle(color:_BottomNavigationColor) ) ), BottomNavigationBarItem( icon:Icon( Icons.email, color:_BottomNavigationColor, ), title:Text( 'Email', style:TextStyle(color:_BottomNavigationColor) ) ), BottomNavigationBarItem( icon:Icon( Icons.pages, color:_BottomNavigationColor, ), title:Text( 'Pages', style:TextStyle(color:_BottomNavigationColor) ) ), BottomNavigationBarItem( icon:Icon( Icons.airplay, color:_BottomNavigationColor, ), title:Text( 'AipPlay', style:TextStyle(color:_BottomNavigationColor) ) ), ], currentIndex:_currentIndex, onTap:(int index){ setState((){ _currentIndex= index; }); }, type:BottomNavigationBarType.fixed ), ); }}
大部分的底部导航都是中规中矩的,但有些时候也需要突出个性,比如在中间部位增加一个突出的按钮
Flutter支持自定义主题,如果使用自定义主题,设置的内容项是非常多的,这可能让初学者头疼,Flutter贴心的为给我们准备了主题样本。
primarySwatch :现在支持18种主题样本了。
theme: ThemeData( primarySwatch: Colors.lightBlue,),
会了这个知识后,我们就可以先把我们的主入口文件编写一下了,具体代码如下:
import 'package:flutter/material.dart';import 'bottom_appBar_demo.dart';void main()=>runApp(new MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title:'Flutter Demo',theme: ThemeData( primarySwatch: Colors.lightBlue,), home:BottomAppBarDemo(), ); }}
这时候bottom_appBar_demo.dart
这个文件还没有,也找不到,这个文件是我们的主要文件,我们的主要业务逻辑会写在这个文件里。
floatingActionButton
工作中我们通常简称它为“FAB”,也许只是我们公司这样称呼,从字面理解可以看出,它是“可交互的浮动按钮”,其实在Flutter默认生成的代码中就有这家伙,只是我们没有正式的接触。
一般来说,它是一个圆形,中间放着图标,会优先显示在其他Widget的前面。
下面我们来看看它的常用属性:
onPressed :点击相应事件,最常用的一个属性。
tooltip:长按显示的提示文字,因为一般只放一个图标在上面,防止用户不知道,当我们点击长按时就会出现一段文字性解释。非常友好,不妨碍整体布局。
child :放置子元素,一般放置Icon Widget。
我们来看一下floatingActionButton
的主要代码:
floatingActionButton: FloatingActionButton( onPressed: (){ Navigator.of(context).push(MaterialPageRoute(builder:(BuildContext context){ return EachView('New Page'); })); }, tooltip: 'Increment', child: Icon( Icons.add, color: Colors.white, ), ),
写完这些代码已经有了一个悬浮的按钮,但这个悬浮按钮还没有和低栏进行融合,这时候需要一个属性。
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
此时就和底部导航融合为一体了
BottomAppBar
是 底部工具栏的意思,这个要比BottomNavigationBar
widget灵活很多,可以放置文字和图标,当然也可以放置容器。
BottomAppBar
的常用属性:
- color:这个不用多说,底部工具栏的颜色。
- shape:设置底栏的形状,一般使用这个都是为了和
floatingActionButton
融合,所以使用的值都是CircularNotchedRectangle(),有缺口的圆形矩形。 - child : 里边可以放置大部分Widget,让我们随心所欲的设计底栏。
import 'package:flutter/material.dart';class BottomAppBarDemo extends StatefulWidget { _BottomAppBarDemoState createState() => _BottomAppBarDemoState();}class _BottomAppBarDemoState extends State<BottomAppBarDemo> { @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: (){ }, tooltip: 'Increment', child: Icon( Icons.add, color: Colors.white, ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, bottomNavigationBar: BottomAppBar( color:Colors.lightBlue, shape:CircularNotchedRectangle(), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ IconButton( icon:Icon(Icons.home), color:Colors.white, onPressed:(){ } ), IconButton( icon:Icon(Icons.airport_shuttle), color:Colors.white, onPressed:(){ } ), ], ), ) , ); }}
截止到上面的代码,基本功能都已经实现,但是还没有交互效果
在前两节课的实例中我们使用了子页面,但子页面继承与StatelessWidget
(不可变控件),所以很麻烦的写了4个页面,其实完全可以写一个继承于StatefulWidget
的控件,进行搞定。
新建一个each_view.dart
文件,然后输入如下代码:
import 'package:flutter/material.dart';class EachView extends StatefulWidget { String _title; EachView(this._title); @override _EachViewState createState() => _EachViewState();}class _EachViewState extends State<EachView> { @override Widget build(BuildContext context) { return Scaffold( appBar:AppBar(title:Text(widget._title)), body: Center(child:Text(widget._title)), ); }}
代码中设置了一个内部的_title
变量,这个变量是从主页面传递过来的,然后根据传递过来的具体值显示在APP的标题栏和屏幕中间。
这些效果都是在bottom_appBar_demo.dart
页面完成的。首先我们需要引入新作的子页面each_view.dart
。
import 'each_view.dart';
新建两个变量,主要作用是控制body中的视图,也就是显示不同的子页面。
List<Widget> _eachView; //创建视图数组 int _index = 0; //数组索引,通过改变索引值改变视图
下一步是为_eachView
进行初始化赋值,我们可以直接重写初始化方法,具体代码如下:
@override void initState() { // TODO: implement initState super.initState(); _eachView = List(); _eachView..add(EachView('Home'))..add(EachView('Me')); }
剩下的就是写个个按钮的交互事件,交互的动作分两种:
直接打开子导航,比如我们点击了中间的” “按钮,我们直接开启子页面。
onPressed: (){ Navigator.of(context).push(MaterialPageRoute(builder:(BuildContext context){ return EachView('New Page'); }));},
- 改变状态,通过改变状态,来切换页面,这样我们整体页面并没有被刷新。
onPressed:(){setState(() { _index=0; });}
bottom_appBar_demo.dart
所有代码,代码如下:
import 'package:flutter/material.dart';import 'each_view.dart';class BottomAppBarDemo extends StatefulWidget { _BottomAppBarDemoState createState() => _BottomAppBarDemoState();}class _BottomAppBarDemoState extends State<BottomAppBarDemo> { List<Widget> _eachView; //创建视图数组 int _index = 0; //数组索引,通过改变索引值改变视图 @override void initState() { // TODO: implement initState super.initState(); _eachView = List(); _eachView..add(EachView('Home'))..add(EachView('Me')); } @override Widget build(BuildContext context) { return Scaffold( body:_eachView[_index], floatingActionButton: FloatingActionButton( onPressed: (){ Navigator.of(context).push(MaterialPageRoute(builder:(BuildContext context){ return EachView('New Page'); })); }, tooltip: 'Increment', child: Icon( Icons.add, color: Colors.white, ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, bottomNavigationBar: BottomAppBar( color:Colors.lightBlue, shape:CircularNotchedRectangle(), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ IconButton( icon:Icon(Icons.home), color:Colors.white, onPressed:(){ setState(() { _index=0; }); } ), IconButton( icon:Icon(Icons.airport_shuttle), color:Colors.white, onPressed:(){ setState(() { _index=1; }); } ), ], ), ) , ); }}
现在Flutter的路由效果已经非常不错了,能满足大部分App的需求,但是谁不希望自己的App更酷更炫那,学完这节课你就可以给自己的APP加上酷炫的路由动画了。
其实路由动画的原理很简单,就是重写并继承PageRouterBuilder
这个类里的transitionsBuilder
方法。不过这个方法还是有很多写法的,通过写法的不同,产生的动画效果也有所不同。
先编写一个主入口方法,还是最简单的格式,只不过home属性,使用的FirstPage
的组件是我们自定义的,需要我们再次编写。入口文件的代码如下:
import 'package:flutter/material.dart';import 'pages.dart';void main()=>runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title:'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home:FirstPage() ); }}
主入口文件用import
引入了pages.dart
文件,这个文件就是生成了两个页面(Flutter里的页面也是Widget,这个你要跟网页区分开)。有了两个页面就可以实现路由跳转了。
pages.dart
文件的代码如下,这里我们先用普通路由代替,看一看效果。
import 'package:flutter/material.dart';class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.blue, appBar:AppBar( title:Text('FirstPage',style: TextStyle(fontSize: 36.0)), elevation: 0.0, ), body:Center( child: MaterialButton( child: Icon( Icons.navigate_next, color:Colors.white, size:64.0, ), onPressed: (){ Navigator.of(context).push( MaterialPageRoute( builder:(BuildContext context){ return SecondPage(); })); }, ), ) ); }}class SecondPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.pinkAccent, appBar: AppBar( title: Text('SecondPage',style:TextStyle(fontSize:36.0),), backgroundColor: Colors.pinkAccent, leading:Container(), elevation: 0.0, ), body:Center( child: MaterialButton( child: Icon( Icons.navigate_before, color:Colors.white, size:64.0 ), onPressed: ()=>Navigator.of(context).pop(), ), ) ); }}
上面代码中有一个新知识点,需要学习一下:
- AppBar Widger的 elevation 属性:这个值是AppBar 滚动时的融合程度,一般有滚动时默认是4.0,现在我们设置成0.0,就是和也main完全融合了。
写完这个页面代码后,已经可以进行最基本的导航了,但是并没有什么酷炫的动画。
新建一个custome_router.dart
文件,这个就是要自定义的路由方法,自定义首先要继承于通用的路由的构造器类PageRouterBuilder
。继承之后重写父类的CustomRoute
构造方法。
构造方法可以简单理解为:只要一调用这个类或者说是Widget,构造方法里的所有代码就执行了。
custome_router.dart代码详情
import 'package:flutter/material.dart';class CustomRoute extends PageRouteBuilder{ final Widget widget; CustomRoute(this.widget) :super( transitionDuration:const Duration(seconds:1), pageBuilder:( BuildContext context, Animation<double> animation1, Animation<double> animation2){ return widget; }, transitionsBuilder:( BuildContext context, Animation<double> animation1, Animation<double> animation2, Widget child){ return FadeTransition( opacity: Tween(begin:0.0,end :1.0).animate(CurvedAnimation( parent:animation1, curve:Curves.fastOutSlowIn )), child: child, ); } ); }
- FadeTransition:渐隐渐现过渡效果,主要设置opactiy(透明度)属性,值是0.0-1.0。
animate :动画的样式,一般使用动画曲线组件(CurvedAnimation)。
curve: 设置动画的节奏,也就是常说的曲线,Flutter准备了很多节奏,通过改变动画取消可以做出很多不同的效果。
transitionDuration:设置动画持续的时间,建议再1和2之间。
写完代码,我们已经可以看到在切换路由时有了动画效果,
return ScaleTransition( scale:Tween(begin:0.0,end:1.0).animate(CurvedAnimation( parent:animation1, curve: Curves.fastOutSlowIn )), child:child);
旋转 缩放的思路是在一个路由动画里的child属性里再加入一个动画,让两个动画同时执行。来看详细代码:
return RotationTransition( turns:Tween(begin:0.0,end:1.0) .animate(CurvedAnimation( parent: animation1, curve: Curves.fastOutSlowIn )), child:ScaleTransition( scale:Tween(begin: 0.0,end:1.0) .animate(CurvedAnimation( parent: animation1, curve:Curves.fastOutSlowIn )), child: child, ));
其实用的做多的还是左右滑动路由动画,其实实现起来也是非常简单,直接使用SlideTransition
就可以了。
// 幻灯片路由动画return SlideTransition( position: Tween<Offset>( begin: Offset(-1.0, 0.0), end:Offset(0.0, 0.0) ) .animate(CurvedAnimation( parent: animation1, curve: Curves.fastOutSlowIn )), child: child,);
总结:动画的使用会让我们的APP更加酷炫,也会让别人觉的你不是一个新手,再Flutter里使用动画是非常方便的,所以你可以把这些动画效果事先写好,在工作中直接使用。
Flutter的Fliter Widget 也是非常强大的,它可以制作出你想要的神奇滤镜效果。这节我们就以实战的方式,制作一个毛玻璃效果,通过实例来学习Fitler 的用法
这个和以前的写法都一样,所以就直接贴代码了。详细解释会在视频中解说,不过我相信不解释,小伙伴也一定可以看明白
import 'package:flutter/material.dart';import 'frosted_glass_emo.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title:'Flutter Demo', theme:ThemeData( primarySwatch: Colors.blue, ), home:Scaffold( body:FrostedGlassDemo(), ) ); }}
BackdropFilter
就是背景滤镜组件,使用它可以给父元素增加滤镜效果,它里边最重要的一个属性是filter
。 filter
属性中要添加一个滤镜组件,实例中我们添加了图片滤镜组件,并给了模糊效果。
我们新建一个frosted_glass_demo.dart
的文件,然后写入下面的代码,具体的解释已经写到了代码的注释中。
import 'package:flutter/material.dart';import 'dart:ui'; //引入ui库,因为ImageFilter Widget在这个里边。class FrostedGlassDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body:Stack( //重叠的Stack Widget,实现重贴 children: <Widget>[ ConstrainedBox( //约束盒子组件,添加额外的限制条件到 child上。 constraints: const BoxConstraints.expand(), //限制条件,可扩展的。 child:Image.network('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1545738629147&di=22e12a65bbc6c4123ae5596e24dbc5d3&imgtype=0&src=http://pic30.photophoto.cn/20140309/0034034413812339_b.jpg') ), Center( child: ClipRect( //裁切长方形 child: BackdropFilter( //背景滤镜器 filter: ImageFilter.blur(sigmaX: 5.0,sigmaY: 5.0), //图片模糊过滤,横向竖向都设置5.0 child: Opacity( //透明控件 opacity: 0.5, child: Container(// 容器组件 width: 500.0, height: 700.0, decoration: BoxDecoration(color:Colors.grey.shade200), //盒子装饰器,进行装饰,设置颜色为灰色 child: Center( child: Text( 'JSPang', style: Theme.of(context).textTheme.display3, //设置比较酷炫的字体 ), ), ), ), ), ), ) ], ) ); }}
这个代码嵌套很多,所以一定要注意你的代码层次,容易出错的地方就是嵌套错误。这个效果尽量少用,因为我测试了一下,它对系统的消耗还是比较大的。
在工作中切换页面时,再切换回来,时要求页面状态不发生改变的。这能给APP浏览者最好的体验,机会所有的APP都有这个需求,属于一个大众需求。这节课我们就来看看这样的效果如何实现
with是dart的关键字,意思是混入的意思,就是说可以将一个或者多个类的功能添加到自己的类无需继承这些类, 避免多重继承导致的问题。
class _KeepAliveDemoState extends State<KeepAliveDemo> with SingleTickerProviderStateMixin {}
需要注意的是with后边是Mixin,而不是普通的Widget,这个初学者比较爱犯错误。需要强调一下。
TabBar
是切换组件,它需要设置两个属性。
- controller: 控制器,后边跟的多是
TabController
组件。 - tabs:具体切换项,是一个数组,里边放的也是Tab Widget。
bottom:TabBar( controller: _controller, tabs:[ Tab(icon:Icon(Icons.directions_car)), Tab(icon:Icon(Icons.directions_transit)), Tab(icon:Icon(Icons.directions_bike)), ],)
我们先把入口页面布局好,下节课我们再让他保持状态。学了上面两个知识,你其实可以做出来布局了。全部代码如下:
import 'package:flutter/material.dart';void main()=>runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme:ThemeData( primarySwatch: Colors.blue, ), home:KeepAliveDemo() ); }} class KeepAliveDemo extends StatefulWidget { _KeepAliveDemoState createState() => _KeepAliveDemoState();}/*with是dart的关键字,意思是混入的意思,就是说可以将一个或者多个类的功能添加到自己的类无需继承这些类,避免多重继承导致的问题。SingleTickerProviderStateMixin 主要是我们初始化TabController时,需要用到vsync ,垂直属性,然后传递this*/class _KeepAliveDemoState extends State<KeepAliveDemo> with SingleTickerProviderStateMixin { TabController _controller; @override void initState(){ super.initState(); _controller = TabController(length:3, vsync: this); } //重写被释放的方法,只释放TabController @override void dispose(){ _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar:AppBar( title:Text('Keep Alive Demo'), bottom:TabBar( controller: _controller, tabs:[ Tab(icon:Icon(Icons.directions_car)), Tab(icon:Icon(Icons.directions_transit)), Tab(icon:Icon(Icons.directions_bike)), ], ) ), body:TabBarView( controller: _controller, children: <Widget>[ Text('1111'), Text('2222'), Text('3333') ], ) ); }}
下面我们来实现页面保持状态
那为了能看出是保持状态的,我们作一个按钮,然后点一下加一。就跟Flutter为我们生成的例子一样。
实现功能的全部代码:
import 'package:flutter/material.dart';class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState();}//混入AutomaticKeepAliveClientMixin,这是保持状态的关键//然后重写wantKeppAlive 的值为true。class _MyHomePageState extends State<MyHomePage> with AutomaticKeepAliveClientMixin { int _counter = 0; //重写keepAlive 为ture ,就是可以有记忆功能了。 @override bool get wantKeepAlive => true; //声明一个内部方法,用来点击按钮后增加数量 void _incrementCounter(){ setState((){ _counter ;}); } @override Widget build(BuildContext context) { return Scaffold( body:Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('点一下加1,点一下加1:'), Text( '$_counter', style:Theme.of(context).textTheme.display1, ) ], ), ), //增加一个悬浮按钮,点击时触犯_incrementCounter方法 floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); }}
写完这个Widget,然后在入口文件中引入。
import 'keep_alive_demo.dart';
然后把Body区域改成我们刚写的MyHomePage Widget 就可以了,注意是改三个。
搜索这个功能,大部分APP都会存在,这节课我们就学习一下,如何做一个有提示功能,而且交互很好的搜索条
这个还是继承StatelessWidget
,然后在home属性中加入SearchBarDemo
,这是一个自定义的Widget,主要代码都在这个文件中。
main.dart
文件的代码如下:
import 'package:flutter/material.dart';import 'search_bar_demo.dart';void main() =>runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title:'Flutter Demo', theme: ThemeData.light(), home: SearchBarDemo() ); }}
asset.dart
相当于数据文件,工作中这些数据是后台传递给我们,或者写成配置文件的,这里我们就以List的方式代替了。我们在这个文件中定义了两个List:
searchList : 这个相当于数据库中的数据,我们要在这里进行搜索。
recentSuggest : 目前的推荐数据,就是搜索时,自动为我们进行推荐。
整体代码如下 :
const searchList = [ "jiejie-大长腿", "jiejie-水蛇腰", "gege1-帅气欧巴", "gege2-小鲜肉"];const recentSuggest = [ "推荐-1", "推荐-2"];
这节课我们先把第一个搜索界面布好,下节课我们主要作搜索的交互效果。看下面的代码:
import 'package:flutter/material.dart';import 'asset.dart';class SearchBarDemo extends StatefulWidget { _SearchBarDemoState createState() => _SearchBarDemoState();}class _SearchBarDemoState extends State<SearchBarDemo> { @override Widget build(BuildContext context) { return Scaffold( appBar:AppBar( title:Text('SearchBarDemo'), actions:<Widget>[ IconButton( icon:Icon(Icons.search), onPressed: (){ print('开始搜索'); } ), ] ) ); }}
这时候就可以在虚拟机中进行预览了,但是这时候点击搜索按钮还没有任何反应。
现在我们来实现点击搜索图标后,变成搜索条的样式,并且有一定的交互效果。
在点击图标时执行searchBarDelegate
类,这个类继承与SearchDelegate
类,继承后要重写里边的四个方法。
buildActions
方法时搜索条右侧的按钮执行方法,我们在这里方法里放入一个clear图标。 当点击图片时,清空搜索的内容。
代码如下:
@override List<Widget> buildActions(BuildContext context){ return [ IconButton( icon:Icon(Icons.clear), onPressed: ()=>query = "",) ]; }
这个时搜索栏左侧的图标和功能的编写,这里我们才用AnimatedIcon
,然后在点击时关闭整个搜索页面,代码如下。
@override Widget buildLeading(BuildContext context) { return IconButton( icon: AnimatedIcon( icon: AnimatedIcons.menu_arrow, progress: transitionAnimation), onPressed: () => close(context, null)); }
buildResults
方法,是搜到到内容后的展现,因为我们的数据都是模拟的,所以我这里就使用最简单的Container
Card
组件进行演示了,不做过多的花式修饰了。
@override Widget buildResults(BuildContext context) { return Container( width: 100.0, height: 100.0, child: Card( color: Colors.redAccent, child: Center( child: Text(query), ), ), ); }
这个方法主要的作用就是设置推荐,就是我们输入一个字,然后自动为我们推送相关的搜索结果,这样的体验是非常好的。
具体代码如下:
@override Widget buildSuggestions(BuildContext context) { final suggestionList = query.isEmpty ? recentSuggest : searchList.where((input) => input.startsWith(query)).toList(); return ListView.builder( itemCount: suggestionList.length, itemBuilder: (context, index) => ListTile( title: RichText( text: TextSpan( text: suggestionList[index].substring(0, query.length), style: TextStyle( color: Colors.black, fontWeight: FontWeight.bold), children: [ TextSpan( text: suggestionList[index].substring(query.length), style: TextStyle(color: Colors.grey)) ])), )); }}
为了方便小伙伴们学习,给出所有search_bar_demo.dart
文件的代码:
import 'package:flutter/material.dart';import 'asset.dart';class SearchBarDemo extends StatefulWidget { _SearchBarDemoState createState() => _SearchBarDemoState();}class _SearchBarDemoState extends State<SearchBarDemo> { @override Widget build(BuildContext context) { return Scaffold( appBar:AppBar( title:Text('SearchBarDemo'), actions:<Widget>[ IconButton( icon:Icon(Icons.search), onPressed: (){ showSearch(context:context,delegate: searchBarDelegate()); } // showSearch(context:context,delegate: searchBarDelegate()), ), ] ) ); }}class searchBarDelegate extends SearchDelegate<String>{ @override List<Widget> buildActions(BuildContext context){ return [ IconButton( icon:Icon(Icons.clear), onPressed: ()=>query = "",) ]; } @override Widget buildLeading(BuildContext context) { return IconButton( icon: AnimatedIcon( icon: AnimatedIcons.menu_arrow, progress: transitionAnimation), onPressed: () => close(context, null)); } @override Widget buildResults(BuildContext context) { return Container( width: 100.0, height: 100.0, child: Card( color: Colors.redAccent, child: Center( child: Text(query), ), ), ); } @override Widget buildSuggestions(BuildContext context) { final suggestionList = query.isEmpty ? recentSuggest : searchList.where((input) => input.startsWith(query)).toList(); return ListView.builder( itemCount: suggestionList.length, itemBuilder: (context, index) => ListTile( title: RichText( text: TextSpan( text: suggestionList[index].substring(0, query.length), style: TextStyle( color: Colors.black, fontWeight: FontWeight.bold), children: [ TextSpan( text: suggestionList[index].substring(query.length), style: TextStyle(color: Colors.grey)) ])), )); }}
这节已一个模拟添加多张照片的小实例,主要学习一下流式布局在Flutter里的应用。如果你作为一个前端开发者,那这节课的内容将非常容易。
使用meidaQuery
可以很容易的得到屏幕的宽和高,得到宽和高的代码如下:
final width = MediaQuery.of(context).size.width;final height = MediaQuery.of(context).size.height;
Flutter中流式布局大概有三种常用方法,这节课先学一下Wrap的流式布局。有的小伙伴会说Wrap中的流式布局可以用Flow很轻松的实现出来,但是Wrap更多的式在使用了Flex中的一些概念,某种意义上说式跟Row、Column更加相似的。
单行的Wrap跟Row表现几乎一致,单列的Wrap则跟Column表现几乎一致。但Row与Column都是单行单列的,Wrap则突破了这个限制,mainAxis上空间不足时,则向crossAxis上去扩展显示。
从效率上讲,Flow肯定会比Wrap高,但Wrap使用起来会更方便一些。
这个会在实例中用到,所以,我在实例中会讲解这个代码。
GestureDetector
它式一个Widget,但没有任何的显示功能,而只是一个手势操作,用来触发事件的。虽然很多Button组件是有触发事件的,比如点击,但是也有一些组件是没有触发事件的,比如:Padding、Container、Center这时候我们想让它有触发事件就需要再它们的外层增加一个GestureDetector
,比如我们让Padding有触发事件,代码如下:
Widget buildAddButton(){ return GestureDetector( onTap:(){ if(list.length<9){ setState(() { list.insert(list.length-1,buildPhoto()); }); } }, child: Padding( padding:const EdgeInsets.all(8.0), child: Container( width: 80.0, height: 80.0, color: Colors.black54, child: Icon(Icons.add), ), ), ); }
入口文件很简单,就是引用了warp_demo.dart
文件,然后再home属性中使用了WarpDemo
,代码如下:
import 'package:flutter/material.dart';import 'warp_demo.dart';void main()=>runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: new ThemeData.dark(), home:WarpDemo() ); }}
主要的文件代码我就列在下面了
import 'package:flutter/material.dart';//继承与动态组件class WarpDemo extends StatefulWidget { _WarpDemoState createState() => _WarpDemoState();}class _WarpDemoState extends State<WarpDemo> { List<Widget> list; //声明一个list数组 @override //初始化状态,给list添加值,这时候调用了一个自定义方法`buildAddButton` void initState() { super.initState(); list = List<Widget>()..add(buildAddButton()); } @override Widget build(BuildContext context) { //得到屏幕的高度和宽度,用来设置Container的宽和高 final width = MediaQuery.of(context).size.width; final height = MediaQuery.of(context).size.height; return Scaffold( appBar: AppBar( title: Text('Wrap流式布局'), ), body:Center( child: Opacity( opacity: 0.8, child: Container( width: width, height: height/2, color: Colors.grey, child: Wrap( //流式布局, children: list, spacing: 26.0, //设置间距 ), ), ), ) ); } Widget buildAddButton(){ //返回一个手势Widget,只用用于显示事件 return GestureDetector( onTap:(){ if(list.length<9){ setState(() { list.insert(list.length-1,buildPhoto()); }); } }, child: Padding( padding:const EdgeInsets.all(8.0), child: Container( width: 80.0, height: 80.0, color: Colors.black54, child: Icon(Icons.add), ), ), ); } Widget buildPhoto(){ return Padding( padding: const EdgeInsets.all(8.0), child: Container( width: 80.0, height: 80.0, color: Colors.amber, child: Center( child: Text('照片'), ), ), ); }}
手机的屏幕本身就很小,所以要合理利用空间,把主要的元素展示出来,次要或者不重要的元素等用户向看的时候再给用户展示。这类操作最常见的交互就是展开和闭合了。这节课我们主要学习一下ExpansionTile
组件的使用。
ExpansionTile Widget
就是一个可以展开闭合的组件,常用的属性有如下几个。
- title:闭合时显示的标题,这个部分经常使用
Text Widget
。 - leading:标题左侧图标,多是用来修饰,让界面显得美观。
- backgroundColor: 展开时的背景颜色,当然也是有过度动画的,效果非常好。
- children: 子元素,是一个数组,可以放入多个元素。
- trailing : 右侧的箭头,你可以自行替换但是我觉的很少替换,因为谷歌已经表现的很完美了。
- initiallyExpanded: 初始状态是否展开,为true时,是展开,默认为false,是不展开。
import 'package:flutter/material.dart';import 'expansion_tile.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title:'Flutter Demo', theme: new ThemeData.dark(), home:ExpansionTileDemo() ); }}
这个文件是我们的主要学习文件,但是并不复杂,就是一个可展开组件。全部代码如下:
import 'package:flutter/material.dart';class ExpansionTileDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title:Text('expansion tile demo')), body:Center( child: ExpansionTile( title:Text('Expansion Tile'), leading:Icon(Icons.ac_unit), backgroundColor: Colors.white12, children: <Widget>[ ListTile( title:Text('list tile'), subtitle:Text('subtitle') ) ], initiallyExpanded: true, ) ) ); }}
这时候就可以预览了,效果也应该出现了。
上节课学的只是一个单个的展开闭合组件,你当然可以把这个组件作为List元素,组成一个数组,形成列表。不过Flutter也很贴心的为提供了一个ExpansionPanelList Widget,它可以实现展开闭合的列表功能。
需要注意的是这个列表必须放在可滑动组件中使用,否则会报错
- expansionCallback:点击和交互的回掉事件,有两个参数,第一个是触发动作的索引,第二个是布尔类型的触发值。
- children:列表的子元素,里边多是一个List数组。
为了方便管理制作了一个ExpandStateBean
类,里边就是两个状态,一个是是否展开isOpen
,另一个索引值。代码如下:
class ExpandStateBean{ var isOpen; var index; ExpandStateBean(this.index,this.isOpen);}
这个文件我就直接上代码了
import 'package:flutter/material.dart';class ExpansionPanelListDemo extends StatefulWidget { _ExpansionPanelListDemoState createState() => _ExpansionPanelListDemoState();}class _ExpansionPanelListDemoState extends State<ExpansionPanelListDemo> { var currentPanelIndex = -1; List<int> mList; //组成一个int类型数组,用来控制索引 List<ExpandStateBean> expandStateList; //开展开的状态列表, ExpandStateBean是自定义的类 //构造方法,调用这个类的时候自动执行 _ExpansionPanelListDemoState(){ mList = new List(); expandStateList = new List(); //便利为两个List进行赋值 for(int i=0;i<10;i ){ mList.add(i); expandStateList.add(ExpandStateBean(i,false)); } } //修改展开与闭合的内部方法 _setCurrentIndex(int index, isExpand){ setState(() { //遍历可展开状态列表 expandStateList.forEach((item){ if(item.index==index){ //取反,经典取反方法 item.isOpen = !isExpand; } }); }); } @override Widget build(BuildContext context) { return Scaffold( appBar:AppBar( title:Text("expansion panel list") ), //加入可滚动组件 body:SingleChildScrollView( child: ExpansionPanelList( //交互回掉属性,里边是个匿名函数 expansionCallback: (index,bol){ //调用内部方法 _setCurrentIndex(index, bol); }, children: mList.map((index){//进行map操作,然后用toList再次组成List return ExpansionPanel( headerBuilder: (context,isExpanded){ return ListTile( title:Text('This is No. $index') ); }, body:ListTile( title:Text('expansion no.$index') ), isExpanded: expandStateList[index].isOpen ); }).toList(), ), ) ); }}//自定义扩展状态类class ExpandStateBean{ var isOpen; var index; ExpandStateBean(this.index,this.isOpen);}
现在人们对于网站的美感要求是越来越高了,所以很多布局需要优美的曲线设计。当然最简单的办法是作一个PNG的透明图片,然后外边放一个Container
.但其内容如果本身就不是图片,只是容器,这种放入图片的做法会让包体变大。其实我们完全可以使用贝塞尔曲线进行切割。
在讲正式内容之前,先回答小伙伴们的一个问题,就是如何去掉DeBug图标。在我们进行编写代码预览时,有一Debug的图标一直在屏幕上,确实不太美观,其实只要语句代码就可以去掉的。
debugShowCheckedModeBanner: false,
这个代码要配置在主入口文件里,全部代码如下
import 'package:flutter/material.dart';import 'custom_clipper.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title:'Flutter Demo', debugShowCheckedModeBanner: false, theme:ThemeData( primarySwatch: Colors.blue, ), home:HomePage() ); }}
clipPath
控件可以把其内部的子控件切割,它有两个主要属性(参数):
- child :要切割的元素,可以是容器,图片.....
- clipper : 切割的路径,这个要和CustomClipper对象配合使用。
import 'package:flutter/material.dart';class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body:Column( children: <Widget>[ ClipPath( clipper:BottomClipper(), child: Container( color:Colors.deepPurpleAccent, height: 200.0, ), ) ], ) ); }}
在Scaffold
里放置了一个列容器,然后把ClipPath
控件放到了里边,ClipPath
的子元素是一个容器控件Container
。BootomClipper
是我们自定义的一个对象,里边主要就是切割的路径。
我们主要的贝塞尔曲线路径就写在getClip
方法里,它返回一段路径。
一个二阶的贝塞尔曲线是需要控制点和终点的,控制点就像一块磁铁,把直线吸引过去,形成一个完美的弧度,这个弧度就是贝塞尔曲线了。
我们先来熟悉一下裁切路径和贝塞尔曲线,作一个最简单的贝塞尔曲线出来。
全部代码如下:
import 'package:flutter/material.dart';class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body:Column( children: <Widget>[ ClipPath( clipper:BottomClipper(), child: Container( color:Colors.deepPurpleAccent, height: 200.0, ), ) ], ) ); }}class BottomClipperTest extends CustomClipper<Path>{ @override Path getClip(Size size) { // TODO: implement getClip var path = Path(); path.lineTo(0, 0); path.lineTo(0, size.height-30); var firstControlPoint =Offset(size.width/2,size.height); var firstEndPoint = Offset(size.width,size.height-30); path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy, firstEndPoint.dx, firstEndPoint.dy); path.lineTo(size.width, size.height-30); path.lineTo(size.width, 0); return path; } @override bool shouldReclip(CustomClipper<Path> oldClipper) { // TODO: implement shouldReclip return false; }}
上节课已经对知识点有了了解,这节课我们主要就是加大一些难度,作个更复杂的贝塞尔裁切出来。
这节课主要改造上节课的代码,作一个波浪形的贝塞尔裁切。波浪形式的只要把裁切变成两个对称的贝塞尔曲线就可以实现了。代码如下:
class BottomClipper extends CustomClipper<Path>{ @override Path getClip(Size size) { // TODO: implement getClip var path = Path(); path.lineTo(0, size.height-20); var firstControlPoint =Offset(size.width/4,size.height); var firstEndPoint = Offset(size.width/2.25,size.height-30); path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy, firstEndPoint.dx, firstEndPoint.dy); var secondControlPoint = Offset(size.width/4*3,size.height-80); var secondEndPoint = Offset(size.width,size.height-40); path.quadraticBezierTo(secondControlPoint.dx, secondControlPoint.dy, secondEndPoint.dx, secondEndPoint.dy); path.lineTo(size.width, size.height-40); path.lineTo(size.width, 0); return path; } @override bool shouldReclip(CustomClipper<Path> oldClipper) { // TODO: implement shouldReclip return false; }}
这两节课的主要内容就是如何裁切和贝塞尔曲线的原理,其实裁切还有圆形裁切、圆角裁切和矩形裁切,因为都比较容易,我就不再讲解了。
打开一个APP,经常会看到精美的启动页,这种启动页也称为闪屏动画。它是从无到有有一个透明度的渐变动画的。图像展示完事后,才跳转到用户可操作的页面。这节课主要学习一下闪屏动画的制作。
AnimationController
是Animation
的一个子类,它可以控制Animation
, 也就是说它是来控制动画的,比如说控制动画的执行时间。
我们这里有了两个参数 :
vsync:this
:垂直同步设置,使用this就可以了。duration
: 动画持续时间,这个可以使用seconds
秒,也可以使用milliseconds
毫秒,工作中经常使用毫秒,因为秒还是太粗糙了。