新网创想网站建设,新征程启航
为企业提供网站建设、域名注册、服务器等服务
小菜在业务开发过程中会自定义 Slider 滑动条,而在自定义之前,小菜先简单了解一下 Flutter 自带的 Slider ;
创新互联专注为客户提供全方位的互联网综合服务,包含不限于成都网站建设、网站设计、福州网络推广、微信小程序、福州网络营销、福州企业策划、福州品牌公关、搜索引擎seo、人物专访、企业宣传片、企业代运营等,从售前售中售后,我们都将竭诚为您服务,您的肯定,是我们最大的嘉奖;创新互联为所有大学生创业者提供福州建站搭建服务,24小时服务热线:18980820575,官方网址:www.cdcxhl.com
简单分析源码可得, Slider 是一个有状态的 StatefulWidget 组件,属性也很清晰易懂,其中滑动过程中对应的 value 值和 onChanged 回调是必须参数;
value 未滑动过程中对应的值,在 min 和 max 之间; onChanged 是在滑动过程中回调,当 onChanged 为 null 或 value 所在的 min 和 max 集合范围为空时, Slider 禁止滑动;
min 和 max 为滑动条范围,而 value 的取值范围是在 min 和 max 之间,无论 value 为正还是负,均需要在 min 和 max 之间;
activeColor 为滑动条已滑动过的颜色; inactiveColor 为滑动条中未滑动的颜色;两者均可以在 SliderTheme 中设置;
label 为滑动条滑动到某一节点的标签文案; divisions 是把 min 和 max 等分为 divisions 份数;只有在 divisions 生效时, label 才会展示;
onChangeStart 和 onChangeEnd 分别对应滑动过程中 value 值何时开始更改或何时完成更改时对应的回调;
Slider 的主题效果可以通过 SliderTheme 或 ThemeData.sliderTheme 中获取更新,相较于 Slider 只提供已滑动和未滑动颜色效果,属性粒度更细;
activeTrackColor 和 inactiveTrackColor 分别对应 Slider 已滑动过和未滑动过的轨道颜色;
thumbColor 对应滑动按钮颜色,而 overlayColor 对应滑动按钮映射的叠层颜色,通常设置为半透明状态; overlayShape 对应叠层样式;
valueIndicatorColor 对应 label 气泡颜色; valueIndicatorShape 对应气泡内文字属性; valueIndicatorShape 对应气泡样式,可以再此进行自定义气泡;
activeTickMarkColor 对应已选中刻度颜色; inactiveTickMarkColor 对应未选中刻度颜色; tickMarkShape 对应刻度样式;
trackHeight 为 Slider 轨道高度; trackShape 对应轨道样式,主要再此处进行自定义样式;
对于不可滑动状态, SliderThemeData 提供了对应属性;
Slider 案例源码
小菜本节暂未涉及自定义滑动条样式,对于底层的 Slider 了解还不够深入;如有错误,请多多指导!
源码分析:
分析源码可得,TextField 是有状态 StatefulWidget,有丰富的属性,自定义化较高,实践中需要合理利用各种回调;
1、光标的相关属性;cursorColor 为光标颜色,cursorWidth 为光标宽度,cursorRadius 为光标圆角;其中 Radius 提供了 circle 圆角和 elliptical 非圆角两种;
2、textAlign 为文字起始位置,可根据业务光标居左/居右/居中等;注意只是文字开始方向;textDirection 问文字内容方向,从左向右或从右向左;
3、maxLength 为字符长度,设置时默认是展示一行,且右下角有编辑长度与整体长度对比;与 maxLengthEnforced 配合,maxLengthEnforced 为 true 时达到最大字符长度后不可编辑;为 false 时可继续编辑展示有差别;
4、设置 maxLength 之后右下角默认有字符计数器,设置 TextField.noMaxLength 即可只展示输入字符数;
5、maxLines 为允许展现的最大行数,在使用 maxLength 时内容超过一行不会自动换行,因为默认 maxLines=1,此时设置为 null 或固定展示行数即可自动换行;区别在于 null 会展示多行,而 maxLines 最多只展示到设置行数;
6、obscureText 是否隐藏编辑内容,常见的密码格式;
7、enableInteractiveSelection 长按是否出现【剪切/复制/粘贴】菜单;不可为空;
8、keyboardAppearance 为键盘亮度,包括 Brightness.dark/light 两种,但仅限于 iOS 设备;
9、textCapitalization 文字大小写;理论上 sentences 为每句话第一个字母大写;characters为每个字母大写;words 为每个单词首字母大写;但该属性仅限于 text keybord,和尚在本地更换多种方式并未实现,有待研究;
10、keyboardType 为键盘类型,和尚理解整体分为数字键盘和字母键盘等;根据设置的键盘类型,键盘会有差别;
a. 数字键盘
--1-- datetime 键盘上可随时访问 : 和 /;
--2-- phone 键盘上可随时访问 # 和 *;
--3-- number 键盘上可随时访问 + - * /
b. 字母键盘
--1-- emailAddress 键盘上可随时访问 @ 和 .;
--2-- url 键盘上可随时访问 / 和 .;
--3-- multiline 适用于多行文本换行;
--4-- text 默认字母键盘;
11、textInputAction 通常为键盘右下角操作类型,类型众多,建议多多尝试;
12、autofocus 是否自动获取焦点,进入页面优先获取焦点,并弹出键盘,若页面中有多个 TextField 设置 autofocus 为 true 则优先获取第一个焦点;
13、focusNode 手动获取焦点,可配合键盘输入等减少用户操作次数,直接获取下一个 TextField 焦点;
14、enabled 设为 false 之后 TextField 为不可编辑状态;
15、decoration 为边框修饰,可以借此来调整 TextField 展示效果;可以设置前置图标,后置图片,边框属性,内容属性等,会在后续集中尝试;若要完全删除装饰,将 decoration 设置为空即可;
16、inputFormatters 为格式验证,例如原生 Android 中通常会限制输入手机号或其他特殊字符,在 Flutter 中也可以借此来进行格式限制,包括正则表达式;使用时需要引入 package:flutter/services.dart;
a. LengthLimitingTextInputFormatter 限制最长字符;
b. WhitelistingTextInputFormatter 仅允许输入白名单中字符;如 digitsOnly 仅支持数字 [0-9];
c. BlacklistingTextInputFormatter 防止输入黑名单中字符;如 singleLineFormatter 强制输入单行;
分析源码 RegExp("[/]") 可以设置正则表达式;
17、onChanged 文本内容变更时回调,可实时监听 TextField 输入内容;
18、controller 文本控制器,监听输入内容回调;
19、onTap 点击 TextField时回调;
20、onEditingComplete 在提交内容时回调,通常是点击回车按键时回调;
21、onSubmit 在提交时回调,不可与 onEditingComplete 同时使用,区别在于 onSubmit 是带返回值的回调;
问题小结:
当 TextField 设置 enableInteractiveSelection 属性后长按会出现菜单,默认为英文,可通过设置 Flutter 国际化来处理;
(1)在 pubspec.yaml 中集成 flutter_localizations;
2)在 MaterialApp 中设置本地化代理和支持的语言类型;
(1)将 maxLength 设置为 null 仅使用 LengthLimitingTextInputFormatter 限制最长字符;
(2)设置 InputDecoration 中 decoration 属性为空;但是底部有空余,只是隐藏而并非消失;
Flutter Dio源码分析(一)--Dio介绍
Flutter Dio源码分析(二)--HttpClient、Http、Dio对比
Flutter Dio源码分析(三)--深度剖析
Flutter Dio源码分析(四)--封装
Flutter Dio源码分析(一)--Dio介绍视频教程
Flutter Dio源码分析(二)--HttpClient、Http、Dio对比视频教程
Flutter Dio源码分析(三)--深度剖析视频教程
Flutter Dio源码分析(四)--封装视频教程
github仓库地址
本文会手把手教你该怎么去封装一个类库,平时在我们的工作中都是拿着别人的造好的轮子在使用,这篇文章将带你怎么去自己造轮子,以后再碰到别的类库需要对其进行封装的时候提供一个的思路和方法。
在前面的文章中,我们对 Dio 的基本使用、请求库对比、源码分析,我们知道 Dio 的使用非常的简单,那为什么还需要进行封装呢?有两点如下:
当组件库方法发生重要改变需要迁移的时候如果有多处地方用到,那么需要对使用到的每个文件都进行修改,非常的繁琐而且很容易出问题。
当不需要 Dio 库的时候,我们可以随时方便切换到别的网络请求库,当然 Dio 目前内置支持使用第三方库的适配器。
因为一个应用程序基本都是统一的配置方式,所以我们可以针对 拦截器 、 转换器 、 缓存 、 统一处理错误 、 代理配置 、 证书校验 等多个配置进行统一管理。
因为我们的应用程序在每个页面中都会用到网络请求,那么如果我们每次请求的时候都去实例化一个 Dio ,无非是增加了系统不必要的开销,而使用单例模式对象一旦创建每次访问都是同一个对象,不需要再次实例化该类的对象。
这是通过静态变量的私有构造器来创建的单例模式
我们对 超时时间 、 响应时间 、 BaseUrl 进行统一设置
因为不管是 get() 还是 post() 请求, Dio 内部最终都会调用 request 方法,只是传入的 method 不一样,所以我们这里定义一个枚举类型在一个方法中进行处理
我们已经把 Restful API 风格简化成了一个方法,通过 DioMethod 来标明不同的请求方式。在我们平时开发的过程中,需要在请求前、响应前、错误时对某一些接口做特殊的处理,那我们就需要用到拦截器。 Dio 为我们提供了自定义拦截器功能,很容易轻松的实现对请求、响应、错误时进行拦截
我们发现虽然 Dio 框架已经封装了一个 DioError 类库,但如果需要对返回的错误进行统一弹窗处理或者路由跳转等就只能自定义了
在我们发送请求的时候会碰到几种情况,比如需要对非open开头的接口自动加上一些特定的参数,获取需要在请求头增加统一的 token
在我们请求接口前可以对响应数据进行一些基础的处理,比如对响应的结果进行自定义封装,还可以针对单独的 url 做特殊处理等。
我们看了转换器的介绍,发现和拦截器的功能差不多,那为什么还要存在转换器,有两点:
执行流程: 请求拦截器 请求转换器 发起请求 响应转换器 响应拦截器 最终结果 。
只会被用于 'PUT'、 'POST'、 'PATCH'方法,因为只有这些方法才可以携带请求体(request body)
会被用于所有请求方法的返回数据。
在开发过程中,客户端和服务器打交道的时候,往往会用一个 token 来做校验,因为每个公司处理刷新token的逻辑都不一样,我这里举一个简单的例子
为什么我们需要有取消请求的功能,如果当我们的页面在发送请求时,用户主动退出当前界面或者app应用程序退出的时候数据还没有响应,那我们就需要取消该网络请求,防止不必要的错误。
由 服务器生成 的 一小段文本信息 ,发送给浏览器,浏览器把 cookie 以kv形式保存到本地 某个目录下的文本文件内,下一次请求同一网站时会把该 cookie 发送给服务器。
cookie 的使用需要用到两个第三方组件 dio_cookie_manager 和 cookie_jar
因为在我们平时的开发过程中,会碰到一种情况,在进行网络请求时,我们希望能正常访问到上次的数据,对于用户的体验比较好,而不是展示一个空白的页面,该缓存主要是 《Flutter实战》网络接口缓存 提供参考。
我们在程序退出后内存缓存将会消失,所以我们用 shared_preferences 进行磁盘缓存数据。
在我们用flutter进行抓包的时候需要配置 Dio 代理。由 DefaultHttpClientAdapter 提供了一个 onHttpClientCreate 回调来设置底层 HttpClient 的代理。
用于验证正在访问的网站是否真实。提供安全性,因为证书和域名绑定,并且由根证书机构签名确认。
日志打印主要是帮助我们开发时进行辅助排错
最近公司做技术分享写的文章的demo
Flutter中的InheritedWidget状态管理
1.InheritedWidget是什么?
InheritedWidget是Flutter中非常重要的一个功能型组件,它提供了一种数据在widget树中从上到下传递、共享的方式,比如我们在应用的根widget中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget中来获取该共享的数据!这个特性在一些需要在widget树中共享数据的场景中非常方便!如Flutter SDK中正是通过InheritedWidget来共享应用主题(Theme)和Locale (当前语言环境)信息的。
InheritedWidget和React中的context功能类似,和逐级传递数据相比,它们能实现组件跨级传递数据。InheritedWidget的在widget树中数据传递方向是从上到下的,这和通知Notification的传递方向正好相反。
2.源码分析
InheritedWidget
先来看下InheritedWidget的源码:
abstract class InheritedWidget extends ProxyWidget { const InheritedWidget({ Key key, Widget child }): super(key: key, child: child); @override InheritedElement createElement() =InheritedElement(this); @protected bool updateShouldNotify(covariant InheritedWidget oldWidget);}
它继承自ProxyWidget:
abstract class ProxyWidget extends Widget { const ProxyWidget({ Key key, @required this.child }) : super(key: key); final Widget child;}
可以看出Widget内除了实现了createElement方法外没有其他操作了,它的实现关键一定就是InheritedElement了。
InheritedElement 来看下InheritedElement源码
class InheritedElement extends ProxyElement { InheritedElement(InheritedWidget widget) : super(widget); @override InheritedWidget get widget = super.widget; // 这个Set记录了所有依赖的Elementfinal MapElement, Object _dependents = HashMapElement, Object();
//该方法会在Element mount和activate方法中调用,_inheritedWidgets为基类Element中的成员,用于提高Widget查找父节点中的InheritedWidget的效率,它使用HashMap缓存了该节点的父节点中所有相关的InheritedElement,因此查找的时间复杂度为o(1) @override void _updateInheritance() {final MapType, InheritedElement incomingWidgets = _parent?._inheritedWidgets;if (incomingWidgets != null) _inheritedWidgets = HashMapType, InheritedElement.from(incomingWidgets); else _inheritedWidgets = HashMapType, InheritedElement(); _inheritedWidgets[widget.runtimeType] = this; }
//该方法在父类ProxyElement中调用,看名字就知道是通知依赖方该进行更新了,这里首先会调用重写的updateShouldNotify方法是否需要进行更新,然后遍历_dependents列表并调用didChangeDependencies方法,该方法内会调用mardNeedsBuild,于是在下一帧绘制流程中,对应的Widget就会进行rebuild,界面也就进行了更新 @override void notifyClients(InheritedWidget oldWidget) { assert(_debugCheckOwnerBuildTargetExists('notifyClients'));for (Element dependent in _dependents.keys) { notifyDependent(oldWidget, dependent); } }
其中_updateInheritance方法在基类Element中的实现如下:
void _updateInheritance() {
_inheritedWidgets = _parent?._inheritedWidgets;
}
总结来说就是Element在mount的过程中,如果不是InheritedElement,就简单的将缓存指向父节点的缓存,如果是InheritedElement,就创建一个缓存的副本,然后将自身添加到该副本中,这样做会有两个值得注意的点:
InheritedElement的父节点们是无法查找到自己的,即InheritedWidget的数据只能由父节点向子节点传递,反之不能。
如果某节点的父节点有不止一个同一类型的InheritedWidget,调用inheritFromWidgetOfExactType获取到的是离自身最近的该类型的InheritedWidget。
看到这里似乎还有一个问题没有解决,依赖它的Widget是在何时被添加到_dependents这个列表中的呢?
回忆一下从InheritedWidget中取数据的过程,对于InheritedWidget有一个通用的约定就是添加static的of方法,该方法中通过inheritFromWidgetOfExactType找到parent中对应类型的的InheritedWidget的实例并返回,与此同时,也将自己注册到了依赖列表中,该方法的实现位于Element类,实现如下:
@overrideT dependOnInheritedWidgetOfExactType
// 这里通过上述mount过程中建立的HashMap缓存找到对应类型的InheritedElement final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];if (ancestor != null) { assert(ancestor is InheritedElement);return dependOnInheritedElement(ancestor, aspect: aspect); } _hadUnsatisfiedDependencies = true; return null;}
@overrideInheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) { assert(ancestor != null);
// 这个列表记录了当前Element依赖的所有InheritedElement,用于在当前Element deactivate时,将自己从InheritedElement的_dependents列表中移除,避免不必要的更新操作 _dependencies ??= HashSetInheritedElement(); _dependencies.add(ancestor); ancestor.updateDependencies(this, aspect);return ancestor.widget;}
3.如何使用InheritedWidget
1)、创建一个类继承自Inheritedwidget
class InheritedContext extends InheritedWidget{ final InheritedTestModel inheritedTestModel; InheritedContext({ Key key, @required this.inheritedTestModel, @required Widget child}): super(key: key, child: child);static InheritedContext of (BuildContext context) { return context.dependOnInheritedWidgetOfExactTypeInheritedContext(); } @override bool updateShouldNotify(InheritedContext oldWidget) { return inheritedTestModel != oldWidget.inheritedTestModel; }}
2)、InheritedTestModel类为数据容器(这里定义了一个Listint数据源)
class InheritedTestModel{ final List _list; InheritedTestModel(this._list); List getList(){ return _list; }}
class ArrayListData{ static List _list ;static List getListData (){ _list = new List(); _list .add(1); _list .add(2); _list .add(3); _list .add(4);return _list ; }}
3)、定义一个Widget 使用 InheritedContext类的数据 InheritedTestModel
class ListDemo extends StatefulWidget{ @override State createState() { return new ListDemoState(); }}class ListDemoState extends StateListDemo{List _list; InheritedTestModel _inheritedTestModel; Timer _timer; Duration oneSec = const Duration(seconds: 1); @override void initState() { _list = ArrayListData. getListData (); _inheritedTestModel = new InheritedTestModel(_list); _timer = Timer.periodic(oneSec, (timer) { _doTimer(); }); } void _doTimer() { for(int i = 0; i _list.length; i++){ _list[i] = _list[i]+ 1; } setState(() { _inheritedTestModel = new InheritedTestModel(_list); }); }Widget _buildBody() { return Container(child: ListDemo2(), ); } @override Widget build(BuildContext context) { return InheritedContext(inheritedTestModel: _inheritedTestModel, child: Scaffold(appBar: AppBar(title: Text("ListDemo"), actions: Widget[ IconButton(icon: Icon(Icons. add ), ) ],), body: _buildBody(), ), ); } @override void dispose() { super.dispose();if (_timer != null) { _timer.cancel(); } }}
4)、在ListDemo中通过Timer更新InheritedTestModel 中的数据,然后再下一个Widget中获取更新的数据作为展示
class ListDemo2 extends StatefulWidget{ @override State createState() { return new ListDemoState2(); }}class ListDemoState2 extends StateListDemo2{InheritedTestModel _inheritedTestModel; Widget _buildListItem(BuildContext context,int index) { return Container(height: 50, width: 100, alignment: Alignment. center , child: Text(_inheritedTestModel.getList()[index].toString()), ); }Widget _buildBody() { _inheritedTestModel = InheritedContext. of (context).inheritedTestModel;return Container(child: ListView.builder(itemBuilder:(context, index)=_buildListItem(context,index),itemCount: _inheritedTestModel.getList().length,), ); } @override Widget build(BuildContext context) { return _buildBody(); }}
这样就可以在父widget中更新数据,子View不需任何操作直接从数据容器InheritedTestModel 中获取到更新后的新数据
这是一个数据共享的简单的例子,基本的流程,大致就是A去更新B的数据,A和B有一个共同的父类,实现数据的共享
4.上面说了原理和基本的使用,但是在实际项目当中,我当然不建议这样来使用,Google 已经为我们封装好了功能更加强大的插件Provider,其内部原理就是基于InheritedWidget来实现的,我们理解了基本原理,可以更好的在项目中运用Provider
最近一个项目要实现可以无限循环的PageView,主要思路是在初始化pageview的list的时候在开始和结尾多加一个结尾和开头的widget,当滑动到开头和结尾的时候手动进行页面的切换,详细可以搜索pageview无限轮播。
这种方法有一个要点就是要维护两个索引,一个是内部list的索引,一个是外部显示的索引,由于list的容量是比显示的数量多2的,所以如果要在外部进行一些比如指示器或者计时器功能要进行和页面同步显示或者切换页面操作时,需要将显示的索引转换成list的索引。
不过网上说的都是一些比较简单的实现,看到比较多的就是当滑动到要手动切换的时候进行一个时延,这样可以避免直接切换页面造成的卡顿和跳动现象。但是存在一个问题,如果要同时实现一个跟随页面切换的指示器,就会出现当页面切换过去之后指示器才会跟着过去,因为页面切换的时候执行了时延,而时延之后才会真正改变索引,此时才会setstate,之后指示器才能响应到索引的切换,但是如果在时延之前就切换的话又会出现指示器先行的情况。因此这种方法其实是存在一些问题的。
所以解决这个问题的关键在于如何进行页面切换的判断。这里可以有两种思路实现,第一种是实现viewpage的onpagechanged方法,在里面进行逻辑的判断,然后用controller来进行页面跳转,不过这种方法存在当controller跳转的时候又会回调onpagechanged,所以就会出现多次对索引不必要操作,而且如果有比如计时器等额外的功能的话可能不方便将页面逻辑分开,而且依旧无法解决指示器延迟问题,同时也很难进行细粒度的操作。
第二种方法我们就要去看pageview的源码了,从源码的角度来解决问题才是正确的方法。首先我们点进去pageview的源码
看到这里其实已经有一些思路了,我们之前难点在于重写了onpagechanged方法导致问题无法很好的解决,现在我们找到了onpagechanged调用的地方,只要找办法避免掉就可以实现了。
当然这里我们要说到NotificationListener,以及flutter对应的冒泡事件传输机制,这里大家可以去看看这篇 文章 。
我来总结一下,其实就是flutter对于notification这个组件,有一中事件规则叫冒泡传递,底层的notification如果在它的 onNotification写的逻辑中返回是false以及它不是根结点,就会去向上遍历寻找它的祖先notification组件,知道遇到root节点或者某一个返回true,则事件传递结束。
而且在onNotification中可以对多种事件进行监听和处理,所以我们可以把对viewpage页面跳转对索引处理的逻辑写在这里,而且我们可以分别处理比如滑动开始的start事件和结束的end事件,分别进行细粒度的逻辑的处理,这样就可以在外部进行操作和别的功能实现了。
因此不仅无限轮播事件可以通过这种方法来解决,如果有其他的操作也可以这样进行处理,而且因为我们没有传入onpagechanged方法,所以不存在多次调用的问题,pageview那里判断onpagechanged是null方法就不会进去了,会直接我们写在pageview外面的notification的逻辑。
最后的结构大概这样