这是我的第一个flutter项目,我想通过项目实战来学习这一门技术,资料的地址(https://book.flutterchina.club/);
A new Flutter project.
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
For help getting started with Flutter, view our online documentation, which offers tutorials, samples, guidance on mobile development, and a full API reference.
(https://flutterchina.club/setup-windows/)
- 下载安装flutter, android studio,
- 配置插件
- 在 android studio 和 vscode 分别使用flutter创建一个空项目
使用镜像:(添加用户环境变量,flutter官方为中国开启的临时镜像,写在电脑的:系统属性>高级>用户环境变量里面)
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
- 简单学习dart语言
声明变量 函数 异步
- 以上全部只是看了一遍
- dart 官网 教程一个简单的dart程序
- 小目标,先把dart的语法接着看一部份(生成一次commit)
- (昨天没看,今天多看些!!!)
- 异步
- 看书
- dart 和java 和 js
JavaScript无疑是动态化支持最好的脚本语言 Dart既能进行服务端脚本、APP开发、web开发
- 开始flutter计数器(创建flutter项目时默认的一个组件)
看动代码
- 计数器(看懂代码)
简单理解
- 路由
- 实现路由传参
目前还没完全看懂
- yaml (的简单了解)
- route完善
- 路由传值
mainResolve 引入routerTestRoute widget, push 到 tipRoute (携带test参数) tip返回routerTestRoute 携带 参数 (如果点击tip的左上角返回按钮返回,则会返回null) 上面是非命名路由,
- 命名路由
路由表:注册路由(起名字)
Map<String, WidgetBuilder> routes
map数据 key是string类型, 表示路由名称 widgetBuilder是路由的回调函数 注册路由表 MyApp类中添加routes属性
routes: {
"new_page": (content) => NewRoute()
}
表示首页home 的路由
"/": (context) => MyHomePage(title: 'flutter Demo Home Page')
通过命名路由打开新页面 方法 Navigator.pushNameFuture pushNamed(BuildContext context, String routeName,{Object arguments})
命名路由传参 通过settings对象注册参数
(context) {
return TipRoute(text: ModalRoute.of(context).settings.arguments);
}
也可以通过settings对象获取参数(原本是)
var args=ModalRoute.of(context).settings.arguments;
路由钩子 onGenerateRoute : MaterailApp的属性 ,和routes同级 在routes中没有注册,但被navigator.pushNamed调用时会触发
- 接着包管理资料
pub仓库实例 添加 english_words 包 把english_words 添加到依赖包管理列表
- 修改yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.0
# 新添加的依赖
english_words: ^3.1.3
- 控制台: flutter packages get
如果在 android studio 编辑器,可以单继yaml右上角的package get,获取最新依赖 如果在 vs,yaml修改后自动更新
- 页面引入
import 'package:english_words/english_words.dart';
- 页面中创建组件并使用
new WordPair.random().toString()
返回一个随机字符串
- 依赖本地包
dependencies:
pkg1:
path: ../../code/pkg1
- 依赖git(包位于Git存储库的根目录中)
dependencies:
pkg1:
git:
url: git://github.com/xxx/pkg1.git
- assets
flutte 分为 code 和 assets 两部分
- 通过yaml配置flutter assets
flutter:
assets:
- assets/my_icon.png
- assets/background.png
- variant
asset 变体 flutter 的 assets 在构建过程中 会在相邻子目录中查找具有相同名称的任何文件 例入: 配置
assets/background.png
,构建时也会包含assets/dark/background.png
- 目前看variant 貌似没什么用(将来有用)
- 加载 assets
主流两种方法加载assets(文字和图片)
1 rootBundle 对象, 每一个flutter 都有一个rootBundle对象,可以获取主资源包.
package:flutter/services.dart
当前方法会暴露一个rootBundle对象 2 DefaultAssetsBundle 对象, 通过它获取AssetsBundle 推荐第二种(好像是有响应关系的) 常见用法 在组件运行上下文中使用DefaultAssetBundle.of()
间接加载assets 在组件上下文之外使用rootBundle 加载图片 AssetImage 可以根据设备像素比例引入对应的图片尺寸 (满足上述条件: 目录配置倍数分辨率图片) 1
new DecorationImage(
image: new AssetImage('xxx.png'),
),
返回一个ImageProvider不是widget,所以用DecorationImage
2
Image.asset('xxx.png')
,返回的是一个图片widget 其他 依赖包的资源 (给AssetImage配置package参数)注意:包在使用本身的资源时也应该加上package参数来获取。 加载非flutter应用资源(上面的都是flutter启动后才能用) 举例: app图标,app启动图 设置APP图标 Android开发人员指南
- 调试(总算该看点能看懂的了)
dart 分析器
flutter analyze
, 一个静态代码检查工具, 测试你的代码, (intellij 会自动启用) 不知道vs 现在这个插件有没有analyze检测 dart Observatory 单步调试和分析器 需要配置 debugger() 声明 使用上面的调试工具,代码里面写这个就可以插入断点, 需要先引入import 'dart: developer';
设置条件断点debugger(when: offset > 30.0);
还有: print, debugPrint, flutter logs, 调试动画,调试性能, debugPrint: 把当前组件状态转存dump(debugDumpApp: 转存widget树的状态)
异常捕捉
捕捉之前先看dart的运行机制 ![dart运行]](https://book.flutterchina.club/assets/img/2-12.eb7484c9.png)
app执行 先执行microtask(微任务),在执行event (事件队列) (flutter 可以通过Future.microtask(…)向微任务中添加事件) 在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其它任务执行的。 flutter 框架捕捉错误方法 try catch finally flutter 默认
catch (e, stack) {
// 有异常时则弹出错误提示
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
}
自定义捕捉错误
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details);
};
flutter 框架不捕捉的错误处理(空对象异常,flutter执行异常)
同步 通过try 异步 runZoned 理解: 类似沙箱, 影响降低 下面是组件
周日了,加了一个学flutter的新手,已经关注 名字叫 toknowmore
- 组件
- widget
不是页面展示的组件, 是一个配置数据
1 屏幕显示的是 类
element
. widget树生成element树(广义:widget树 是 ui树) 2 一个widget可以对应 个element widget 抽象类 继承DiagnosticableTree
用来 提供调试信息 key,性能优化(是否复用组件,canUpdate方法用来判断) createElement() statelessWidget 继承widget widget的构造函数参数应使用命名参数,命名参数中的必要参数要添加@required标注,这样有利于静态代码分析器进行检查 context : build方法有一个context参数,它是BuildContext类的一个实例,表示当前widget在widget树中的上下文 StatefulWidget 继承 widget 重写了父类的createElement()方法 添加新的方法,createState() statefullWidget对应一个state类 widget构建时可以同步获取 在widget生命周期中可以改变,并调用setState() 通知框架重新执行build 属性1widget:表示与该state绑定的widget,属性2context:同statelessWidget的context 写一个组件,研究state声明周期CounterWidget
- 复习state声明周期
- widget获取state对象
通过context获取
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>();
//调用ScaffoldState的showSnackBar来弹出SnackBar
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
通过of直接获取
// 直接通过of静态方法来获取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
通过GlobalKey
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
- 操作: 在widget中获取state
看不懂Builder 点击时,showSnackBar会弹出一个窗口显示文字 通过of
提供of方法表示表示可以通过of获取(scaffold组件默认提供了of) 没有提供of方法表示没有不可以返回父级state
- flutter 内置组件库
Text, 带样式的文本 Row, Coulmn, 类似web中的flexbox布局 Stack, 取代线性布局: 类似与web中的绝对定位盒子(配合Positioned) Container, 矩形视觉元素: 可以装饰一个BoxDecoration(如背景,边框,阴影),也可以设置margin,padding
- widget组件库(Material和Cupertino)
1 内部都引入flutter/widgets.dart,所以使用这俩组件时,不需在引入 2 不同域web开发,不需要担心引入两个组件库,导致安装包变大,dart只会编译你使用了哪些代码
- 同react和vue 的状态管理
1 如果是用户数据,由父组件管理 (选中状态,滑块位置) 2 如果是外观数据,由当前组件管理 (颜色,动画) 3 如果出现不同组件使用同一数据, 由他俩的父元素管理
- 简单组件
TapboxAState
自己管理自己状态的组件
- 父管子组件
ParentWidget
组件名字都得是驼峰,首字母都得大写
- 父子分别管组件
ParentWidgetC
flutter DouBan : 学习他的lib文件结构 创建文件夹划分模块(状态管理:
statusManagement
)import 语法中的
package: + 项目名称
表示lib文件夹下 (项目名称在pubspec文件) 发现了: dart语言中的()
,可以写属性Container( child: new Text('...tip...'),)
decoration,用来写样式的 , 赋值 用 new BoxDecoration
- 先把状态组件放在一块
需要安装Android 4.1(API level 16)或更高版本的Android设备 在您的设备上启用 开发人员选项 和 USB调试 。详细说明可在Android文档中找到。 使用USB将手机插入电脑。如果您的设备出现提示,请授权您的计算机访问您的设备。 在终端中,运行 flutter devices 命令以验证Flutter识别您连接的Android设备。 运行启动您的应用程序 flutter run。
- 看了一篇文,
- 原生的还是原生的,但是学起来是有难度的,
- flutter 相当于js的vue, 扩展,
- 我本来对它很有期待,
- 现在有一点怀疑了。
- 会用对我来说应该更加重要
- 我为了准备面试,学了目前能学到的所有东西
通过全局状态管理器, 处理相距较远的组件通信 1 全局事件总线, app组件的initState方法中订阅语言改变的事件 2 使用状态管理包, Provider,Redux
由于基础的git操作失误,错失了这几天的commit日志,虽然,没改啥. 基础还是有点差劲
ssi: 服务器端渲染, 可以选择reactssi框架,搭建项目
一个作业帮的小哥,93年,4年前端,20k以上,年轻,帅气,又是一个比我厉害的人 理论:每天早上一小时,写flutter或者react
- 基础组件Text
- 基础组件Text (2)
TextSpan
- 文字算是看完了,最后的引入文字文件不是很理解,其他的应该问题不大
- 基础组件 按钮
- 昨天的字体引入有问题: 导致无法运行程序
字体先不管,提交了一个issue继续写按钮的
- 图片和icon
ImageProvider 是一个抽象类, 就是定义了图片获取接口(load) image
包括 AssetsImage: 定义从assets加载图片 NetworkImage: 定义从network加载图片 使用image的属性
SingleChildScrollView
,这玩意可以实现滚动EdgeInsets
,SizedBox
,BoxFit
, 这些widget相当于见过了 对 child 使用 .map 和 toList 也是见过了(就是把数组里面的每一个部件进行过滤和加工处理) 类型判断A is B
注意: flutter 缓存图片数量最大1000,图片内存最大100m icon 查看所有的icons 使用map的时候的问题(baseWidgetImg的49行注释)
icon
使用meaterialIcons
MainAxisAlignment
使用引入字体(ttf格式) 无法引入字体文件并使用(原因是因为,修改了pubspec文件需要重新启动项目,而且控制台已经报错了)
表单中switch和checkbox
Switch 和 Checkbox 都继承StatefulWidget,但是他们本身不会保存状态,父级来管理
No Material widget found Switch widgets require a Material widget ancestor
使用switch需要再material的scaffold组件下使用 下一个,表单的控制焦点
表单: 空值焦点
FocusNode 和 FocusScopeNode来控制焦点
autofocus: true
, 一个页面中同时两个默认获取焦点,第一个获取到FocusNode.unfocus
: 失去焦点FocusScope.of(context)
来获取Widget树中默认的FocusScopeNode 主题色: 按钮和输入框的默认颜色就是主题色中的hintColor
继续按钮
昨天 的 Theme 是个组件...理解上还有点问题 theme 将主题应用于字类 1 描述排版, 颜色 2 全局theme,MaterialApp 创建(可以局部使用) 问题: 如何实现表单域的滚动效果 没有解决这个问题 往下 form
- form
Expanded
, 类似于column,row,flex 用来展示多个组件集合的组件封装Spacer组件(根据指定比例占位)
Spacer(
flex: 1,
),
class Spacer extends StatelessWidget {
const Spacer({Key key, this.flex = 1})
: assert(flex != null),
assert(flex > 0),
super(key: key);
final int flex;
@override
Widget build(BuildContext context) {
return Expanded(
flex: flex,
child: const SizedBox.shrink(),
);
}
}
问题: 不知道为什么按钮沾满了个剩余高度. 理解
使用 Builder
: 使用builder的目的是改变当前context.(初步理解)
- 进度指示器
- 布局
flutter布局简介 根据是否包含字节点把widget分成了三类(布局类组件都是有子组件的) LeafRenderObjectWidget,SingleChildRenderObjectWidget,MultiChildRenderObjectWidget 1 布局类组件就是直接或间接包含MultiChildRenderObjectWidget的widget 2 一般都有children用域接受widget 3 继承关系: widget > RenderObjectWidget > (上面三类) RenderObjectWidget 定义了创建更新 RenderObject的方法
- 线性布局: row 和 column
- 接着说线性布局
row , column 问题: 使用column嵌套子级 时会默认沾满高度,但是我的没有, 如果column嵌套column ,并且想让子级column高度占满column使用Expanded组件
- 下面时弹性布局
flex , Expanded
因为Row和Column都继承自Flex
Spacer创建一个可调整的空间隔,可用于调整Flex容器(如行或列)中窗口小部件之间的间距。(包装好的Expanded)
和示例有点差别
- 流式布局(超出)
Wrap
除了超出显示范围 Wrap会折行以外,其他行为基本相同 runAlignment不知道效果Flow
自定义布局和性能要求高的情况(性能好,灵活) 比wrap复杂,必须指定父组件大小.
- 层叠布局
stack positioned
相当于position: relative和position: absolute
- 对齐,align
FlutterLogo
Alingnment
1 布局时: Alingnment 会以矩形的中心点作为坐标原点, 理解: 数值 和 静态常量得写法
Alignment(-1.0, -1.0)
=Alignment.topLeft
表示左顶点,Alignment(1.0, -1.0)
=Alignment.topRight
表示右顶点 2 坐标转换公式 布局(Alignment.x*childWidth/2+childWidth/2, Alignment.y*childHeight/2+childHeight/2)
上面得childWidth childHeight
表示子元素宽高 上面两个式子算出来的时子元素实际偏移量(相对于左上角)FractionalOffset
这个和alignment一样都是用来定位的,区别在于: 他是用左上角来定位(和web一样) 坐标转换公式:(FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)
- 对比(align和stack)
- 定位参考体系不同
- Stack可以有多个子元素
- (相当于web里面的 text-align和position 的区别)
Center
组件
继承了
Align
对齐方式确定(Alignment.center)DecoratedBox
: 可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框、渐变等
- 容器类组件(竟然还挺快的)
padding
尺寸限制类组件
ConstrainedBox、SizedBox、UnconstrainedBox、AspectRatio
- 容器组件
SizeBox
多个父元素尺寸限制UnconstrainedBox
- 装饰组件
DecoratedBox
decoration: BoxDecoration(
Color color, //颜色
DecorationImage image,//图片
BoxBorder border, //边框
BorderRadiusGeometry borderRadius, //圆角
List<BoxShadow> boxShadow, //阴影,可以指定多个
Gradient gradient, //渐变
BlendMode backgroundBlendMode, //背景混合模式
BoxShape shape = BoxShape.rectangle, //形状
)
LinearGradient
用域定义线性渐变的类(还有其他定义渐变的类)
- 变换组件
transForm
RotatendBox
Container
本身不具备RendeObject, 可以装饰变化限制 因为: 他集合了很多其他组件功能: decoratedBox,constrainedBox,trnasform,padding,align 设置宽高: constraints 或者 width/height, 后者优先 设置背景: color 和 decoration,(只能设置一个)
- Flutter Gallery是Flutter官方提供的Flutter Demo,源码位于flutter源码中的examples目录下,笔者强烈建议用户将Flutter Gallery示例跑起来,它是一个很全面的Flutter示例应用,是非常好的参考Demo,也是笔者学习Flutter的第一手资料。
`Could not find a command named "channer". flutter多写一个t
Setting "enable-windows-desktop" value to "true".
You may need to restart any open editors for them to read new settings. 控制台报错: Unexpected child "deferred-components" found under "flutter".
- github user 1: Jun Shi Yan
- github user 1: 李泽鹏
@DhavalRKansara Your Flutter is on the stable channel. As mentioned previously, the gallery runs off the master channel, to which you can switch with
flutter channel master
flutter upgrade
- gallery
无法执行 命令:
flutter channel
列表或开关Flutter通道。 命令:flutter create .
创建一个新的Flutter项目。 命令:flutter upgrade
升级你的Flutter副本。 重新执行master 和 upgrade,执行flutter upgrade
自动执行一次flutter doctor
,出现错误: Please install the "Desktop development with C++" workload, including all of its default components 下面是根据博客上的文章执行的: 安装visual studio
(它和vscode的区分查看: https://www.zhihu.com/question/384334551)
- 打开网站下载
- 打开安装包,点确认
- 点击添加负荷
- 选中:
"Desktop development with C++"
- 选中:
Windows 10 SDK (10.0.17763.0)
(Windows 10 SDK (10.0.17763.0) ,需要下载的是10.0.17763.0这个版本的) - 选择下载路径,点击安装
- 执行flutter doctor
这个问题和昨天的问题有关联,具体内容查看(https://zhuanlan.zhihu.com/p/91686888)
- gallery
- 首先重新执行
flutter doctor
其他: 如何查看flutter 版本并切换1
查看flutter --version
2flutter versioin
3flutter versioin vxxx
(版本号)
- 然后执行
flutter channel stable
- 然后执行
flutter upgrade
报错 unable to access 'https://github.com/flutter/flutter.git/' 应该是网络问题导致的,重新来一次(如果不行就配置代理,这个回头在学)
- 然后
to run the app on Windows:
,执行:flutter config --enable-windows-desktop
- 然后执行
flutter create .
报错: 还是
Unexpected child "deferred-components" found under "flutter"
(也就是说,我通过下载安装上面c++那个软件并没有用 或者 我使用错误) 没有解决, 忽略直接run(还是报错) 晚上看 答案就放在那,我解决不了问题(问) 问群里
如果你的电脑没有在开发者模式,使用插件会出错。 你可以在设置-->更新和安全-->开发者选项里设置
Building with plugins requires symlink support. Please enable Developer Mode in your system settings
- 重新 clone
- 失败
- 决定随缘,不写了
- marterial
组件名称: 解释 AppBar: 一个导航栏骨架 MyDrawer: 抽屉菜单 BottomNavigationBar: 底部导航栏 FloatingActionButton: 漂浮按钮
(看react native 官网) (
- 文档的交互示例,组件类型(函数和class),提示
- 核心组件和原生组件(该看这个了)
)
Tab
Tab({
Key key,
this.text, // 菜单文本
this.icon, // 菜单图标
this.child, // 自定义组件样式
})
使用任何图片都需要在yaml文件先引入
- 解决了浮动按钮,嵌到底部的问题
- 裁剪
剪裁Widget 作用
ClipOval 子组件为正方形时剪裁为内贴圆形,为矩形时,剪裁为内贴椭圆
ClipRRect 将子组件剪裁为圆角矩形
ClipRect 剪裁子组件到实际占用的矩形大小(溢出部分剪裁)
'CustomClipper' 'clip'
- react 能干什么: 用js访问移动平台的api 实现外观和行为,通过react ui 组件
- 基本概念 a. 视图: react ui的最小组成部分, 相当于flutter的widget,或者html的标签 b. 原生组件:react native 编写的应用和原生的一样,实质是对系统原生组件的封装(react native会把组件自动转换为系统的原生组件)(系统的原生组件:android的Kotlin或java编写视图,ios的swift或objective-c编写视图)。 c. 核心组件:基础/常用的原生组件。
- 和react一样的api
react components > react native components > core/native/community(第三方组件)
所以,接下来就是react的基础(组件,jsx,自定义组件,prop,state)
另外:jsx中传递一个 JS 对象值的时候,就必须用到两层括号:{{width: 200, height: 200}}。
另外:React.Fragment是抽象类,相当于小程序的block,写法
<> </>``<React.Fragment key={item.id}></React.Fragment>
暂停,使用检视阅读先读一次 -检视阅读开始 1:跳过不过的 2:重点看主要的 a. 环境 :译注:请注意!!!国内用户必须必须必须有稳定的代理软件,否则在下载、安装、配置过程中会不断遭遇链接超时或断开,无法进行开发工作。某些代理软件可能只提供浏览器的代理功能,或只针对特定网站代理等等,请自行研究配置或更换其他软件。总之如果报错中出现有网址,那么 99% 就是无法正常连接网络。 全部看标题看了一遍,感觉,把这个看完,了解了react native 开发之前的所有理论知识,除了配置环境的时候可以实际操作一下,大部分都是需要自己理解的。不过,看完知道了这是一个大的领域,蛋蛋android和ios就是两个完全不同的世界。目前看来,会比flutter更好开发一些,但是我看的官网,所以理解起来可能不像实战书那么熟。决定还是先看文档。
- 可滚动组件
默认超出会报错 可滚动组件直接或间接包含一个scrollable组件
scrollable
axisDirection属性:滚动方向 physics属性:决定响应用户操作方式,接受scrollPhysics类型对象(包括ClampingScrollPhysics,和,bouncingScrollPhysics) controller: 控制位置和监听事件,接受ScrollController类型对象
scrollbar
给可滚动组件加滚动条 会在ios平台自动切换成CupertinoScrollbar
(ios滚动条风格)sliver
只构建出现在当前视口的子组件一种性能优化(视口: ViewProt,当前widget的实际显示区域)SingleChildScrollView
不支持sliver
-
singleChildScrollView
-
react native: 入门组件
TextInput
注意 react 中的 onChange 对应的是 rn 中的 onChangeText 具有“动态状态”的最简单的组件
ScrollView
所有组件都会被渲染,不进行sliver,不适合长列表 再android和ios都有个子的区别flatList
长列表: 立即渲染所有元素,而是优先渲染屏幕上可见的元素secionList
长列表: 需要分组的数据
- react native: 针对不同平台的处理
方法一:
Platform
模块 适用于平常代码中解决不同平台的少量代码冲突
Platform.OS
返回ios或这android
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
height: Platform.OS === 'ios' ? 200 : 100
});
Platform.Select()
直接返回设定的value值
import { Platform, StyleSheet } from 'react-native'
const styles = StyleSheet.create({
container: {
flex: 1,
...Platform.select({
ios: {
backgroundColor: 'red'
},
android: {
backgroundColor: 'blue
}
})
}
})
接受任何合法类型的参数,包括组件
Platform.Version
, 返回android和ios的当前版本在 Android 上,Version属性是一个数字 在 iOS 上,返回当前系统版本的字符串,比如可能是"10.3"。
方法二: 特定平台扩展名 适用于:不同平台代码放在不同的文件里时
目录下有俩文件
BigButton.ios.js
BigButton.andorid.js
引入时会根据不同平台引入对应的后缀
import BigButton from './BigButton';
明天就是环境搭建
23:00 在家没法运行flutter, 先把native看一遍(环境配置)
- 明确环境
- 区分开发平台
- 区分目标平台(在哪个平台使用) 想从一个平台转移另一个平台,查看官方进行对应的环境搭建就好(部分区别)
- 明确依赖
- node,JDK,android studio (android studio 是一个编译器,开发android应用的。开发时需要使用他提供的工具和环境) (node版本大于12) (JDK 是对java基础环境和相应开发平台标准和工具包的封装,安装版本必须是1.8,也称8版本)
不能使用淘宝镜像(cnpm)
(yarn是脸书替代npm的工具,可以加速node的下载,官方推荐使用yarn代替npm)
- (工作上使用taro 又熟练了一波)
- 开始搭建rn环境
- 本机已经安装android studio,(然后跟着官网走)
- 以下是
Android 开发环境
- 安装 Android Studio(注意 安装 Android sdk, Android sdk platform, Android virtual device)
- 安装 Android SDK (找官网,再配置中找到并且安装指定版本)
- 配置 ANDROID_HOME 环境变量
- 把一些工具目录添加到环境变量 Path
- 以下开始创建项目
npx react-native init AwesomeTSProject --template react-native-template-typescript
使用ts模板创建rn项目 (如果没有安装 react-native 会先安装这个)- 接下来就是在
react native
的项目里面操作
- 继续flutter组件学习
ListView
- ListView 的
默认构造函数
指的是直接使用
ListView({children: list})
通过这样的方式 list 中的所有widget都会提前全部渲染,(也就是不支持sliver)
- ListView 的 builder
滚动组件普遍规律: 构造函数构建的可滚动组件通常就是支持基于Sliver的懒加载模型的,反之则不支持
- ListView 的
separated
separated和builder的区别就是,多了个
separatorBuilder
,根据条件控制列表的每一项 问题: 无法使用word_pair构造出组件,baseScroll4无法渲染
- 上次报错原因:
listTile
必须包在material
组件下
学习上拉组件
initState
使用主题颜色借鉴:
Theme.of(context).primaryColor
为什么叫做状态: 针对于组件,组件的当前的状态就是组件的state 不使用箭头函数的stateFulWidget的createState方法
@override
State<StatefulWidget> createState() {
return ScrollHomePageState();
}
找到了
initState() 方法是在创建 State 对象后要调用的第一个方法,常常用做于初始化一些数据 有点像钩子函数,类似的方法还有:didChangeDependencies() (initState() 方法执行完毕后执行的第二个方法) Widget: 部件, context: 当前Widget创建的element对象, state: element的状态
- 下次接着看吧(先看懂再说)
- 只想更新应用依赖的包(
pubspec.yaml
中的依赖)
flutter packages get
或者flutter pub get
- 如果想看命令行命令, 输入 xxx -h 自己就可以看到
实验
flutter run
执行后会有选择执行环境,然后会自动拉起设备 好处: 1 看到了命令
应该和vscode启动的顶部菜单是一致的功能 坏处: 好像vs code 并没有打开 app运行的状态(底部的黄条,以及 顶部的功能菜单) 坏处: 无法自动更新
f1
看到常用的flutter命令 输入
launch emulator
比较设备
- 继续学习无限滚动组件
Divider
分割线组件
Future.delayed(Duration(seconds: 2))
相当于web里面的setTimeout
generateWordPairs
english_words中的一个方法 用来模拟数据CircularProgressIndicator
之前写过,是一个圆形进度条.可以用来做加载中的 icon
- 滚动组件 添加固定表头
使用文档中的内容,控制台会报错,原因不详(并且会打开一个文件)
可能原因: 外层嵌套了
Column
所以会报错,直接放在scaffold
的body没有报错 应该是因为我复制错了代码,文档中也说会出错,需要设置 复制文档中的实例实现了顶部固定,底部滚动的效果 实现原理是,Expanded
和Column
(解决了适配不同屏幕的效果) 也可以公国material中的sizeBox
来对高度进行计算,保证高度适配(没有第一个方法好)
GridView
和listview大部分参数一样 唯一需要关注的参数gridDelegate(设置子组件如何排列)
值为:
SliverGridDelegate
类,flutter提供了两个子类,SliverGridDelegateWithFixedCrossAxisCount
和SliverGridDelegateWithMaxCrossAxisExtent
GridView.count
可以用来替换GridView+SliverGridDelegateWithFixedCrossAxisCount的情况GridView.extent
可以用来替换GridView+SliverGridDelegateWithMaxCrossAxisExtent的情况GridView.builder
用来显示异步的子项情况,或子项较多的时候
- 证实,无法再Column中使用gridview
GridView.Builder
的示例
- 接受两个参数
gridDelegate
和itemBuilder
CustomScrollView
自定义滚动组件
- 滚动模型
Sliver版的可滚动组件,不包含滚动模型 非Sliver版的可滚动组件,包含滚动模型
Material
也是一个widget,可以作为根组件返回
属性
child
CustomScrollView
slivers
属性,设置一个数组 CustomScrollView的子组件必须都是Sliver。
SliverAppBar
相当于AppBar
,前者可以集成到CustomScrollView
(实现头部伸缩的效果)SliverPadding
sliver 增加paddingSliverGrid
组件网格SliverFixedExtentList
组件列表
- 学习
CustomScrollView
组件
Image.asset
的 fit属性,指定图片在容器的分配方式,值为BoxFit
BoxFit
常见的有: scaleDown,contain,cover
SliverGrid
构建一个网格组列表
属性
gridDelegate
和GridDelegate
一样 属性delegate
设置每一个子项的widget,参数(值为SliverChildBuilderDelegate
)
SliverFixedExtentList
构建一个列表(值为SliverChildBuilderDelegate
)
- scroll 监听
ScrollController
offset
可滚动组件当前位置jumpTo``animateTo
两个方法跳转指定位置
ScrollController
间接继承自Listenable
也就是说可以使用
assListener
方法 示例为了避免内存泄露,需要调用_controller.dispose
super.dispose
放在最后调用
@override
void dispose() {
_controller.dispose();
super.dispose();
}
- 滚动页面位置恢复
PageStorage
用来保存页面相关数据的组件 里面的widget 可以通过指定不同的
pageStorageKey
来存储各自的数据和状态每次滚动结束,都会把
offset
存储在PageStorage
中 重新创建时恢复offset
判断keepScrollOffset
的值,有救使用,没有就用initialScrollOffset
ScrollPosition
用来保存可滚动组件的滚动位置 animateTo() 和 jumpTo() 用来控制跳转位置的方法 执行过程
先会调用ScrollController (“注册位置”)可滚动组件会调用attach()方法 组件销毁时 会调用ScrollController的detach()方法 将其ScrollPosition对象从ScrollController的positions属性中移除
NotificationListener
可以用来监听(类似冒泡)
和
scrollController
的区别 controller 只能监听关联的组件,notification 可以让所有父级醉驾案监听 controller 只能获取当前滚动位置信息, notification 可以额外获取viewPort的一些信息 示例 学习示例内容 太困了,学了一半
ScrollNotification
类
metrics
属性, 值为SrollMetrics
(包含当前ViewPort的滚动位置等信息)
理解 ViewPort的含义,当前设备可视窗口信息 包括
pixels
: 当前滚动位置,maxScrollExtent
: 最大可滚动长度,extentBefore
: 划出ViewPort的顶部的长度(划出屏幕上方的列表长度),extentInside
: ViewPort内部长度(屏幕中显示的列表长度),extentAfter
: 没有滑入ViewPort部分的长度(列表底部没显示区域的长度),atEdge
: 是否滚动到了边界(顶部或者底部) 目前问题很多,一个滚动没有想到会有那么多不懂得地方,感觉有点复杂,现在调整一下心态.现在得主要目的是了解,识别.所以示例明白怎么写的就好,不看和官方文档以外得部分
- 功能性组件
- 导航返回拦截
WillPopScope
使用场景: 防误触判断,eg:用户在某一个时间段内点击两次时,才会认为用户是要退出 属性
onWillPop: 回调函数,返回一个Futrue对象,如果futrue最终值是false,则不出栈(不返回) 示例 返回时有打印,但是没有返回上个页面,原因不详
开始早上来了,先学习,在游戏得习惯
- 数据共享(类似stare,redux)
- 能实现组件跨级传递数据
InheritedWidget
父组件传递子组件,Notification
子组件传递父组件didChangeDependencies
子组件的回调, 当父组件的属性发生变化,会调用这个函数- 示例:
报错未处理(代码没有复制完)
接着把示例搞定
搞定
- 看懂,跨级传递数据(类似props)
- 单独拿出来会报错?
自己没有写
import
- RaisedButton按钮被废弃,使用
ElevatedButton
来代替 ShareDataWidget extends InheritedWidget
父组件继承
InheritedWidget
父组件必须定义updateShouldNotify
决定是否更新状态 子组件要定义didChangeDependencies
子组件要定义 必须使用 父组件 中的共享数据 访问:ShareDataWidget.of(context)
- 场景2: 父组件数据改变,触发子组件build, 不触发
didChangeDependencies
- 父组件
getElementForInheritedWidgetOfExactType
dependOnInheritedWidgetOfExactType
注册了依赖关系,getElementForInheritedWidgetOfExactType
不会
- 重点预告
现在只要调用_InheritedWidgetTestRouteState的setState()方法,所有子节点都会被重新build,这很没必要 下一节我们将通过实现一个Provider Widget 来演示如何缓存,以及如何利用InheritedWidget 来实现Flutter全局状态共享。
- 跨组件共享(Provider)
- 一般的原则是:如果状态是组件私有的,则应该由组件自己管理;如果状态要跨组件共享,则该状态应该由各个组件共同的父元素来管理
- 使用全局事件总线EventBus(观察者模式)(类似 vue Bus)
enum Event{
login,
... //省略其它事件
}
bus.emit(Event.login);
void onLoginChanged(e){
//登录状态变化处理逻辑
}
@override
void initState() {
//订阅登录状态改变事件
bus.on(Event.login,onLogin);
super.initState();
}
@override
void dispose() {
//取消订阅
bus.off(Event.login,onLogin);
super.dispose();
}
(观察者模式)缺点: 必须显式定义各种事件,不好管理 订阅者必须需显式注册状态改变回调,也必须在组件销毁时手动去解绑回调以避免内存泄露。
- 利用
InheritedWidget
自动更新子组件特性,我们可以将需要跨组件共享的状态保存在InheritedWidget
中,然后在子组件中引用InheritedWidget
即可,Flutter社区著名的Provider
包正是基于这个思想实现的一套跨组件状态共享解决方案
- 示例
- 定义一个通用的
InheritedProvider
类,它继承自InheritedWidget
问题一:通知数据变化,通过 eventBus方式,来进行事件通知 通过flutter 的
ChangeNotifier
,继承Listenable
,也可以实现(发布订阅)
class ChangeNotifier implements Listenable {
List listeners=[];
@override
void addListener(VoidCallback listener) {
//添加监听器
listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
//移除监听器
listeners.remove(listener);
}
void notifyListeners() {
//通知所有监听器,触发监听器回调
listeners.forEach((item)=>item());
}
}
通过调用
addListener()
和removeListener()
来添加、移除监听器(订阅者); 通过调用notifyListeners()
可以触发所有监听器回调。
ChangeNotifierProvider
编译报错
_typeOf
报错,原因不明,
重新看一次文档 除了
_typeOf
其他的报错已经解决,还是因为没有理解他们之间的关系,直接自己写的有问题
- 使用上面的方法,实现 跨组件传递
示例 目的: 显示购物车中所有商品总价
1 组件
UnmodifiableListView
一种禁止修改的ListView,比如电商app购物车里面的物品是禁止修改的。 不能变更List的话,尽量使用unmodifiableListView有助提高编程习惯。 2CartModel extends ChangeNotifier
3Builder
(第一次记录在 454行,这次通过老孟flutter进行理解)
Builder(
builder: (BuildContext context){
return Container();
},
)
builder 可以更加解决scaffold的body获取不到 context的问题(扩展了context)
- 如果我们将ChangeNotifierProvider放在整个应用的Widget树的根上,
那么整个APP就可以共享购物车的数据了,这时ChangeNotifierProvider的优势将会非常明显
- 优化1
封装
Consumer
组件 解决1: 依赖CartModel很多时,这样的代码将很冗余 解决2: 语义将会很明确使用
Consumer
会报错 报错原因没有找到 优化2listen
可以实现 数据便哈按钮本身没有变化,不重新build的
- 颜色和主题
Color
类
色值转换和亮度 将rgba值
#xxxxxx
类型转换为 flutter 中的Color类
- 示例,导航变色
知识一,
创建一个类,构造函数中的对象可以添加组件调用时的参数(这个类似 react)
- 回顾昨天
Color
构造函数,传入8位十六进制直接使用Color
如果传入6位十六进制,透明度位00int.parse(_color2, radix: 16)
可以将颜色(字符串类型)转换为数字类型0x
开头的数字表示16进制- 修改透明度方法
0x00FFFFFF | 0xFF000000
把前两位替换成FF
(透明度)Color.withAlpha
方法,传入十进制alpha数值(相当于修改透明度)
color.computeLuminance()
方法返回当前颜色一个亮度值(0-1之间)
MaterialColor
- 实现颜色的类: 包含一种颜色的10个级别的渐变色
- 通过"[]"运算符的索引值来代表颜色的深度
- MaterialColor的默认值为索引等于500的颜色(Colors.blue 相当于 设置Colors.blue[500])
Theme
- 定义主题数据
ThemeData
用于保存是Material 组件库的主题数据
子组件通过Theme.of方法来获取当前的ThemeData 有些是不能自定义的,如导航栏高度,ThemeData只包含了可自定义部分。
ThemeData({
Brightness brightness, //深色还是浅色
MaterialColor primarySwatch, //主题颜色样本,见下面介绍
Color primaryColor, //主色,决定导航栏颜色
Color accentColor, //次级色,决定大多数Widget的颜色,如进度条、开关等。
Color cardColor, //卡片颜色
Color dividerColor, //分割线颜色
ButtonThemeData buttonTheme, //按钮主题
Color cursorColor, //输入框光标颜色
Color dialogBackgroundColor,//对话框背景颜色
String fontFamily, //文字字体
TextTheme textTheme,// 字体主题,包括标题、body等文字样式
IconThemeData iconTheme, // Icon的默认样式
TargetPlatform platform, //指定平台,应用特定平台控件风格
...
})
- 路由换肤示例
示例完成 悬浮按钮没有实现变色
- 实例中实现了 不使用父组件的样式(第二行的黑色)
- async
FutureBuilder
FutureBuilder({
this.future,
this.initialData,
@required this.builder,
})
future
以来的Future,通产是一个异步操作initialData
初始数据,用户设置默认数据builder
Widget构建器 实例, 进入页面2秒后返回一个字符串显示ConnectionState
一个枚举类,四个状态
none,// 当前没有异步任务,比如[FutureBuilder]的[future]为null时 waiting,// 异步任务处于等待状态。 active,// Stream处于激活状态(流上已经有数据传递了),对于FutureBuilder没有该状态(active只在StreamBuilder中才会出现。) done,// 异步任务已经终止.
StreamBuilder
接受多个异步操作结果 常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等
- 对话框
AlertDialog
const AlertDialog({
this.title, //对话框标题组件
this.content, // 对话框内容组件
this.actions, // 对话框操作按钮组
})
showDialog()
返回一个Future值,(点击弹窗的按钮可以设置返回值)
Future<T> showDialog<T>({
@required BuildContext context,
bool barrierDismissible = true, //点击遮罩时是否关闭它
WidgetBuilder builder, // 对话框UI的builder(AlertDialog)
})
SimpleDialog
展示一个列表,用户选择
Dialog
showDialog
和SimpleDialog
的父级showDialog
和SimpleDialog
都使用了IntrinsicWidth
来通过子组件的实际高度调整自身尺寸,所以无法延迟加载模型Dialog
可以实现延迟加载模型不是特别理解,但是可以明白他俩的区别,
dialog
更偏向于动态的多条弹窗数据 MySimple2
- showDialog 方法中的builder用来返回一个弹窗,上面已经说了三种
可以不返回上面三个,可以返回别的. 实例dialog2>UnconstrainedBox
- 对话框打开动画和遮罩
- 前面说的都是material 的方法, flutter自己也有dialog方法
showGeneralDialog
showDialog
正是基于showGeneralDialog
的封装showDialog
默认打开对话框是一个Fade的动画,我们可以自己定义showGeneralDialog
的showCustomDialog
方法,来设置动画
- 实例
showCustomDialog
transitionBuilder
设置打开的动画
- 弹窗管理状态
- 弹窗中需要通过状态动态显示数据,并且传给页面组件
setState(() {
//更新复选框状态
withTree = !withTree;
});
问题: context 不一样, 产生原因:
1 setState方法只会针对当前context的子树重新build,对话框不是再父组件中构建的是通过showDialog单独构建的 2 showDialog是通过路由创建的,修改父级的状态不会影响下一个路由 解决方法 1 单独抽离出StatefulWidget
单独封装一个checkbox 并且继承 StatefulWidget 组件,自己修改状态 2 使用StatefulBuilder方法 3 精妙的解法
- 弹窗中管理状态
- 实例1, 错误写法
打开弹窗,点击选中,没有效果
- 实例2, 单独抽离checkbox
重新封装一层checkbox,context作为参数传进去
- 实例3, 使用StateBuilder方法
使用builder 方法简化了一下2方法
- 实例4, 最好的方法
setData调用时,调用_element.markNeedsBuild(),页面才会重构
markNeedsBuild 方法标记当前element为 dirty,由此实现重构 如果我们能标记为dirty,自然就能实现重构 context实际上就是Element对象的引用, 所以接下来
- 其他对话框
showModalBottomSheet
底部对话框 实例 5
showBottomSheet
底部弹出整个页面的弹窗 实例 5 PersistentBottomSheetController 报错: 不知道怎么使用这个弹窗无法弹出内容,调用方法时控制台会报错
Loading
通过showDialog+AlertDialog来自定义 实例 6 showDialog中已经给对话框设置了宽度限制,所以不能直接修改宽度
先 UnconstrainedBox 抵消宽度限制 再使用 SizedBox
- 日历弹窗
实例6
- Pointer Event: 原始指针事件处理(触摸事件)
- 一次完整的事件分为三个阶段
手指按下、手指移动、和手指抬起(高级别的手势(如点击、双击、拖动等)都是基于这些原始事件的)
- 过程: 事件会在组件树中向上冒泡
- Flutter中没有机制取消或停止“冒泡”过程
Listener
- 监听原始触摸事件
使用示例
PointerDownEvent
PointerMoveEvent
PointerUpEvent
都是 PointerEvent 的子类 常用的属性有:position:它是鼠标相对于当对于全局坐标的偏移。 delta:两次指针移动事件(PointerMoveEvent)的距离。 pressure:按压力度,如果手机屏幕支持压力传感器(如iPhone的3D Touch),此属性会更有意义,如果手机不支持,则始终为1。 orientation:指针移动方向,是一个角度值。
- Listener.behavior = HitTestBehavior.deferToChild,在命中测试期间如何表现
behavior , 值为HitTestBehavior,枚举 分别是
deferToChild 子组件会一个接一个的进行命中测试 opaque 将当前组件当成不透明处理 translucent 当点击组件透明区域时,可以对自身边界内及底部可视区域都进行命中测试
- 不想让某个子树响应PointerEvent
IgnorePointer和AbsorbPointer
Listener(
child: AbsorbPointer(
child: Listener(
child: Container(
color: Colors.red,
width: 200.0,
height: 100.0,
),
onPointerDown: (event)=>print("in"),
),
),
onPointerDown: (event)=>print("up"),
)
如果将AbsorbPointer换成IgnorePointer,那么两个都不会输出。
behavior
这个属性不是特别理解
- 手势
GestureDetector
实例1 点击,双击,长按
onTop 延迟200毫秒,如果用户只监听了onTap(没有监听onDoubleTap)事件时,则没有延时。 实例2 拖动,滑动
GestureRecognizer
是一个抽象类,一个手势的识别器对应一个GestureRecognizer的字类 gestureDetector 内部是使用一个或多个GestureRecognizer来识别各种手势的 实例
GestureDetector
的 child 属性是一个widget组件,所以,如果不是widget组件,就无法使用GestureDetector
来绑定手势事件,
GestureDetector
内部是使用一个或多个GestureRecognizer
来识别各种手势的,所以,可以用GestureRecognizer
来尝试不是widget的情况 实例中使用了TextSpan的 recognizer属性,可以是一个TapGestureRecognizer()
使用GestureRecognizer后一定要调用其dispose()方法来释放资源(主要是取消内部的计时器)。
- 手势竞争与冲突
- 场景: 同时监听水平和垂直方向,斜着拖动
取决于第一次移动时两个轴上的位移分量 实例
BothDirectionTestRoute
组件
CircleAvatar
,Positioned
属性onVerticalDragUpdate
,onHorizontalDragUpdate
- 解决冲突
当 有一个widget 可以左右拖动,现在我们也想检测在它上面手指按下和抬起的事件 不能写在
GestureDetector
里面(会发现 并没有答应 down和up) 通过Listener
来见同(打印了down1和up1)手势冲突只是手势级别的,而手势是对原始指针的语义化的识别, 所以在遇到复杂的冲突场景时,都可以通过Listener直接识别原始指针事件来解决冲突。
- 番外读物
- 原生开发
程序稳定后的必走之路
优点 | 缺点 |
---|---|
速度快、性能高、稳定性强、用户体验好 | 前期开发费用高 |
可以访问手机所有功能 | 开发效率偏低 |
支持大量图形和动画 | 后期维护繁琐 |
可离线使用 | 上线时间无法固定 |
跨平台的特点
优点 | 缺点 |
---|---|
节省人力、开发成本 | 性能低于原生 |
节省开发时间 | 用户体验低于原生 |
多端的一致性 | 包体积变大 |
易上手 | 跨平台框架自身bug、缺陷 |
- Web App 有以下缺点,使得它始终是 “主角的心,配角的命”
a: 性能低,操作体验不好 b: 无法调用原生 API,很多功能无法实现, c: 依赖于网络,网速慢时体验很差,并且没有离线功能,优化不好的话会消耗流量 d: 只能做为一个临时的入口,用户留存率低
- Hybrid App 采用原生和 Web 开发 App(还可以采用 HTML5 + 原生)
理解: 主要是用js和原生技术相互调用,可以初步实现跨平台使用的效果 实现: Hybrid App 的原生 UI 组件用来展示交互复杂和渲染要求高的界面,其他的可以给 HTML5 来展示。 适用: 对于需要快速试错、快速占领市场的团队来说,Hybrid App 是一个不错的选择 相关技术: Hybrid 相关的技术有很多,比如 PhoneGap、Cordova、Ionic、VasSonic 等等
- 原生渲染
react native 理解: 写react的代码,可以再ios和android执行 实现: ios端: 在Objective-C 和 JavaScript 两端都保存了一份配置表,里面标记了所有 Objective-C 暴露给 JavaScript 的模块和方法, 当Objective-C 接收js传来的参数,调用对应的参数 特点:
优点 | 缺点 |
---|---|
复用了 React 的思想,有利于前端开发者涉足移动端。 | 做不到 Write once, Run everywhere |
能够利用 JavaScript 动态更新的特性,快速迭代。 | 不能做到完全屏蔽 iOS 端或 Android 的细节 |
相比于原生平台,开发速度更快,相比于 Hybrid 框架,性能更好。 | 由于 Objective-C 与 JavaScript 之间切换存在固定的时间开销,所以性能必定不及原生 |
weex(阿里开发的轻量级的原生渲染解决方案,大公司的 KPI 产物) uniapp(小程序最早的开创者) 在2015年9月,DCloud推进微信团队开展小程序业务.微信团队经过分析,于2016年初决定上线小程序业务 理解: 在 App端内置了一个webview和一个基于 weex 改进的原生渲染引擎,提供了原生渲染能力。 用途: 在App端: a: 如果使用vue页面,则使用webview渲染 b: 如果使用nvue页面(native vue的缩写),则使用原生渲染
- 自渲染
flutter Google 开源的 UI 工具包.支持移动、Web、桌面和嵌入式平台 特点1: Dart 语言来避免 JsBridge引起的性能问题 特点2: Flutter不使用OEM Widgets(或DOM WebViews),它提供了自己的 Widgets。 特点3: 高效率,开发快(模拟器运行热重载) 特点3: 高度一致,ios和android样式基本一样
- 重点提醒:
不管选择何种框架,前提还得对原生的开发环境以及开发模式有一定的了解,不然也是事倍功半。 并不是所有公司都能长期承担起原生App开发与维护的成本
- 事件总线
- 跨页面事件通知(例如: 登陆时登录和注销来进行一些状态更新)
- 实现一个简单的事件总线
示例: EventBus
- Dart中实现单例模式的标准做法就是使用static变量+工厂构造函数的方式,
就可以保证new EventBus()始终返回都是同一个实例,读者应该理解并掌握这种方法。 关于组件之间状态共享也有一些专门的包如redux、以及前面介绍过的Provider。
- Notification
- 通知冒泡(Notification Bubbling)
每一个节点都可以分发通知,通知会沿着当前节点向上传递, 所有父节点都可以通过NotificationListener来监听通知 通知冒泡和触摸事件冒泡相似,但终止通知冒泡后,通知将不会再向上传递。 示例1: 滚动组件使用滚动通知
NotificationListener的泛型引入可以限制监听的事件范围 onNotification函数返回布尔值,返回true阻断冒泡,返回false继续向上冒泡
- 自定义通知
dispatch原理刨析: Notification.dispatch 可以发起冒泡通知 dispatch(context)中调用了当前context的visitAncestorElements方法,该方法会从当前Element开始向上遍历父级元素 visitAncestorElements方法有一个遍历回调参数visitAncestor,会判断每一个遍历到的父级Widget是否是NotificationListener,如果是则调用NotificationListener的_dispatch方法 执行onNotification方法
- 动画
- 动画实现的原理
在一段时间内,快速地多次改变UI外观 超过16FPS,就比较流畅了,超过32FPS就会非常的细腻平滑 超过32FPS,人眼基本上就感受不到差别了 理想情况下是可以实现60FPS的,这和原生应用能达到的帧率是基本是持平的 为了方便开发者创建动画,不同的UI系统对动画都进行了一些抽象
- Animation
addListener(), 监听每一帧变化 addStatusListener(), 状态改变的监听器(开始、结束、正向或反向)
- CurvedAnimation可以通过包装AnimationController和Curve生成一个新的动画对象
常见的Curves
Curves | 描述 |
---|---|
linear | 匀速的 |
decelerate | 匀减速 |
ease | 开始加速,后面减速 |
easeIn | 开始慢,后面快 |
easeOut | 开始快,后面慢 |
easeInOut | 开始慢,然后加速,最后再减速 |
可以自定义 Curves
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
- AnimationController
- 控制动画
forward(), stop(), reverse() // 表示启动,停止和反向 派生自Animation,因此可以在需要Animation对象的任何地方使用 创建一个controller对象
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 20.0,
vsync: this
);
- Ticker
其中,vsync是一个TickerProvider对象,用来创建Ticker Ticker就是通过SchedulerBinding来添加屏幕刷新回调 锁屏时,SchedulerBinding(每次屏幕刷新的回调)不会执行,所以不会执行ticker
- Tween
定义从输入范围到输出范围的映射
- 动画实现
- 简单实现
实例1 (线性放大) 实例1 (增加 Curve) 实例2 (AnimatedWidget简化)(AnimatedWidget类封装了调用setState()的细节,并允许我们将widget分离出来) 实例2 AnimatedBuilder 正是将渲染逻辑分离出来, 实例2 实现循环: 动画正向执行结束时反转动画
- 自定义 路由切换 时的动画
MaterialPageRoute
它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换
CupertinoPageRoute
是Cupertino组件库提供的iOS风格的路由切换组件,它实现的就是左右滑动切换。PageRouteBuilder
来实现切换动画
- 以渐隐渐入动画来实现路由过渡
实例 PageRouteBuilder
pageBuilder
有一个属性animation,所以可以实现自定义动画过渡PageRouteBuilder
其实只是PageRoute的一个包装,我们可以直接继承PageRoute类来实现自定义路由 实例 PageRouteBuilder > FadeRoute 实现: 在打开新路由时应用动画,而在返回时不使用动画 实例 PageRouteBuilder > FadeRoute > (修改部分)
- Hero动画(英雄 动画)
- 指的是可以在两个页面(路由)之前都存在的动画
实现: Hero组件将要共享的widget包装起来,并提供一个相同的tag
InkWell
组件 组件在用户点击时出现“水波纹”效果
- 交织动画(Stagger Animation)
- 由一个动画序列或重叠的动画组成
- 注意以下几点:
要创建交织动画,需要使用多个动画对象(Animation)。 一个AnimationController控制所有的动画对象。 给每一个动画对象指定时间间隔(Interval)
- 实例: 柱状图
dart中extends、 implements、with的用法与区别 继承(关键字 extends) 混入 mixins (关键字 with) 接口实现(关键字 implements) 有前后顺序: extens在前,mixins在中间,implements最后; extends 规则 子类会继承父类里面可见的属性和方法 但是不会继承构造函数 子类能复写父类的方法 getter和setter 子类重写超类的方法,要用@override 子类调用超类的方法,要用super 子类可以继承父类的非私有变量 mixins 规则 作为mixins的类只能继承自Object,不能继承其他类 作为mixins的类不能有构造函数 一个类可以mixins多个mixins类 mixins绝不是继承,也不是接口,而是一种全新的特性 implements 接口实现 (没有interface的,但是Flutter中的每个类都是一个隐式的接口,Flutter中:class 就是 interface)
AnimatedSwitcher
- 通常在切换(Tab切换、路由切换)时都会指定一个动画,以使切换过程显得平滑
Flutter SDK组件库中已经提供了一些常用的切换组件,如PageView、TabView等, 但是,这些组件并不能覆盖全部的需求场景
- AnimatedSwitcher 可以同时对其新、旧子元素添加显示、隐藏动画
实例 计数器
- 实现原理
Widget _widget; //
void didUpdateWidget(AnimatedSwitcher oldWidget) {
super.didUpdateWidget(oldWidget);
// 检查新旧child是否发生变化(key和类型同时相等则返回true,认为没变化)
if (Widget.canUpdate(widget.child, oldWidget.child)) {
// child没变化,...
} else {
//child发生了变化,构建一个Stack来分别给新旧child执行动画
_widget= Stack(
alignment: Alignment.center,
children:[
//旧child应用FadeTransition
FadeTransition(
opacity: _controllerOldAnimation,
child : oldWidget.child,
),
//新child应用FadeTransition
FadeTransition(
opacity: _controllerNewAnimation,
child : widget.child,
),
]
);
// 给旧child执行反向退场动画
_controllerOldAnimation.reverse();
//给新child执行正向入场动画
_controllerNewAnimation.forward();
}
}
//build方法
Widget build(BuildContext context){
return _widget;
}
类似海有: AnimatedCrossFade
- 高级用法
实例: MySlideTransition2 无法实现实例
实例完成,因为没有写child元素
记录错误:
The relevant error-causing widget was
AnimatedSwitcher
lib\animation\MySlideTransition.dart:49
应该是因为没有用child
- 自己封装一个切换动画(任意方向)
实例: SlideTransitionX
- 动画过渡组件
- Widget属性发生变化时会执行过渡动画的组件
- 特征: 就是它会在内部自管理AnimationController
所以: 自己封装一个
AnimationController
可以大大提升 过渡动画的易用性 实例: AnimatedDecoratedBox1: decoration属性发生变化时执行一个过渡动画 ImplicitlyAnimatedWidget类, 用来封装动画
- Flutter预置的动画过渡组件
实例:
AnimatedWidgetsTest
- 自定义组件
- 场景
flutter提供组件无法满足需求 为了共享代码,封装一些公用组件
- 创建方式
通过组合其他组件 自绘 实现 RenderObject
- 组合其他组件
适用: 自定义组件最简单的方法,优先考虑 例如: Container就是一个组合组件(由: DecoratedBox,ConstrainedBox,Transform,Padding,Align组成) 思想: 开发就是组合提供的组件实现不同布局
- 自绘
适用: 无法通过现有组件实现需要的ui 例如: 实现一个圆形渐变的进度条 局限: CircularProgressIndicator的valueColor只支持执行旋转动画时变化Indicator的颜色 实现: 通过Flutter中提供的CustomPaint和Canvas来实现UI自绘。
- RenderObject
RenderObject是用来渲染文本和图片的,RenderObject.paint抽象方法 paint方法第一个参数表示上下文(PaintingContext), PaintingContext.canvas 就是主要的绘制逻辑 区别于自绘(CustomPaint和Canvas),自绘只是为了方便开发者封装的一个代理类
组合 | 自绘/RenderObject(通过Canvas) |
---|---|
简单 | 强大灵活,理论上可以实现任何外观的UI |
容易 | 必须了解Canvas API细节,并且得自己去实现绘制逻辑 |
- 自定义组件组合
- 自定义渐变按钮
通过组合DecoratedBox和InkWell来实现 实例: GradientButton 理解代码
- 抽离出单独的组件注意: 代码规范
如: 必要参数要用@required 标注 如: 可选参数在特定场景需要判空或设置默认值 如: 错误地使用组件时能够兼容或报错提示 如: 使用assert断言函数 如: 组件更新时是否需要同步状态
- 增强TurnBox组件
功能: 1,任意角度来旋转其子节点;2,过渡动画;3,手动指定动画速度 实例: TurnBox
- 富文本展示组件
实例: MyRichText 注意: 组件更新时是否需要同步状态 RichText TextSpan: 需要设置属性,不设置无法显示文字
- 自定义组件-自绘
- 几乎所有的UI系统都会提供一个自绘UI的接口, Canvas,开发者可以根据api绘制各种图形
flutter 提供了CustomPaint 结合画笔 CustomPainer 绘制图片
- CustomPaint
如果CustomPaint有子节点 将子节点包裹在RepaintBoundary组件 RepaintBoundary 子组件的绘制将独立于父组件的绘制
CustomPaint(
size: Size(300, 300), //指定画布大小
painter: MyPainter(),
child: RepaintBoundary(child:...)),
)
- CustomPainter
void paint(Canvas canvas, Size size);
Canvas:一个画布,包括各种绘制方法
1 | 1 |
---|---|
API名称 | 功能 |
drawLine | 画线 |
drawPoint | 画点 |
drawPath | 画路径 |
drawImage | 画图像 |
drawRect | 画矩形 |
drawCircle | 画圆 |
drawOval | 画椭圆 |
drawArc | 画圆弧 |
Size 绘制区域大小
- 画笔Paint
var paint = Paint() //创建一个画笔并配置其属性
..isAntiAlias = true //是否抗锯齿
..style = PaintingStyle.fill //画笔样式:填充
..color=Color(0x77cdb175);//画笔颜色
- 性能
绘制是比较昂贵的操作 a: 利用好shouldRepaint返回值
如果绘制依赖外部状态,改变则应返回true来重绘,反之相反 实例: CustomPaintRoute 实例: 圆形背景渐变进度条
- 文件操作
- 都是通过Dart IO库来操作文件的
IO库包含了文件读写的相关类,它属于Dart语法标准的一部分 Dart VM下的脚本还是Flutter,都是通过io库来进行操作的
- 访问app目录
PathProvider 这个插件提供一种平台透明(不分平台)访问设备常用位置 支持访问的位置有:
缓存(临时目录) |
---|
getTemporaryDirectory()获取临时目录 |
iOS上,这对应于NSTemporaryDirectory() 返回的值; |
Android上,这是getCacheDir() 返回的值 |
文档目录 |
---|
etApplicationDocumentsDirectory()来获取应用程序的文档目录 |
只有自己可以访问的文件 |
只有当应用程序被卸载时,系统才会清除该目录 |
在iOS上,这对应于NSDocumentDirectory |
Android上,这是AppData目录。 |
外部存储目录 |
---|
getExternalStorageDirectory()来获取外部存储目录 |
eg: sd卡,(ios不支持) |
- 昨天文件操作的补充
dart io库的操作非常丰富,这里只是讲一些前端最基本的部分,具体自己了解
- HttpClient
- HttpClient发起请求分为五步
第一步: HttpClient httpClient = new HttpClient(); 第二步: HttpClientRequest request = await httpClient.getUrl(uri); 包含Query参数:
Uri uri=Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
"xx":"xx",
"yy":"dd"
});
设置请求header:
request.headers.add("user-agent", "test");
携带请求体方法:
String payload="...";
request.add(utf8.encode(payload));
//request.addStream(_inputStream); //可以直接添加输入流
第三步: HttpClientResponse response = await request.close(); 返回对象: 返回一个HttpClientResponse对象,它包含响应头(header)和响应流(响应体的Stream) 第四步: 读取内容 String responseBody = await response.transform(utf8.decoder).join(); 第五步: httpClient.close(); 关闭client后,通过该client发起的所有请求都会中止。
- 实例: HttpTestRoute
没有看效果
- 常见配置参数
属性 | 含义 |
---|---|
idleTimeout | 对应请求头中的keep-alive字段值,为了避免频繁建立连接,httpClient在请求结束后会保持连接一段时间,超过这个阈值后才会关闭连接。 |
connectionTimeout | 和服务器建立连接的超时,如果超过这个值则会抛出SocketException异常。 |
maxConnectionsPerHost | 同一个host,同时允许建立连接的最大数量。 |
autoUncompress | 对应请求头中的Content-Encoding,如果设置为true,则请求头中Content-Encoding的值为当前HttpClient支持的压缩算法列表,目前只有"gzip" |
userAgent | 对应请求头中的User-Agent字段。 |
通过HttpClient设置的对整个httpClient都生效,而通过HttpClientRequest设置的只对当前请求生效
- 其他
证书校验其实就是提供一个badCertificateCallback回调
String PEM="XXXXX";//可以从文件读取
httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){
if(cert.pem==PEM){
return true; //证书一致,则允许发送数据
}
return false;
};
findProxy 代理
client.findProxy = (uri) {
// 如果需要过滤uri,可以手动判断
return "PROXY 192.168.1.2:8888";
};
如果不需要代理,返回"DIRECT"即可。 · APP开发中,很多时候我们需要抓包来调试 · 抓包软件(如charles)就是一个代理 · 可以将请求发送到我们的抓包软件,我们就可以在抓包软件中看到请求的数据了
HTTP请求认证 · 用于保护非公开资源 · 如果Http服务器开启了认证,那么用户在发起请求时就需要携带用户凭据 · 如果你在浏览器中访问了启用Basic认证的资源时,浏览就会弹出一个登录框 Basic认证的基本过程 · 客户端发送http请求给服务器,服务器验证该用户是否已经登录验证过了 · 客户端得到响应码后,将用户名和密码进行base64编码(格式为用户名:密码),设置请求头Authorization,继续访问服务器验证用户凭据,如果通过就返回资源内容 · Flutter的HttpClient只支持Basic和Digest两种认证方式(前者只是简单的通过Base64编码(可逆),而后者会进行哈希运算相对来说安全一点点) · 之外还有:Digest认证、Client认证、Form Based认证等 · 安全起见都应该在Https协议下 Http认证的方法和属性 · addCredentials: 添加用户凭据
httpClient.addCredentials(_uri, "admin", new HttpClientBasicCredentials("username","password"), );
· authenticate: 当服务器需要用户凭据且该用户凭据未被添加时,httpClient会调用此回调,一般这个回调会调用addCredential()来动态添加用户凭证
httpClient.authenticate=(Uri url, String scheme, String realm) async{
if(url.host=="xx.com" && realm=="admin"){
httpClient.addCredentials(url,
"admin",
new HttpClientBasicCredentials("username","pwd"),
);
return true;
}
return false;
};
· addCredentials()来添加全局凭证
- Dio http库
- Dart社区第三方http请求库
直接使用HttpClient发起网络请求是比较麻烦 涉及到文件上传/下载、Cookie管理等就会非常繁琐
- 功能
支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等
- 配置
dependencies:
dio: ^x.x.x #请使用pub上的最新版本
引入dio 的代码要放在dependencies二级
- Http分块下载
- 实例 downloadWithChunks
- 概念
HTTP分块下载,也就是断点续传下载, 根据HTTP1.1协议(RFC2616)中定义的HTTP头Range和Content-Range字段来控制的: 客户端在HTTP请求头里面指明Range,即开始下载位置 服务端在HTTP响应头里面返回Content-Range,告知下载其实点和范围
- 好处
将文件分为若干个块,然后维护一个下载状态文件用以记录每一个块的状态, 这样即使在网络中断后,也可以恢复中断前的状态
- 实际注意
分块大小多少合适? 下载到一半的块如何处理? 要不要维护一个任务队列?
- WebSockets
- 客户端与服务端实时通信而产生的技术
websocket.org提供的测试服务器 1: 连接到WebSocket服务器 web_socket_channel 提供了 WebSocketChannel 可以监听来自服务器的消息,又可以将消息发送到服务器的方法 2: 监听来自服务器的消息
new StreamBuilder(
stream: widget.channel.stream,
builder: (context, snapshot) {
return new Text(snapshot.hasData ? '${snapshot.data}' : '');
},
);
3:将数据发送到服务器
channel.sink.add('Hello!');
4:关闭WebSocket连接channel.sink.close();
报错1 Target of URI doesn't exist 表示有包没有配置的pubspec.yaml中
报错2 Insecure HTTP is not allowed by platform android端 AndroidManifest.xml 文件中修改为
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="flutter_app_vscode"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher">
- 扩展
1: 之前介绍的Http协议和WebSocket协议都属于应用层协议 就是说: 上面说到的http和websocket都是直接使用框架封装好的 2: 应用层协议的实现都是通过Socket API来实现的 3: 类似的应用层协议还有很多如:SMTP、FTP等 4: 高级编程语言中的Socket库其实都是对操作系统的socket API的一个封装 5: 如果我们需要 情况一: 自定义协议或者想直接来控制管理网络链接 情况二: 我们觉得自带的HttpClient不好用想重新实现一个 就需要使用Socket
- 实例: 简单实现()
_request() async{
//建立连接
var socket=await Socket.connect("baidu.com", 80);
//根据http协议,发送请求头
socket.writeln("GET / HTTP/1.1");
socket.writeln("Host:baidu.com");
socket.writeln("Connection:close");
socket.writeln();
await socket.flush(); //发送
//读取返回内容
_response =await socket.transform(utf8.decoder).join();
await socket.close();
}
- Json转Dart Model类
- dart:convert中内置的JSON解码器json.decode() 来实现
//一个JSON格式的用户列表字符串
String jsonStr='[{"name":"Jack"},{"name":"Rose"}]';
//将JSON字符串转为Dart对象(此处是List)
List items=json.decode(jsonStr);
//输出第一个用户的姓名
print(items[0]["name"]);
- 问题
由于json.decode()仅返回一个Map<String, dynamic>, 这意味着直到运行时我们才知道值的类型(类型安全、自动补全和最重要的编译时异常) 实例 dartModel
报错1 Unexpected character 原因可能是字符串格式有问题(这次是因为我多加了一个"") 3. 解决
“Json Model化” 通过预定义一些与Json结构对应的Model类 在请求到数据后再动态根据数据创建出Model类的实例 帖子上看,说就是一个虚拟类
- 实践
通过引入一个简单的模型类(Model class)- User 包括: User.fromJson构造函数,用来从一个map构造出一个 User实例 map structure 包括: toJson 方法,将 User 实例转化为一个map.
- 官方的 son_serializable package
自动化的源代码生成器, 为我们自动处理JSON序列化, 生成JSON序列化模板 实例: dartModel2 报错 *** Target of URL hasn't been generated: 'user.g.dart'*** 这些错误是完全正常的,这是因为Model类的生成代码还不存在。 为了解决这个问题,我们必须运行代码生成器来为我们生成序列化模板。 有两种运行代码生成器的方法: 1: 一次性生成
flutter packages pub run build_runner build
一个好的建议是将所有Model类放在一个单独的目录下,然后在该目录下执行命令。 2: 持续生成 'watcher'flutter packages pub run build_runner watch
在项目根目录下运行来启动_watcher_ 只需启动一次观察器,然后它就会在后台运行,这是安全的。
- 根据json生成模板
- template.dart 模板的模板
- mo.dart (脚本)它可以根据指定的JSON目录,遍历生成模板 如果JSON文件名以下划线“_”开始,则忽略此JSON文件。 复杂的JSON对象往往会出现嵌套,我们可以通过特殊标志来手动指定嵌套的对象
- mo.sh (shell)将生成模板和生成model串起来
- 至此,脚本写好了
- 使用: 在根目录下新建一个json目录,然后把user.json移进去, 然后在lib目录下创建一个models目录,用于保存最终生成的Model类。 现在我们只需要一句命令即可生成Model类了:
./mo.sh
- 开发Package
- 通过package可以创建共享的模块化代码
- Package包括
1: pubspec.yaml 声明了Package的名称、版本、作者等的元数据文件 2: lib 文件夹 公开的(public)代码,最少应有一个dart文件
- Package分类
1: Dart包, 包含Flutter的特定功能,对Flutter框架具有依赖性,这种包仅用于Flutter 2: 插件包, 插件包括原生代码 包含用Dart代码编写的API,以及针对Android(使用Java或Kotlin)和针对iOS(使用OC或Swift)平台的特定实现 Flutter的Dart和Dart VM(组件集合)是不同
- 开发
- 创建dart 包
1: Android Studio:File>New>New Flutter Project 创建一个Package工程 2: --template=package 来执行 flutter create 命令来创建 两个方法都会生成 lib/hello.dart:Package的Dart代码 test/hello_test.dart:Package的单元测试代码。 (hello是包的的名称,会根据创建时的名称自动生成)
- 实现package
1: 纯Dart包,只需在主lib/.dart文件内或lib目录中的文件中添加功能即可 。 2: 要测试软件包,请在test目录中添加unit tests
- 导入包
import 'package:utilities/utilities.dart';
- 生成文档
使用https://github.com/dart-lang/dartdoc#dartdoc,为包生成文档. 开发者需要做的就是遵守文档注释语法在代码中添加文档注释,最后使用dartdoc可以直接生成API文档(一个静态网站) 文档注释是使用三斜线"
- 处理包的相互依赖
需要将该依赖包添加到pubspec.yaml文件的dependencies部分
dependencies:
url_launcher: ^0.4.2
使用
import 'package:url_launcher/url_launcher.dart'
androidandroid/build.gradle
android {
// lines skipped
dependencies {
provided rootProject.findProject(":url_launcher")
}
}
在android/src中使用
import io.flutter.plugins.urllauncher.UrlLauncherPlugin
iOSios/hello.podspec
Pod::Spec.new do |s|
# lines skipped
s.dependency 'url_launcher'
在ios/Classes中使用
#import "UrlLauncherPlugin.h"
- 冲突
假设我们想在我们的hello包中使用some_package和other_package 并且这两个包都依赖url_launcher,但是依赖的是url_launcher的不同的版本 避免这种情况的最好方法是在指定依赖关系时,程序包作者使用版本范围 (opens new window)而不是特定版本
- 发布Package
发布之前,检查pubspec.yaml、README.md以及CHANGELOG.md文件,以确保其内容的完整性和正确性 查看是否准备ok
flutter packages pub publish --dry-run
发布flutter packages pub publish
代理export all_proxy=socks5://127.0.0.1:1080
#### 7/5
- 插件开发: 平台通道简介
- “平台特定”或“特定平台”
平台指的就是Flutter应用程序运行的平台 完整的Flutter应用程序实际上包括原生代码和Flutter代码两部分
- platform channel
Flutter中提供了的一个平台通道 1: Flutter APP和原生平台进行通信 2: 调用平台能力,如: 蓝牙、相机、GPS等 3: lutter本身只是一个UI系统,它本身是无法提供一些系统能力 4: 是Flutter插件的底层基础设施 5: 灵活的系统(无论在Android上的Java或Kotlin代码中,还是iOS上的ObjectiveC或Swift代码中均可用)
- 消息传递方式
1: 通过platform channel将消息发送到其宿主应用(原生应用) 2: 宿主监听平台通道,并接收该消息. 3: 然后调用该平台的API,并将响应(如果有数据是异步的)发送回客户端(应用程序的Flutter部分)
- MethodChannel
MethodChannel API 可以发送与方法调用相对应的消息 在宿主平台上(android 和 ios) 可以接收方法调用并返回结果 属于自定义编解码器,类似的还有BasicMessageChannel
- 获取平台信息
defaultTargetPlatform 用来获取平台信息 是一个枚举 使用
if(defaultTargetPlatform==TargetPlatform.android){ // 是安卓系统,do something }
其他用法 假如: 想让我们的APP在所有平台都表现一致 比如: 比如希望在所有平台路由切换动画都按照ios平台一致的左右滑动切换风格 可以通过显式指定debugDefaultTargetPlatformOverride全局变量的值来指定应用平台
debugDefaultTargetPlatformOverride=TargetPlatform.iOS;
print(defaultTargetPlatform);
上述代码会输出TargetPlatform.iOS defaultTargetPlatform的值也会变为TargetPlatform.iOS
- 开发Flutter插件
- 介绍
获取电池电量的插件 我们在Dart中通过getBatteryLevel 调用Android BatteryManager API和iOS device.batteryLevel API
1: 创建一个新的应用程序项目(之前讲的步骤) 2: 首先,我们构建通道 单个应用中使用的所有通道名称必须是唯一的; 我们建议在通道名称前加一个唯一的“域名前缀” 例如samples.flutter.io/battery main的state类中加入
static const platform = const MethodChannel('samples.flutter.io/battery');
接下来,我们调用通道上的方法,指定通过字符串标识符调用方法getBatteryLevel。 该调用可能失败(平台不支持平台API,例如在模拟器中运行时), 我们将invokeMethod调用包装在try-catch语句中。
- 看懂开发flutter插件
- 创建flutter应用
- 创建Flutter平台客户端
MethodChannel _getBatteryLevel RaisedButton
- Android端API实现 复制官方代码无法启动项目
- 原生和Flutter之间如何共享图像的方法
Texture
和PlatformView
- 以及如何在Flutter中嵌套原生组件
- flutter局限
他的平台通道,消息传送不能覆盖所有的应用场景 摄像头拍照录视频(如果把图像每一帧都传递到flutter应用,代价非常大:内存和CPU的巨大消耗) Flutter提供了一种基于
Texture
的图片数据共享机制。
Texture
是一个gpu内存将要绘制的图像数据对象
Flutter engine会将
Texture
的数据在内存中直接进行映射(而无需在原生和Flutter之间再进行数据传递) Flutter会给每一个Texture
分配一个id,同时Flutter中提供了一个Texture
组件Texture
组件正是通过textureId与Texture
数据关联起来 整个流程 1: 图像数据先在原生部分缓存 2: Flutter部分再通过textureId和缓存关联 3: 绘制由Flutter完成
- 如果我们开发插件
textureId完全可以通过
MethodChannel
来传递。
- 注意
原生摄像头捕获的图像发生变化时,
Texture
组件会自动重绘,这不需要我们写任何Dart 代码去控制。
- Texture用法
- Flutter官方提供的相机
camera
插件和视频播放video_player
插件都是使用Texture来实现的,它们本身就是Texture非常好的示例 camera
包自带的一个示例
1: 可以拍照,也可以拍视频,拍摄完成后可以保存;排号的视频可以播放预览。 2: 可以切换摄像头(前置摄像头、后置摄像头、其它) 3: 可以显示已经拍摄内容的预览图。
- 看一下
camera
具体代码
1: 依赖
camera
插件的最新版
pubspec.yaml
camera: ^0.5.2+2
2: 在main方法中获取可用摄像头列表。
void main() async {
// 获取可用摄像头列表,cameras为全局变量
cameras = await availableCameras();
runApp(MyApp());
}
3: 构建UI 4: 完整代码(camera.dart)
PlatformView
(平台组件)
1: 开发过程中需要使用一个原生组件 例如: webview 2: 将需要使用原生组件的页面全部用原生实现,在flutter中需要打开该页面时通过消息通道打开这个原生的页面 3: Flutter SDK中新增了AndroidView和UIKitView 两个组件, 这两个组件的主要功能就是将原生的Android组件和iOS组件嵌入到Flutter的组件树中,能让Flutter共享原生组件 4: 由于AndroidView和UIKitView 是和具体平台相关的,所以称它们为
PlatformView
5: 使用Platform View 以Flutter官方提供的webview_flutter
插件为例 1: 原生代码中注册要被Flutter嵌入的组件工厂
public static void registerWith(Registrar registrar) {
registrar.platformViewRegistry().registerViewFactory("webview",
WebViewFactory(registrar.messenger()));
}
2: 在Flutter中使用 打开flutter中文首页
class PlatformViewRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return WebView(
initialUrl: "https://flutterchina.club",
javascriptMode: JavascriptMode.unrestricted,
);
}
}
注意 用
PlatformView
的开销是非常大的,因此,如果一个原生组件用Flutter实现的难度不大时,我们应该首选Flutter实现。
- 多语言
- Flutter SDK已经提供了一些组件和类来帮助我们实现国际化,下面我们来介绍一下Flutter中实现国际化的步骤
1: 下面举例
MaterialApp
类为入口的应用来说明如何支持国际化 2: 大多数应用程序都是通过MaterialApp
为入口,MaterialApp
实际上也是WidgetsApp的一个包装 3: 本地化的值和资源指我们针对不同语言准备的不同资源,资源一般是指文案(字符串) 4: 默认情况,仅提供美国英语本地化资源 要添加对其他语言的支持,应用程序须添加一个名为flutter_localizations
的包依赖 还需要在MaterialApp
中进行一些配置
- 使用
flutter_localizations
包
1: 添加依赖
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
2: 下载
flutter_localizations
3: 然后指定MaterialApp的localizationsDelegates
和supportedLocales
,
import 'package:flutter_localizations/flutter_localizations.dart';
new MaterialApp(
localizationsDelegates: [
// 本地化的代理类
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'), // 美国英语
const Locale('zh', 'CN'), // 中文简体
//其它Locales
],
// ...
)
1:
localizationsDelegates
列表中的元素是生成本地化值集合的工厂。 2:GlobalMaterialLocalizations.delegate
为Material 组件库提供的本地化的字符串和其他值,它可以使Material 组件支持多语言。 3:GlobalWidgetsLocalizations.delegate
定义组件默认的文本方向,从左到右或从右到左,这是因为有些语言的阅读习惯并不是从左到右,比如如阿拉伯语就是从右向左的。 4:supportedLocales
也接收一个Locale数组,表示我们的应用支持的语言列表,在本例中我们的应用只支持美国英语和中文简体两种语言
- 获取当前区域Locale
Locale用来标识用户的语言环境的,它包括语言和国家两个标志如
const Locale('zh', 'CN')
获取应用的当前区域Locale
Locale myLocale = Localizations.localeOf(context);
4. 监听系统语言切换
切换语言这个过程是隐式完成的 可以通过
localeResolutionCallback
或localeListResolutionCallback
回调来监听locale改变的事件localeResolutionCallback
的回调函数签名:
Locale Function(Locale locale, Iterable<Locale> supportedLocales)
1: 参数locale的值为当前的当前的系统语言设置 2:
supportedLocales
为当前应用支持的locale列表,是开发者在MaterialApp中通过supportedLocales
属性注册的。 3: 返回值是一个Locale,此Locale为Flutter APP最终使用的Locale。通常在不支持的语言区域时返回一个默认的Locale。
使用
localeListResolutionCallback
方法 前者接收的是一个Locale列表,而后者接收的是单个Locale
Locale Function(List<Locale> locales, Iterable<Locale> supportedLocales)
- Localization 组件
1: 前面提到的Localizations组件用于加载和查找应用当前语言下的本地化值或资源 2: 通过
Localizations.of(context,type)
(opens new window)来引用这些对象 3: 如果设备的Locale区域设置发生更改,则Localizations 组件会自动加载新区域的Locale值,然后重新build使用(依赖)了它们的组件, 4: 大型应用程序中,不同模块或Package可能会与自己的本地化值捆绑在一起 5: Localizations.of()表达式会经常使用,所以MaterialLocalizations类提供了一个便捷方法
static MaterialLocalizations of(BuildContext context) {
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
}
// 可以直接调用便捷方法
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
- flutter软件包中仅提供美国英语值的
MaterialLocalizations
和WidgetsLocalizations
接口的实现
- 昨天的camera报错,
import 'package:camera/camera.dart';
List<CameraDescription> cameras;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
cameras = await availableCameras();
runApp(MyApp());
}
重新添加依赖和 把mian文件修改成上面的代码没有报错了 参考的cameras博客
camera有报错
pubspec 引入
video_player: ^0.10.11+1
path_provider: ^1.6.9
接下来尝试打开 电脑打开失败 MissingPluginException (MissingPluginException(No implementation found for method availableCameras on channel plugins.flutter.io/camera)) 尝试手机打开 没有设备 注释了首页的
// cameras = await availableCameras();
可以打开
- 尝试国际化
没有想到使用的实际场景
- 实现Localizations
GlobalMaterialLocalizations
和GlobalWidgetsLocalizations
只是Material组件库的本地化实现,
想要让自己的布局支持多语言,那么就需要实现在即的Localizations
- 开始
1: 实现
Localizations
类Localizations
类 实现提供了本地化值 示例 Localizations 2: 实现Delegate
类Delegate
类 在Locale改变时加载新的Locale资源Delegate
类需要继承自LocalizationsDelegate类 它有一个load方法 示例 Delegate shouldReload 方法返回值决定,build时,是否调用load方法重新加载Locale资源 一版返回false就好 而且,每当Locale改变时Flutter都会再调用load方法加载新的Locale 3: 添加多语言支持 先注册DemoLocalizationsDelegate类 在MaterialApp或WidgetsApp的localizationsDelegates列表中添加 Delegate实例即可完成注册 再通过DemoLocalizations.of(context)来动态获取当前Locale文本 接下来我们可以在Widget中使用Locale值 示例 (在main文件中添加)
return Scaffold(
appBar: AppBar(
//使用Locale title
title: Text(DemoLocalizations.of(context).title),
),
... //省略无关代码
)
当在美国英语和中文简体之间切换系统语言时,APP的标题会自动切换
- 问题
1: 如果我们要支持的语言不是两种而是8种甚至20几种时 如果为每个文本属性都要分别去判断到底是哪种Locale从而获取相应语言的文本 将会是一件非常复杂的事 2: 通常情况下翻译人员并不是开发人员 能否可以将翻译单独保存为一个arb文件交由翻译人员去翻译 翻译好之后开发人员再通过工具将arb文件转为代码
- 可以通过Dart intl包来实现这些
- Intl 包
- 干嘛的
实现国际化 ,把字符串分离成单独的文件(方便开发和翻译人员) 1: 从代码中提取要国际化的字符串到单独的arb文件 2: 根据arb文件生成对应语言的dart代码
- 依赖
dependencies:
intl: ^0.15.7
dev_dependencies:
intl_translation: ^0.17.2
intl包主要是引用和加载
intl_translation
生成后的dart代码
- 使用
1: 根目录创建一个l10n-arb目录(用来保存
intl_translation
命令生成的 arb文件) arb文件内容示例
{
"@@last_modified": "2018-12-10T15:46:20.897228",
"@@locale":"zh_CH",
"title": "Flutter应用",
"@title": {
"description": "Title for the Demo application",
"type": "text",
"placeholders": {}
}
}
示例中
@@locale
的值表示为中文 示例中title
对应中文简体翻译 示例中@title
对title的一些描述信息 2: 根目录创建l10n目录(保存从arb文件生成的dart代码文件) 3: 实现Localizations和Delegate类 示例 localization_intl.dart 4: 添加需要国际化的属性 DemoLocalizations类中添加需要国际化的属性或方法 这时我们就要用到Intl库提供的一些方法 示例 一个电子邮件列表页,我们需要在顶部显示未读邮件的数量 在未读数量不同事,我们展示的文本不同
未读邮件数 | 提示语 |
---|---|
0 | There are no emails left |
1 | There is 1 email left |
n(n>1) | There are n emails left |
实现方法: Intl.plural(...) // Intl 包还有一些其他的方法,这里只是示例
remainingEmailsMessage(int howMany) => Intl.plural(
howMany,
zero: 'There are no emails left',
one: 'There is $howMany email left',
other: 'There are $howMany emails left',
name: "remainingEmailsMessage",
args: [howMany],
desc: "How many emails remain after archiving.",
examples: const {'howMany': 42, 'userName': 'Fred'},
);
5: 生成arb文件 可以通intl_translation包的工具来提取代码中的字符串到一个arb文件 执行命令
flutter pub pub run intl_translation:extract_to_arb --output-dir=l10n-arb \ lib/l10n/localization_intl.dart
报错: The pubspec.yaml file has changed since the pubspec.lock file was generated, please run "pub get" again. a: 执行 'flutter pub get' 报错 Because flutter_app_vscode depends on flutter_localizations any from sdk which depends on intl 0.17.0, intl 0.17.0 is required. So, because flutter_app_vscode depends on intl ^0.15.7, version solving failed. 修改pubspec文件的intl版本为17以上, 重新执行 还是提示pub get 报错
Because intl_translation >=0.17.7 depends on intl >=0.15.3 <0.17.0 and intl_translation >=0.17.0 <0.17.7 depends on intl ^0.15.3, intl_translation >=0.17.0 requires intl >=0.15.3 <0.17.0.
解决问题失败,回到之前**intl ^0.15.7,**问题 参考 博客 使用 dependency_overrides暂时解决 接下来继续 (上面文字部分是在7/9日早上写的) 7/10早上继续 执行pub get 警告
you are using these overrideden dependencies
博客说不用太在意 执行
flutter pub pub run intl_translation:extract_to_arb --output-dir=l10n-arb \ lib/l10n/localization_intl.dart
报错 The getter 'elements2' isn't defined for the class 'ListLiteral' 博客上没答案,应该是版本问题 结束
- Intl 的总结
- 第二步和第一步只在第一次需要,开发的主要工作在第三步
- 最后两步命令,可以放在shell脚本里面(完成第三步或者完成arb文件翻译后执行)
创建intl.sh 文件 执行
chmod +x intl.sh
(chmod +x xxx.sh: 表示为xxx文件增加可执行权限) 然后就可以执行./intl.sh
了
- 国际化的常见问题
- 默认的Locale不是中文简体:
非大陆行货渠道买的一些Android和iOS设备,会出现的情况 为了防止设备获取的Locale与实际的地区不一致 app都必须提供一个手动选择语言的入口
- 对应用标题进行国际化
MaterialApp有一个title属性来指定APP的标题 问题在于: 无法在构建MaterialApp时通过Localizations.of来获取本地化资源
MaterialApp(
title: DemoLocalizations.of(context).title, //不能正常工作!
localizationsDelegates: [
// 本地化的代理类
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
DemoLocalizationsDelegate() // 设置Delegate
],
);
1: Localizations.of会从当前的context沿着widget树向顶部查找DemoLocalizations 2: 但是实际上DemoLocalizations是在当前context的子树中的 DemoLocalizations.of(context)会返回null 3: 解决办法 设置一个onGenerateTitle回调
MaterialApp(
onGenerateTitle: (context){
// 此时context在Localizations的子树中
return DemoLocalizations.of(context).title;
},
localizationsDelegates: [
DemoLocalizationsDelegate(),
...
],
);
为英语系的国家指定同一个locale 1: 提供一种英语(如美国英语)供所有英语系国家使用 可以在前面介绍的localeListResolutionCallback中来做兼容:
localeListResolutionCallback:
(List<Locale> locales, Iterable<Locale> supportedLocales) {
// 判断当前locale是否为英语系国家,如果是直接返回Locale('en', 'US')
}
- 触碰到核心
- UI系统的含义
1: 基于一个平台(操作系统) 2: 在该平台实现GUI的一个系统
- 注意
1: 各个平台UI系统的原理是相通的 2: 无论是Android还是iOS,他们将一个用户界面展示到屏幕的流程是相似的
3: UI系统的原理
1: 屏幕显示图像的基本原理 显示器 显示器由一个个物理显示单元(物理像素点)组成 显示器成相原理: 在不同的物理像素点上显示不同的颜色构成完整的图像 位色 位色: 是显示器的一个重要指标 位色指: 一个像素点能发出的所有颜色总数是2的几次方 例如:1600万即2的24次方,称为24位色 刷新频率 显示画面 就是: 以固定的频率刷新 刷新需要从GPU获取数据 每次刷新: 显示器会发出一个垂直同步信号,用来同步CPU、GPU和显示器的 CPU、GPU和显示器的协作方式: CPU将计算好的显示内容提交给 GPU,GPU渲染后放入帧缓冲区,视频控制器按照同步信号从帧缓冲区取帧数据,并且传递给显示器显示 (CPU主要用于基本数学和逻辑计算,GPU的主要作用就是确定最终输送给显示器的各个像素点的色值) 例如: 一部手机屏幕的刷新频率是 60Hz,就是屏幕就会一秒内发出 60次这样的信号 2: 操作系统绘制API a: 图形计算和绘制是由相应的硬件来完成 b: 直接操作硬件的指令通常都会有操作系统屏蔽 c: 操作系统提供一些封装后的API,供操作系统之上的应用调用 d: 操作系统提供的API往往比较基础,直接调用比较复杂和低效的,需要了解API的很多细节 e: 几乎所有关于开发GUI程序的编程语言都会在操作系统之上再封装一层(操作系统原生API封装在一个编程框架和模型中,然后定义一种简单的开发规则来开发GUI应用程序) f: 我们所说的“UI”系统,就是指这个 g:
ui系统 | 被封装的系统 |
---|---|
Android SDK(中:UI描述文件XML+Java操作DOM) | Android操作系统 |
UIKit | ios操作系统 |
3: Flutter UI系统 Flutter的原理 a: 使用同一种编程语言开发 b: 不同操作系统API抽象一个的中间层(Dart API) c: 在打包编译时再使用相应的中间层代码 d: 底层使用OpenGL这种跨平台的绘制库(OpenGL只是操作系统API的一个封装库,相当于直接调用操作系统API) 4: 组合和响应式 Flutter UI系统对应用开发者定义的开发标准就是: 组合和响应式 理解: Flutter中,一切都是Widget,一个UI界面通过组合其它Widget来实现 理解: UI要发生变化时,不去直接修改DOM,而是通过更新状态,让Flutter UI系统来根据新的状态来重新构建UI
- Element与BuildContext
- 基础知识
最终的UI树其实是由一个个独立的Element节点构成 组件最终的Layout、渲染都是通过RenderObject来完成的
- 流程
1: 根据Widget生成Element 2: 创建相应的RenderObject 3: 关联到Element.renderObject属性上 4: 通过RenderObject来完成布局排列和绘制
- 功能
Widget 树: 咱们写的组件 Element 树: 页面上的一个个节点 RenderObject 树: 每个节点对应的渲染对象
- Element的生命周期
1: 创建, Framework会调用Widget.createElement 创建一个Element实例 2: active状态, Framework会调用element.mount方法会创建RenderObject对象,并且添加到渲染树后的状态 3: 复用, 在更新前会调用对应Widget的canUpdate方法,判断是否复用(可以通过指定不同的Key来避免复用) 4: inactive状态, 移除element 时,Element就会调用deactivateChild 方法来移除它,element.renderObject也会被从渲染树中移除,然后Framework会调用element.deactivate方法,状态变成inactive,不会再显示到屏幕 5: defunct状态, inactive的element在动画执行结束后它还未能重新变成active状态,Framework就会调用其unmount方法将其彻底移除,变成defunct,不会再被插入到树中.
- BuildContext
1: context 的初步使用
功能 | 代码 |
---|---|
获取主题 | Theme.of(context) |
获取主题 | Theme.of(context) |
入栈新路由 | Navigator.push(context, route) |
2: 介绍 BuildContext是一个抽象接口类
abstract class BuildContext {}
抽象类必须要有实现才能用 接下来看context的实现类
class StatelessElement extends ComponentElement {
...
@override
Widget build() => widget.build(this);
...
}
源码1: this 就是 StatelessElement 源码2: StatelessElement 继承 Element 类
class Element extends DiagnosticableTree implements BuildContext {
...
}
源码3: Element 有BuildContext 接口
结论: BuildContext就是widget对应的Element
- buildContext 进阶
- BuildContext 就是 Element对象,
- 大多数时候开发者只需要关注widget层,在build时传入Element对象, 就是为了在需要时候直接操作
- 考验理解力的两个问题
- 通过Element来搭建一个UI框架?(示例: HomeView ) 如果在Flutter框架中所有组件都像示例的HomeView一样以Element形式提供,那么就可以用纯Element来构建UI了HomeView的build方法返回值类型就可以是Element了。
- flutter能不能做响应式?
- 布局过程: 1 RenderObject 和 RenderBox
- 每个Element都对应一个RenderObject,RenderObject职责是Layout和绘制
- 简单理解
RenderObject就是渲染树中的一个对象, 它拥有一个parent和一个parentData 插槽(slot)(有点不理解)
- 布局过程
1: RenderBox 的layout是通过传递 BoxConstraints 对象实现的 BoxConstraints可以限制子节点的最大和最小宽高 布局时: 父节点会调用子节点的layout()方法
void layout(Constraints constraints, { bool parentUsesSize = false }) {
...
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight
|| parent is! RenderObject) {
relayoutBoundary = this;
} else {
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
...
if (sizedByParent) {
performResize();
}
performLayout();
...
}
1: constraints 是对子节点大小的限制 2: parentUsesSize(布尔值,表示子节点布局变化是否影响父节点) parentUsesSize 同时用于确定 relayoutBoundary 3: relayoutBoundary 想知道relayoutBoundary,得先知道markNeedsLayout markNeedsLayout(1) 前面的只是中,当Element标记为 dirty 时便会重新build markNeedsLayout(2) 调用 markNeedsBuild() 可以标记Element为dirty markNeedsLayout(3) 类似的, RenderObject中markNeedsLayout()方法也可以标记为dirty markNeedsLayout(4) 部分源码
void markNeedsLayout() {
...
assert(_relayoutBoundary != null);
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
...
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
markNeedsLayout(5) 源码可知: markNeedsLayout会向上查找到是 relayoutBoundary 的 RenderObject为止,然后再将其标记为 dirty markNeedsLayout(6) 也就是说:如果一个 RenderObject 是 relayoutBoundary,就表示它的大小变化不会再影响到 parent 的大小了,于是 parent 也就不用重新布局了
- 布局过程: 2 performResize 和 performLayout
- performLayout 每次布局都会被调用
- performResize sizedByParent 为 true 时调用
- sizedByParent 该节点的大小(属性)和其子节点是否无关
- performLayout() 方法中除了完成自身布局,也必须完成子节点的布局,这是因为只有父子节点全部完成后布局流程才算真正完成。
- 所以最终的调用栈将会变成:layout() > performResize()/performLayout() > child.layout() > ... ,如此递归完成整个UI的布局。
- 布局过程: 3 ParentData
- layout结束后, 节点的位置就确定了, RenderObject就可以进行绘制
- 那么,节点的位置信息怎么保存(假如多个子组件,给每一个子组件设置一个位置)
- RenderObject的parentData属性来保存
parentData属性默认是一个BoxParentData对象 该属性只能通过父节点的setupParentData()方法来设置
abstract class RenderBox extends RenderObject {
@override
void setupParentData(covariant RenderObject child) {
if (child.parentData is! BoxParentData)
child.parentData = BoxParentData();
}
...
}
- ParentData并不仅仅可以用来存储偏移信息,通常所有和子节点特定的数据都可以存储到子节点的ParentData中
- 绘制过程: 1
- 通过paint()方法来完成具体绘制逻辑
签名
void paint(PaintingContext context, Offset offset) { }
context.canvas可以取到Canvas对象, 接下来调用Canvas API来实现具体的绘制逻辑
- 当有子节点的情况,除了完成自身绘制逻辑之外,还要调用子节点的绘制方法
部分源码
// 如果子元素未超出当前边界,则绘制子元素
if (_overflow <= 0.0) {
defaultPaint(context, offset);
return;
}
// 如果size为空,则无需绘制
if (size.isEmpty)
return;
// 剪裁掉溢出边界的部分
context.pushClipRect(needsCompositing, offset, Offset.zero & size, defaultPaint);
assert(() {
final String debugOverflowHints = '...'; //溢出提示内容,省略
// 绘制溢出部分的错误提示样式
Rect overflowChildRect;
switch (_direction) {
case Axis.horizontal:
overflowChildRect = Rect.fromLTWH(0.0, 0.0, size.width + _overflow, 0.0);
break;
case Axis.vertical:
overflowChildRect = Rect.fromLTWH(0.0, 0.0, 0.0, size.height + _overflow);
break;
}
paintOverflowIndicator(context, offset, Offset.zero & size,
overflowChildRect, overflowHints: debugOverflowHints);
return true;
}());
根据有无溢出,调用defaultPaint(context, offset)来完成绘制 当需要绘制的内容大小溢出当前空间时,将会执行paintOverflowIndicator() 来绘制溢出部分提示,这个就是我们经常看到的溢出提示 defaultPaint 部分代码
void defaultPaint(PaintingContext context, Offset offset) {
ChildType child = firstChild;
while (child != null) {
final ParentDataType childParentData = child.parentData;
//绘制子节点,
context.paintChild(child, childParentData.offset + offset);
child = childParentData.nextSibling;
}
}
defaultPaint中会调用paintChild()来绘制子节点
- 绘制过程 2 RepaintBoundary
- RepaintBoundary 和 RelayoutBoundary 相似
- 用于在确定重绘边界, 另外绘制边界需要由开发者通过RepaintBoundary 组件自己指定
例如:
CustomPaint(
size: Size(300, 300), //指定画布大小
painter: MyPainter(),
child: RepaintBoundary(
child: Container(...),
),
),
- RepaintBoundary的原理
void paintChild(RenderObject child, Offset offset) {
...
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
...
}
isRepaintBoundary该属性决定这个RenderObject重绘时是否独立于其父元素 _compositeChild源码
void _compositeChild(RenderObject child, Offset offset) {
// 给子节点创建一个layer ,然后再上面绘制子节点
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else {
...
}
assert(child._layer != null);
child._layer.offset = offset;
appendLayer(child._layer);
}
通过在不同的layer(层)上绘制的(正确使用isRepaintBoundary属性可以提高绘制效率,避免不必要的重绘) RenderObject也提供了一个markNeedsPaint()方法
void markNeedsPaint() {
...
//如果RenderObject.isRepaintBoundary 为true,则该RenderObject拥有layer,直接绘制
if (isRepaintBoundary) {
...
if (owner != null) {
//找到最近的layer,绘制
owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate();
}
} else if (parent is RenderObject) {
// 没有自己的layer, 会和一个祖先节点共用一个layer
assert(_layer == null);
final RenderObject parent = this.parent;
// 向父级递归查找
parent.markNeedsPaint();
assert(parent == this.parent);
} else {
// 如果直到根节点也没找到一个Layer,那么便需要绘制自身,因为没有其它节点可以绘制根节点。
if (owner != null)
owner.requestVisualUpdate();
}
}
调用 markNeedsPaint() 从当前 RenderObject 开始一直向父节点查找 直到找到 一个isRepaintBoundary 为 true的RenderObject 时,才会触发重绘 可以实现局部重绘 开发中通过RepaintBoundary Widget来指定isRepaintBoundary 为 true,绘制时仅会重绘自身而无需重绘它的 parent,如此便可提高性能。 如果使用了RepaintBoundary,其对应的RenderRepaintBoundary会自动将isRepaintBoundary设为true的(如下)
class RenderRepaintBoundary extends RenderProxyBox {
/// Creates a repaint boundary around [child].
RenderRepaintBoundary({ RenderBox child }) : super(child);
@override
bool get isRepaintBoundary => true;
}
- 命中测试
- 一个对象是否可以响应事件,取决于其对命中测试的返回
当发生用户事件时,会从根节点(RenderView)开始进行命中测试 RenderView默认的hitTest()如下
bool hitTest(HitTestResult result, { Offset position }) {
if (child != null)
child.hitTest(result, position: position); //递归子RenderBox进行命中测试
result.add(HitTestEntry(this)); //将测试结果添加到result中
return true;
}
RenderBox默认的hitTest()如下
bool hitTest(HitTestResult result, { @required Offset position }) {
...
if (_size.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
- hitTest
用来判断该RenderObject 是否在被点击的范围内 同时负责将被点击的 RenderBox 添加到 HitTestResult 列表中 参数 position 为事件触发的坐标 回 true 则表示有RenderBox 通过了命中测试 可以直接重写hitTest()方法
- 总结
从头到尾实现一个RenderObject是比较麻烦的,我们必须去实现layout、绘制和命中测试逻辑Front-End
- 运行机制
- 启动
入口: 在"lib/main.dart"的main()函数中,Dart应用程序的起点 main()函数
void main(){runApp(MyApp())}
runApp()方法
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
1: app是一个widget,它是Flutter应用启动后要展示的第一个Widget 2: WidgetsFlutterBinding是绑定widget 框架和Flutter engine的桥梁 WidgetsFlutterBinding
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}
1: Binding 了解Binding之前先了解一下 Window Window
class Window {
// 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。
// DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5
double get devicePixelRatio => _devicePixelRatio;
// Flutter UI绘制区域的大小
Size get physicalSize => _physicalSize;
// 当前系统默认的语言Locale
Locale get locale;
// 当前系统字体缩放比例。
double get textScaleFactor => _textScaleFactor;
// 当绘制区域大小改变回调
VoidCallback get onMetricsChanged => _onMetricsChanged;
// Locale发生变化回调
VoidCallback get onLocaleChanged => _onLocaleChanged;
// 系统字体缩放变化回调
VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
// 绘制前回调,一般会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用
FrameCallback get onBeginFrame => _onBeginFrame;
// 绘制回调
VoidCallback get onDrawFrame => _onDrawFrame;
// 点击或指针事件回调
PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
// 调度Frame,该方法执行后,onBeginFrame和onDrawFrame将紧接着会在合适时机被调用,
// 此方法会直接调用Flutter engine的Window_scheduleFrame方法
void scheduleFrame() native 'Window_scheduleFrame';
// 更新应用在GPU上的渲染,此方法会直接调用Flutter engine的Window_render方法
void render(Scene scene) native 'Window_render';
// 发送平台消息
void sendPlatformMessage(String name,
ByteData data,
PlatformMessageResponseCallback callback) ;
// 平台通道消息处理回调
PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;
}
是 Flutter Framework连接宿主操作系统的接口 包含了当前设备和系统的一些信息以及Flutter Engine的一些回调
再回来看看 WidgetsFlutterBinding 混入的各种 Binding 这些binding 基本都是监听并处理Window对象的一些事件 再理解 WidgetsFlutterBinding, 它正是粘连Flutter engine与上层Framework的“胶水”
2: ensureInitialized: 负责初始化一个WidgetsBinding的全局单例 3: attachRootWidget: 负责将根Widget添加到RenderView上
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}
renderView: 是一个RenderObject,它是渲染树的根 renderViewElement: 是renderView对应的Element对象 attachToRenderTree
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
attachToRenderTree(RenderObjectToWidgetElement)负责创建根element, 并且将element与widget 进行关联(创建出 widget树对应的element树)
- 渲染
attachRootWidget后
WidgetsFlutterBinding.scheduleWarmUpFrame()方法 它被调用后会立即进行一次绘制 在本次绘制结束完成之前Flutter将不会响应各种事件(绘制结束前,该方法会锁定事件分发) 保证在绘制过程中不会再触发新的重绘
scheduleWarmUpFrame()方法
void scheduleWarmUpFrame() {
...
Timer.run(() {
handleBeginFrame(null);
});
Timer.run(() {
handleDrawFrame();
resetEpoch();
});
// 锁定事件
lockEvents(() async {
await endOfFrame;
Timeline.finishSync();
});
...
}
handleBeginFrame() 和 handleDrawFrame() 了解上面两个方法,先了解Frame 和 FrameCallback 的概念 Frame: 一次绘制过程,我们称其为一帧。Flutter engine受显示器垂直同步信号"VSync"的驱使不断的触发绘制。我们之前说的Flutter可以实现60fps(Frame Per-Second),就是指一秒钟可以触发60次重绘,FPS值越大,界面就越流畅。 FrameCallback:SchedulerBinding 类中有三个FrameCallback回调队列, 在一次绘制过程中,这三个回调队列会放在不同时机被执行: FrameCallback回调队列: transientCallbacks:用于存放一些临时回调,一般存放动画回调。可以通过SchedulerBinding.instance.scheduleFrameCallback 添加回调。 FrameCallback回调队列: persistentCallbacks:用于存放一些持久的回调,不能在此类回调中再请求新的绘制帧,持久回调一经注册则不能移除。SchedulerBinding.instance.addPersitentFrameCallback(),这个回调中处理了布局与绘制工作。 FrameCallback回调队列: postFrameCallbacks:在Frame结束时只会被调用一次,调用后会被系统移除,可由 SchedulerBinding.instance.addPostFrameCallback() 注册,注意,不要在此类回调中再触发新的Frame,这可以会导致循环刷新。
再来看handleBeginFrame() 和 handleDrawFrame() 前者主要是执行了transientCallbacks队列,而后者执行了 persistentCallbacks 和 postFrameCallbacks 队列
- 绘制
渲染和绘制逻辑在RendererBinding中实现 RendererBinding中的initInstances()方法
void initInstances() {
... //省略无关代码
//监听Window对象的事件
ui.window
..onMetricsChanged = handleMetricsChanged
..onTextScaleFactorChanged = handleTextScaleFactorChanged
..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
..onSemanticsAction = _handleSemanticsAction;
//添加PersistentFrameCallback
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
}
addPersistentFrameCallback 向persistentCallbacks队列添加回调 _handlePersistentFrameCallback _handlePersistentFrameCallback方法直接调用了RendererBinding的drawFrame()
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); //布局
pipelineOwner.flushCompositingBits(); //重绘之前的预处理操作,检查RenderObject是否需要重绘
pipelineOwner.flushPaint(); // 重绘
renderView.compositeFrame(); // 将需要绘制的比特数据发给GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
flushLayout: 更新了所有被标记为“dirty”的RenderObject的布局信息 flushCompositingBits: 检查RenderObject是否需要重绘,然后更新RenderObject.needsCompositing属性 flushPaint: 该方法进行了最终的绘制,只重绘了需要重绘的 RenderObject compositeFrame: 将Canvas画好的Scene传给window.render()方法 window.render(): 会直接将scene信息发送给Flutter engine,最终由engine将图像画在设备屏幕上
- 图片加载原理与缓存
- ImageProvider
Image 组件的image 参数是一个必选参数,它是ImageProvider类型 是一个抽象类 定义了图片数据获取和加载的相关接口 提供图片数据源, 和缓存图片
abstract class ImageProvider<T> {
ImageStream resolve(ImageConfiguration configuration) {
// 实现代码省略
}
Future<bool> evict({ ImageCache cache,
ImageConfiguration configuration = ImageConfiguration.empty }) async {
// 实现代码省略
}
Future<T> obtainKey(ImageConfiguration configuration);
@protected
ImageStreamCompleter load(T key); // 需子类实现
}
load(T key)方法: 加载图片数据源的接口 以NetworkImage为例,看看其load方法的实现
@override
ImageStreamCompleter load(image_provider.NetworkImage key) {
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents), //调用
chunkEvents: chunkEvents.stream,
scale: key.scale,
... //省略无关代码
);
}
MultiFrameImageStreamCompleter 是ImageStreamCompleter子类 其中ImageStreamCompleter 是一个抽象类,定义了管理图片加载过程的一些接口,通过它来监听图片加载状态的 MultiFrameImageStreamCompleter 需要一个codec参数,类型为: Future<ui.Codec> Codec 是处理图片编解码的类的一个handler,它只是一个flutter engine API 的包装类 也就是说图片的编解码逻辑不是在Dart 代码部分实现,而是在flutter engine中实现的 Codec代码
@pragma('vm:entry-point')
class Codec extends NativeFieldWrapperClass2 {
// 此类由flutter engine创建,不应该手动实例化此类或直接继承此类。
@pragma('vm:entry-point')
Codec._();
/// 图片中的帧数(动态图会有多帧)
int get frameCount native 'Codec_frameCount';
/// 动画重复的次数
/// * 0 表示只执行一次
/// * -1 表示循环执行
int get repetitionCount native 'Codec_repetitionCount';
/// 获取下一个动画帧
Future<FrameInfo> getNextFrame() {
return _futurize(_getNextFrame);
}
String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';
Codec最终的结果是一个或多个(动图)帧
MultiFrameImageStreamCompleter 的 codec参数值为_loadAsync方法的返回值 _loadAsync方法
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
) async {
try {
//下载图片
final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
throw Exception(...);
// 接收图片数据
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
// 对图片数据进行解码
return PaintingBinding.instance.instantiateImageCodec(bytes);
} finally {
chunkEvents.close();
}
}
_loadAsync:下载图片。并且对下载的图片数据进行解码。 下载逻辑比较简单:通过HttpClient从网上下载图片,另外下载请求会设置一些自定义的header,开发者可以通过NetworkImage的headers命名参数来传递。 下载完成后调用了PaintingBinding.instance.instantiateImageCodec(bytes)对图片进行解码
ImageProvider 中 obtainKey: 配合实现图片缓存 ImageProvider 中 resolve: ImageProvider的暴露的给Image的主入口方法(接受ImageConfiguration 包含图片和设备的相关信息,返回: ImageStream 图片数据流) 上面代码A处就是处理缓存的主要代码,这里的PaintingBinding.instance.imageCache 是 ImageCache的一个实例,它是PaintingBinding的一个属性, ImageCache ImageProvider 中 ImageCache ImageCache类定义
const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
class ImageCache {
// 正在加载中的图片队列
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
// 缓存队列
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
// 缓存数量上限(1000)
int _maximumSize = _kDefaultSize;
// 缓存容量上限 (100 MB)
int _maximumSizeBytes = _kDefaultSizeBytes;
// 缓存上限设置的setter
set maximumSize(int value) {...}
set maximumSizeBytes(int value) {...}
... // 省略部分定义
// 清除所有缓存
void clear() {
// ...省略具体实现代码
}
// 清除指定key对应的图片缓存
bool evict(Object key) {
// ...省略具体实现代码
}
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
assert(key != null);
assert(loader != null);
ImageStreamCompleter result = _pendingImages[key]?.completer;
// 图片还未加载成功,直接返回
if (result != null)
return result;
// 有缓存,继续往下走
// 先移除缓存,后再添加,可以让最新使用过的缓存在_map中的位置更近一些,清理时会LRU来清除
final _CachedImage image = _cache.remove(key);
if (image != null) {
_cache[key] = image;
return image.completer;
}
try {
result = loader();
} catch (error, stackTrace) {
if (onError != null) {
onError(error, stackTrace);
return null;
} else {
rethrow;
}
}
void listener(ImageInfo info, bool syncCall) {
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = _CachedImage(result, imageSize);
// 下面是缓存处理的逻辑
if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
_maximumSizeBytes = imageSize + 1000;
}
_currentSizeBytes += imageSize;
final _PendingImage pendingImage = _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}
_cache[key] = image;
_checkCacheSize();
}
if (maximumSize > 0 && maximumSizeBytes > 0) {
final ImageStreamListener streamListener = ImageStreamListener(listener);
_pendingImages[key] = _PendingImage(result, streamListener);
// Listener is removed in [_PendingImage.removeListener].
result.addListener(streamListener);
}
return result;
}
// 当缓存数量超过最大值或缓存的大小超过最大缓存容量,会调用此方法清理到缓存上限以内
void _checkCacheSize() {
while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
final Object key = _cache.keys.first;
final _CachedImage image = _cache[key];
_currentSizeBytes -= image.sizeBytes;
_cache.remove(key);
}
... //省略无关代码
}
}
缓存则使用缓存,没有缓存则调用load方法加载图片 加载成功后, 断图片数据有没有缓存,如果有,则直接返回ImageStream 没有缓存,则调用load(T key)方法从数据源加载图片数据,加载成功后先缓存,然后返回ImageStream setter: 有设置缓存上限的setter(所以,如果我们可以自定义缓存上限)
PaintingBinding.instance.imageCache.maximumSize=2000; //最多2000张
PaintingBinding.instance.imageCache.maximumSizeBytes = 200 << 20; //最大200M
其他: 对于网络图片来说,会将其“url+缩放比例”作为缓存的key。也就是说如果两张图片的url或scale只要有一个不同,便会重新下载并分别缓存。 总结: ImageProvider 加载图片数据并进行缓存、解码
- Image组件原理
研究一下Image是如何和ImageProvider配合: 获取最终解码后的数据,然后又如何将图片绘制到屏幕上的 MyImage(简易版Image)