FLutter 踩坑笔记
Android 集成部分
Flutter 编写部分
- MaterialButton RaisedButton 控件内部与child之间存在padding ,没有属性可以去掉。
- 使用最新版sdk(1.22.0 以及以上)中的TextButton
- 在Button 外面使用SizeBox等可以设置size的控件,设置size可以缩减 padding
- 在 State中声明的Color 类型的属性,要么直接赋值,要么使用 final 进行修饰,不然会报错。
- 在 main.dart 中 使用 runZonedGuarded 可以 捕获异常和输出,代码如下
bool get isInDebugMode {
bool inDebugMode = false;
return inDebugMode;
}
Future<Null> main() async {
FlutterError.onError = (FlutterErrorDetails details) async {
if (isInDebugMode) {
// In development mode simply print to console.
FlutterError.dumpErrorToConsole(details);
} else {
// In production mode report to the application zone to report to
// Sentry.
Zone.current.handleUncaughtError(details.exception, details.stack);
}
};
//自定义错误提示页面
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails) {
return Scaffold(
appBar: AppBar(
title: Text("错误页面"),
),
body: Center(
child: Text("Custom Error Widget"),
));
};
runZonedGuarded(
() => runApp(MyApp()),
zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {collectLog(line); // 捕获所有的print
},
),
(error, stack) {
var details = makeDetails(obj, stack);
reportErrorAndLog(details);
},
);
}
- flutter app 的正常模式和暗黑模式风格的定义是在MaterialApp中 theme 和 darkTheme 指定的。ThemeData中的brightness 属性 指定是正常模式还是暗黑模式
- 使用 dart语言中extension on 语法 可以扩展系统中的类的属性,比如我们可以扩展 ThemeData 来添加项目中使用到的颜色,demo如下:
extension CustomThemeData on ThemeData {
Color get primaryBlue => brightness == Brightness.dark ? Color(0xFF228AEE) : Color(0xFF1880EE);
// ignore: non_constant_identifier_names
Color get primaryBlue_outline => brightness == Brightness.dark ? Color(0xFF228AEE) : Color(0xFF1880EE);
}
- ThemeData中涉及到很多系统中使用到的颜色,和样式,这些内容的修改可以影响到所有使用的页面和控件(自定义过颜色样式的除外)。修改时需要谨慎。
- Platform 类中涉及到了app运行平台的信息,如:运行app的系统是android还是ios,运行环境(Map<String, String> environment)等内容
- dart:ui 包下面的Window对象 包含了app的默认route,设备的PixelRatio等信息,可以之间在 state或 Widget 的方法中之间使用window这个对象
- 可以使用 TextPainter 来提前获取Text中写入字符串时的size,Text 中的style,必须和TextSpan中的style一致,代码如下:
TextPainter painter = TextPainter(text: TextSpan(text: v, style: fieldTextStyle), textDirection: TextDirection.ltr);
painter.layout();
print(painter.width);
- 使用 GlobalKey 的方式获取控件的size,需要在绘制完界面后才有效代码如下:
final GlobalKey _textFieldKey = GlobalKey();
Text("aaa", key: _textFieldKey,);
double inputWidth = _textFieldKey.currentContext.size.width;
-
获取屏幕Size,使用
MediaQuery.of(context).size
-
flutter 中图片的使用需要在pubspec.yaml中 声明,代码如下:
图片放在和pubspec.yaml同级的iamges文件夹中
flutter:
uses-material-design: true
assets:
- images/
//图片使用时的代码,使用在pubspec.yaml声明时的图片路径+图片的名字
//Contanier 的 decoration中可以设置image decoration ,使用 DecorationImage对象,可以直接加载 asserts 中 的image 资源
Container(
……
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("images/warnIcon.png")),
) )
)
- 在 自定义的StatefulWidget中若有需要动态刷新的数据或子widget ,请在build()方法中使用widet.xx 来获取最新的值
- notification 用于 子widget 给父widget 传递数据,使用方式如下:
- 继承 Notification 类,创建 notification 数据类,demo 如下:
class BannerNotification extends Notification {
BannerNotification(this.isShow,
{
this.type,
this.tip,
……
});
String tip;
int type;
……
}```
- 在父widget的build中 return NotificationListener 对象,代码如下:
```dart
@override
Widget build(BuildContext context) {
return NotificationListener<BannerNotification>(
onNotification: (notification) {
//处理逻辑 的代码
……
return ture;
},
child: Text("aaa")
);
}
}
- 子widget中使用 notification ,代码如下:
notification.dispatch(context);
- dispatch(context)中的context必须是在子widget的context,若在如点击事件等callback 方法中使用时,可以在子widget的外层加上Builder,确保context层级小于父widget,代码如下:
Builder(
builder: (context) {
return MaterialButton(onPressed: (){
return MaterialButton(onPressed: (){
……
},);
},);
},
)
- 在 base body 这种 全局式的 父widget ,可以在需要使用notification的子widget的外层加上 Builder(),然后将context赋值给static 修饰的全局 context对象,在定义的static 方法中集中调用 ,使用 notification.dispatch(context);,代码如下:
class BaseBody extends StatelessWidget {
final Widget child;
static BuildContext _buildContext;
static void switchBanner(BannerNotification notification) {
notification.dispatch(_buildContext);
}
@override
Widget build(BuildContext context) {
return NotificationListener<BannerdNotification>(
onNotification: (notification) {
……
//逻辑代码
return true;
},
child: Stack(
children: <Widget>[
Builder(
builder: (context) {
BaseBody._buildContext = context;
return widget.child;
},
),
],
),
);
}
}
- 自定义 widget中 使用了AnimationController ,Timer 等需要dispose的widget时,若在 state 的dispose()方法中调用 dispose方法时,要在super.dispose();之前调用,因为有时会出现controller 在页面退出后没有dispose的问题,代码如下:
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
//若还是出现controller 没有dispose ,可以在deactivate 中执行 controller?.dispose();
@override
void deactivate() {
_controller?.dispose();
super.deactivate();
}
-
完全自定义 TextField中的contentPadding,代码如下:
TextField( scrollPadding: EdgeInsets.zero,//默认是20,现在改成0 decoration: InputDecoration( contentPadding: EdgeInsets.only(left: 12.px, right: 2.px, top: 12.px, bottom: 12.px),//自定义 contentPadding isDense: true,//为true时 ,TextField的vertical方向没有space,默认是false。 ), )
-
TextField 中 keyboardType 属性中 TextInputType.visiblePassword 为设置键盘模式为输入密码模式,但是 输入的字符是可见的,若想不可见,可以设置 TextField的obscureText 属性为 ture,则输入字符不可见。代码如下:
TextField(
keyboardType: TextInputType.visiblePassword,
autofocus: true,
……
),
- flutter中的动画,一般动画使用代码如下,
//动画的控制类,如动画的播放,暂停,反转,重复。
_controller = AnimationController(
duration: Duration(milliseconds: 250), vsync: provider)
..addStatusListener((status) {
//动画状态的监听,completed,dissmiss,forward,reverse
}
})..addListener(() {
//在此获取动画的当前值。
var a = _controller.value; //a的值为0到1之间
var b = _animation.value;/ /b的值在Tween定义的begin与end之间。
});
//CurvedAnimation 定义运动变化的速率,curve: 决定动画的差值器
var curved = CurvedAnimation(parent: _bannerController, curve: Curves.easeOut)
// Tween 中定义动画的取值区间,数值可以是任意类型,如 Color,Offset,double等。
_animation = Tween(begin: Offset(0, -1), end: Offset(0, 0)).animate(curved);
}
- 使用TweenSequence和 TweenSequenceItem 可以定义分阶段的连续动画,代码如下:
//weight:执行时长占总时长的权重
//TweenSequenceItem : 定义不同的动画
Animation animation=TweenSequence([
TweenSequenceItem(tween: Tween(begin: 0,end: 100),weight: 20),
TweenSequenceItem(tween: CurveTween(curve:Curves.easeIn ),weight: 80)
]).animate( _controller);
- ShaderMask 设置 widget的颜色渐变图层
-
Gradient颜色渐变的方式:LinearGradient(线形变换);RadialGradient(放射状变化);SweepGradient(扇形变化)
-
blendMode:颜色混合模式
-
tileMode:指定在begin,end之外的区域颜色如何渲染,repeated:重复使用区域之内的渲染颜色;clamp:按照接近的区域内的颜色;mirror:以镜像模式重复区域内的渲染颜色
-
demo如下:
-
ShaderMask(
shaderCallback: (Rect bounds) {
return RadialGradient(
center: Alignment.bottomCenter,
radius: _gradientValue,
colors: <Color>[_iconColor, _loadingColor],
stops: [_gradientValue, 1],
tileMode: TileMode.repeated,
).createShader(bounds);
},
blendMode: BlendMode.srcATop,
child: Icon(
_icon,
size: _iconSize,
),
)
ShaderMask(
shaderCallback: (Rect bounds) {
return SweepGradient(
center: Alignment.bottomCenter,
startAngle: 0,
endAngle: pi,
tileMode: TileMode.clamp,
colors: <Color>[_iconColor, _loadingColor],
stops: [_gradientValue, 1],
).createShader(bounds);
},
blendMode: BlendMode.dstOver,
child: Icon(
_icon,
size: _iconSize,
),
)
LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
tileMode: TileMode.mirror,
colors: <Color>[_iconColor, _loadingColor],
stops: [_gradientValue, 1],
).createShader(bounds);
},
blendMode: BlendMode.dstATop,
child: Icon(
_icon,
size: _iconSize,
),
);
-
Container 中设置阴影效果
-
的style 有 outer,inner,normal,solid
-
demo
Container( …… decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Color(0xFF000000),//阴影的颜色 blurRadius: 10.px,//阴影的半径 offset: Offset(2.px, 2.px),//shadow 对于 box的偏移量 spreadRadius: 10.px,//box扩展的大小 ), ], ), )
-
-
box shadow的原理
//ui.shadow
static double convertRadiusToSigma(double radius) {
return radius * 0.57735 + 0.5;//阴影的逻辑像素计算公式
}
//根据传入的半径,计算阴影的逻辑像素
double get blurSigma => convertRadiusToSigma(blurRadius);
Paint toPaint() {
return Paint()
..color = color
//使用 MaskFilter.blur 实现paint的阴影效果
..maskFilter = MaskFilter.blur(BlurStyle.normal, blurSigma);
} }
问题:BlurStyle的类型有 normal,inner,outer,solid ;在 box shadow中 发现使用 inner,outer,normal的效果一样,没有区别。
-
widget中textbaseline的类型分为:alphabetic:按照英文字符的baseline对齐;ideographic:按照中文字符的baseline对齐。
-
Text.rich :富文本widget,类似于android的SpannableString:
- demo
Text.rich( //必须设置的属性,在里面设置展示的字符串和样式 TextSpan( text: number,//最开始展示的字符串 children: [//里面依次设置需要的Span //设置相应的字符串和样式 TextSpan( text: "\t$unit", style: _unitStyle, ), //设置其他类型的widget WidgetSpan(child: Padding(padding: EdgeInsets.all(10),child: FlatButton(),)) ], style: _numberStyle,//最开始展示的字符串和样式 ), );
- Text.rich的其他常用属性说明:
const Text.rich(
this.textSpan, {
Key key,
this.style,//设置默认的样式
this.textAlign,//字符串的对齐方式
this.textDirection,//字符串的方向
this.softWrap, //超出控件长度的字符串是否截断
this.overflow,//超出控件长度的字符串的处理方式
this.textScaleFactor,//字体的缩放
this.maxLines,//最大行数
})
- flutter 中 使用 CustomPaint
- CustomPaint 可以实现使用 Canvas 绘制不同的图像,CustomPaint也是一个Widget ,可以直接使用。demo如下:
……
Visibility(
child: CustomPaint(
size: size,//设置绘制区域的大小。如果有 child,则忽略该参数,且绘制区域为 child 的尺寸
foregroundPainter:_ArcPainter(),//前景绘制
painter : _ArcPainter(),//背景绘制
isComplex:false,//是否复杂的绘制,如果是,Flutter 会应用一些缓存策略来减少重复渲染的开销。默认 false
willChange:false,//和 isComplex 配合使用,当启用缓存时,该属性代表在下一帧中绘制是否会改变。默认 false
child: ... //CustomPaint 是可以包含一个子节点的
),
)
//自定义的paint
class _ArcPainter extends CustomPainter {
……
//重写paint方法,使用 Canvas对象绘制图形
@override
void paint(Canvas canvas, Size size) {
//设置paint对象的属性
Paint backgroundPaint = Paint()
backgroundPaint.style = PaintingStyle.stroke;
backgroundPaint.strokeWidth = arcSize.px;
backgroundPaint.color = backgroundColor;
var start = pi / 6.2 + pi;
var swap = 2 * pi / 3;
//设置 绘制扇形的矩形范围
var rect = Rect.fromLTRB(2.px, size.height / 12, size.width, size.height);
canvas.drawArc(rect, start, swap, false, backgroundPaint);
//其他的绘制方法
// 绘制弧线
// drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
// 绘制图片
// drawImage(Image image, Offset p, Paint paint)
// 绘制圆
// drawCircle(Offset c, double radius, Paint paint)
// 绘制线条
// drawLine(Offset p1, Offset p2, Paint paint)
// 绘制椭圆
// drawOval(Rect rect, Paint paint)
// 绘制文字
// drawParagraph(Paragraph paragraph, Offset offset)
// 绘制路径
// drawPath(Path path, Paint paint)
// 绘制点
// drawPoints(PointMode pointMode, List<Offset> points, Paint paint)
// 绘制Rect
// drawRect(Rect rect, Paint paint)
// 绘制阴影
// drawShadow(Path path, Color color, double elevation, bool transparentOccluder)
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
//返回 true 会进行重绘,否则就只会绘制一次。你可以通过一些条件判断来决定是否每次绘制,这样能够节约系统资源。
return false;
}
}
-
flutter中正则表达式使用方式
var reg = new RegExp(r"^[\u4E00-\u9FA5A-Za-z0-9_]+$");//匹配中英文,数字,\u4E00-\u9FA5 表示中文, bool has = reg.hasMatch("aaa");//aaa 是否匹配规则 //RegExp 中还有其他的匹配的方法,如:allMatches(),firstMatch() 等
注意:RegExp的规则参数中是以 r 来开始的,后面是匹配规则的字符串,匹配规则是以^ 开始 $ 结束
-
flutter中 对变量的值进行限制时 可以使用 assert 实现 ,demo如下:
assert(widget.barStyleWrapper != null, "barStyleBuilder must not null");
-
flutter 中打开 bottom sheet 有2种方式:
- 使用package:flutter/material.dart 文件中的 showBottomSheet 方法; 使用 package:flutter/material.dart 文件中的 showModalBottomSheet 方法 ,demo 如下:
```dart // showBottomSheet showBottomSheet( context: context, builder: (context) { return Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height - 60, decoration: BoxDecoration(color: Colors.red), child: MaterialButton( onPressed: () { Navigator.pop(context);//dismiss }, child: Text("click"), ), ); }); //showModalBottomSheet showModalBottomSheet(context: context, isScrollControlled: true,//为true 时,当 内部的widget的高度大于等于屏幕高度时可以滚动显示,为false时,内部的widget的展示的高度只能是小于等于屏幕的一半 enableDrag: false,//是否可以下拉消失 isDismissible: false,//点击外部区域是否消失 barrierColor: Colors.yellow,//设置自定义widget以为区域的颜色 useRootNavigator: true, routeSettings: RouteSettings(name:"aa"),// 这个和useRootNavigator还没用到过 builder: (context){ return Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height - 60, decoration: BoxDecoration(color: Colors.green), child: MaterialButton( onPressed: () { Navigator.pop(context);/dismiss }, child: Text("click"), ), ); }); ```
- 区别:
- 1. 在有底部导航栏的情况下showBottomSheet 弹出的bottom 不会 遮挡 导航了,showModalBottomSheet却会.
- 2. 在设置了内部widget 高度为屏幕高度时,showBottomSheet 会 占满scaffold 的boy 的空间,showModalBottomSheet 需要在 isScrollControlled 为true时才能占满屏幕
- 3. showBottomSheet 可以下拉消失,showModalBottomSheet 需要设置enableDrag 为true ,才能下拉消失
- 4. showBottomSheet 点击 bottom以外的区域,bottom 不会消失 , showModalBottomSheet 需要在isDismissible 为true时可以消失,为false时不消失
- 5. showModalBottomSheet 可以设置useRootNavigator和routeSettings,但还没用到过,咱不了解
- 6. showModalBottomSheet 可以使用 barrierColor 设置自定义widget以为区域的颜色
相同:
- 1. 都在通过一个package 下面 - 2. 都有 backgroundColor 属性,但设置了没作用,不知道为啥 - 3. 在 自定义的widget里面都可以使用Navigator.pop(context); dismiss 掉 bottom
-
在 scaffold中使用 输入框,界面上移时,会超出状态栏,可以将 scaffold中的resizeToAvoidBottomInset 属性设成false来限制,而且 ,设置resizeToAvoidBottomInset=false后 使用 MediaQuery.of(context).viewInsets.bottom获取 键盘的高度时,可以获取到正确的值,若为true时,获取的值一直为0
-
flutter中判断键盘是否弹出,即获取 键盘高度,demo如下:
class _BottomSheetPanelState extends State<BottomSheetPanel> with WidgetsBindingObserver { MediaQueryData _queryData; @override void initState() { super.initState(); /添加监听 WidgetsBinding.instance.addObserver(this); } @override Widget build(BuildContext context) { _queryData = MediaQuery.of(context); return Scaffold( resizeToAvoidBottomInset: false, //在 scaffold中坚挺 键盘的高度必须将resizeToAvoidBottomInset设成false body: Stack( …… ); } //重写WidgetsBindingObserver中的didChangeMetrics方法 @override didChangeMetrics() { super.didChangeMetrics(); //监听界面变化回调 WidgetsBinding.instance.addPostFrameCallback((_) { _fromDrag = false; _isDrag = true; //_queryData.viewInsets.bottom获取 键盘弹起的距离,即键盘的高度 if (_queryData.viewInsets.bottom == 0) { //收起键盘 } else { //显示键盘 } }); } …… }
-
dart 中 " " 表示 一行字符串,""" “”" 表示 段落式的字符串
-
可以超出父Widget的size约束的Widget :OverflowBox,SizedOverflowBox,使用 demo 如下:
- SizedOverflowBox
- 下面demo的代码中因为SizedOverflowBox的父Widget 是 Column ,所以 box中的child的水平方向的受到crossAxisAlignment的影响,alignment的水平方法的约束不起作用,垂直方向的约束是可以起作用的。
- SizedOverflowBox的size 属性指定的size必须不能大于 父Widget的size,在这段代码中,alignment为center ,所以 height的大小会影响到 child上下溢出Container 的size,
- SizedOverflowBox 直接作为 Container的child 时,child的size会受到 Container的size的约束,不会超出 Container的范围,会被剪裁
- Container的child为SizedBox或者Container 并且 size大于 父Widget的size时,会被剪裁
Container( padding: EdgeInsets.all(20), width: 50, alignment: Alignment.bottomRight, height: 50, decoration: BoxDecoration(color: Colors.red), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedOverflowBox( alignment: Alignment.center, size: Size(10, 10), child: Container( width: 50, height: 100, decoration: BoxDecoration(color: Colors.green), ), ), ], ))
- OverflowBox
- OverflowBox中使用minWidth/Height和maxWidth/Height 设置 子Widget 的size的边界,若 这4个值大于父Widget的size并且子Widget的size大于父Widget的size时,就可以超出父Widget的size的约束。
- OverflowBox 的alignment 直接限制子Widget的在父Widget的位置,根据左上角坐标进行限制的
- SizedOverflowBox
Center(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(color: Colors.red),
child: OverflowBox(
alignment: Alignment.center,
minWidth: 0,
minHeight: 0,
maxWidth: 100,
maxHeight: 200,
child: Container(
width: 10,
height: 200,
decoration: BoxDecoration(color: Colors.green.withAlpha(100)),
),
)),
)
- PageStorage 保存 widget中的 属性,使用方式如下:
-
- 父Wiget 必须为PageStorage
-
- 创建子Widget时必须有key这个属性
-
- 初始化子类对象时必须 给 key 赋值,且对象类型必须是PageStorageKey
-
- 在子Wiget中 使用
PageStorage.of(context).writeState(context, data);
保存数据
- 在子Wiget中 使用
-
- 建议在didChangeDependencies 方法中使用
PageStorage.of(context).readState(context);
读取保存的数据,在其他的生命周期方法如 build中读取也是可以的,在didChangeDependencies的好处时,该方法是在 widget 初始化之后直接调用的。而且每次刷新不会重复执行。
- 建议在didChangeDependencies 方法中使用
-
class TestDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _TestDemoState();
}
class _TestDemoState extends State {
final List _list = [
Page1(),
PageStoreDemo(
key: PageStorageKey("key1"),//子Widget 必须设置 key ,并且key的类型必须是PageStorageKey
),
PageStoreDemo(
key: PageStorageKey("key2"),
),
PageStoreDemo(
key: PageStorageKey("key3"),
),
];
int _index = 0;
final PageStorageBucket _bucket = PageStorageBucket();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test"),
),
// 父Wiget 必须为PageStorage
body: PageStorage(
child: _list[_index],
bucket: _bucket,
),
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
_index = index;
});
},
currentIndex: _index,
selectedItemColor: Colors.red,
items: [
BottomNavigationBarItem(icon: Icon(Icons.add), label: "page1"),
BottomNavigationBarItem(icon: Icon(Icons.call), label: "page2"),
BottomNavigationBarItem(icon: Icon(Icons.input), label: "page3"),
BottomNavigationBarItem(icon: Icon(Icons.padding), label: "page4"),
],
),
);
}
}
class Page1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
child: Container(
width: 50,
constraints: BoxConstraints(maxHeight: 100, maxWidth: 100),
height: 50,
decoration: BoxDecoration(color: Colors.red),
child: SizedBox(
width: 100,
height: 100,
child: Container(
decoration: BoxDecoration(color: Colors.green.withAlpha(100)),
),
)),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return PageStoreDemo(
key: PageStorageKey("key1"),
);
}));
},
),
);
}
}
class PageStoreDemo extends StatefulWidget {
PageStoreDemo({
PageStorageKey key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _PageStoreState();
}
}
class _PageStoreState extends State<PageStoreDemo> {
String _text = "0";
int _count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: Row(
children: [
Text(_text),
MaterialButton(
onPressed: () {
setState(() {
_text = "${_count++}";
});
//子widget中保存需要的数据
PageStorage.of(context).writeState(context, _text);
},
child: Text("click"),
)
],
),
);
}
@override
void didChangeDependencies() {
//读取保存的数据
if (PageStorage.of(context)?.readState(context) != null) {
_text = PageStorage.of(context).readState(context);
}
super.didChangeDependencies();
}
}
-
使用 AutomaticKeepAliveClientMixin 保存 页面的状态,demo如下:
- State的类需要加上 with AutomaticKeepAliveClientMixin
- 重写 bool get wantKeepAlive => true; ,将 wantKeepAlive 返回值设置为ture
- 在 build 的重新方法中加上 super.build(context);
但是这种方式子Widget的容器必须是 TabBarView或者PageView
//父widget
_controller = TabController(length: _list.length,vsync: this);
Scaffold(
appBar: AppBar(
title: Text("Test"),
),
// 父Wiget 必须为PageStorage
body: TabBarView(
children: _list,
controller:_controller,
),
bottomNavigationBar: BottomNavigationBar(
onTap: (index) {
setState(() {
_index = index;
});
_controller.animateTo(_index);
},
currentIndex: _index,
selectedItemColor: Colors.red,
items: [
BottomNavigationBarItem(icon: Icon(Icons.add), label: "page1"),
BottomNavigationBarItem(icon: Icon(Icons.call), label: "page2"),
BottomNavigationBarItem(icon: Icon(Icons.input), label: "page3"),
BottomNavigationBarItem(icon: Icon(Icons.padding), label: "page4"),
],
),
);
//子widget
class _PageStoreState extends State<PageStoreDemo>
with AutomaticKeepAliveClientMixin<PageStoreDemo> {
String _text = "0";
int _count = 0;
@override
void initState() {
super.initState();
print("_PageStoreState---initState");
}
@override
Widget build(BuildContext context) {
super.build(context);
return Center(
child: Row(
children: [
Text(_text),
MaterialButton(
onPressed: () {
setState(() {
_text = "${_count++}";
});
// //子widget中保存需要的数据
// PageStorage.of(context).writeState(context, _text);
},
child: Text("click"),
)
],
),
);
}
@override
bool get wantKeepAlive => true;
}
-
flutter中调用native 的扫描二维码的sdk,使用flutter打开相机并预览,然后将摄像头读取到的数据传给native的sdk。
-
在flutter中添加camera的dependencies,如下:
dependencies: camera: path_provider: path: ……
-
-
在 main中初始化 WidgetsFlutterBinding 获取 camera的cameraDescription 对象,如下:
class Camera { static CameraDescription cameraDescription; } Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); Camera.cameraDescription = (await availableCameras()).first;//获取设备摄像头的信息 …… }
-
自定义 二维码扫描的widget,在里面初始化 CameraController对象,打开摄像头,并使用 CameraPreview 进行预览,如下:
class _QRCodeDemoState extends State<QRCodeDemo> {
CameraController _controller;
Future _future;
@override
void initState() {
//使用在main中获取的CameraDescription对象初始化 CameraController
//第二个参数指定图像的分辨率,ResolutionPreset.high 表示 高分辨率的图像
_controller = CameraController(Camera.cameraDescription, ResolutionPreset.high);
_future = _controller.initialize();//初始化并打开 摄像头
super.initState();
//在 初始化摄像头的完成后 ,使用CameraController的 startImageStream方法获取 摄像头传输的实时图像数据
_cameraInit.then((_){
_cameraInit.then((_) {
if (!mounted) {
return;
}
setState(() {});
}).whenComplete(() {
compute(send(_cameraController, context), ""); });
} }
}
//该函数必须是顶级函数或者static 函数
send(CameraController _cameraController, BuildContext context) async {
_cameraController.startImageStream((image) async {
await FordBasicChannel.sendCameraData().callNative({
"data": image.planes.map((e) => e.bytes).toList(), //获取到的图像像素点数据,
"width": image.width, //图像的size
"height": image.height //图像的size
}).then((value) {
if (value != null) {
//扫描结果处理
}
}, onError: (error) {
FordBody.switchBanner(
BannerNotification(true, type: BannerType.error, tip: "扫描出错,请退出重试"));
});
});
}
-
在 native 中 使用 method channel 接受图像的数据,android端代码如下:
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) super.configureFlutterEngine(flutterEngine) transientDataProvider.save(FlutterInitUseCase()) val util = ScanUtils() MethodChannelInvokeHandler(MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "scan_test")) .setMethodCallHandler(MethodChannel.MethodCallHandler { call, result -> if ("scan_test" == call.method) { val flutterData: List<ByteArray>? = call.argument<List<ByteArray>>("data")//图像的数据点 val width = call.argument<Int>("width") ?: 200 val height = call.argument<Int>("height") ?: 200 util.scan(flutterData, width, height, result) } }) }
-
获取到数据之后,需要将数据进行处理,startImageStream 方法的输出为 CameraImage,有 4 个属性: 图像格式, 高度, 宽度以及 planes ,planes 包含图像具体信息。
class CameraImage { final ImageFormat format; final int height; final int width; final List planes; }
-
注意在不同平台上的图像格式并不相同:Android: android.graphics.ImageFormat.YUV_420_888;iOS: kCVPixelFormatType_32BGRA (在 2.8.0 版本中的格式为 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, 后来在 4.0.0 版本中又改回为 32BGRA。)
-
由于图像格式不同,输出的 CameraImage 在 Android 和 iOS 端包含的信息也不一样:Android: planes 含有三个字节数组,分别是 Y、U 和 V plane;iOS:planes 只包含一个字节数组,即图像的 RGBA 字节。
-
参考的链接 在 Flutter 中实现实时图像检测
-
Android 的解析以及调要zxing代码如下:
fun scan(flutterData: List<ByteArray>?, width: Int, height: Int, result: MethodChannel.Result) { flutterData?.apply { //对图像数据进行处理 val Y: ByteBuffer = ByteBuffer.wrap(this[0]) val U: ByteBuffer = ByteBuffer.wrap(this[1]) val V: ByteBuffer = ByteBuffer.wrap(this[2]) val Yb: Int = Y.remaining() val Ub: Int = U.remaining() val Vb: Int = V.remaining() val data = ByteArray(Yb + Ub + Vb) Y[data, 0, Yb] V[data, Yb, Vb] U[data, Yb + Vb, Ub] // 进行zxing 二维码解析 val task = QRParse(result)//创建 zxing解析二维码的线程 task.execute(ScanData(data, width, height)) } } // 调用zxing的线程 class QRParse(private val result: MethodChannel.Result) : AsyncTask<ScanData, Void, String>() { val utils = ScanParseUtils() override fun doInBackground(vararg scanData: ScanData): String { //utils.processData 是 对zxing进行封装,对外提供的方法 val scanResult: String? = utils.processData(scanData[0].data, scanData[0].width, scanData[0].height, false) return scanResult ?: "" } override fun onPostExecute(scanResult: String?) { super.onPostExecute(scanResult) result.success(scanResult ?: "") } } // 对 flutter 传入数据的封装 data class ScanData(val data: ByteArray, val width: Int, val height: Int) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as ScanData if (!data.contentEquals(other.data)) return false if (width != other.width) return false if (height != other.height) return false return true } override fun hashCode(): Int { var result = data.contentHashCode() result = 31 * result + width result = 31 * result + height return result } }
-
ios的解析代码如下:
const FlutterStandardTypedData* typedData = args[@"bytesList"][0]; uint8_t* in = (uint8_t*)[[typedData data] bytes]; float* out = interpreter->typed_tensor<float>(input); for (int y = 0; y < height; ++y) { const int in_y = (y * image_height) / height; uint8_t* in_row = in + (in_y * image_width * image_channels); float* out_row = out + (y * width * input_channels); for (int x = 0; x < width; ++x) { const int in_x = (x * image_width) / width; uint8_t* in_pixel = in_row + (in_x * image_channels); float* out_pixel = out_row + (x * input_channels); for (int c = 0; c < input_channels; ++c) { out_pixel[c] = (in_pixel[c] - input_mean) / input_std; } } }
-
flutter 路由
-
在 MaterialApp 类中 的 routes 属性和 home属性:
-
routes:管理项目中的路由,数据为 Map<String,WidgetBuilder> ,String是路由的key,在页面跳转使用
Navigator.of(context).pushNamed
等方法名中包含name的方法时传入的字符串就是该值,或者native 指定 默认路由,使用 FlutterActivity 重写getInitialRoute()
方法返回的字符串或者使用 flutter engine对象的flutterEngine.navigationChannel.setInitialRoute()
方法字符串参数都是该值。 -
home 属性 ,在没有指定初始化页面路由时,加载的是home属性指定的页面。
-
-
路由跳转
- 使用
Navigator.of(context) .pushNamed
参数:- routeName,在MaterialApp 中routes 属性中的map的key值,必填
- arguments:需要传入的参数,选填。在下个页面 ,参数获取方式为,在build方法中使用
var args = ModalRoute.of(context).settings.arguments;
- 使用
Navigator.of(context).push()
,参数为Route对象,一般使用MaterialPageRoute 对象,
- 使用
-
路由返回
- 使用
Navigator.of(context) .pop(value)
直接返回上一个页面,有一个可选参数:value,表示 返回到上一个页面的数据,在 push方法的返回值future对象中获取。 - 使用
Navigator.of(context).popUntil((route) { return true }))
连续返回页面,popUntil的参数为 function 类型,其返回值为bool类型,当function 返回ture时,停止页面关闭。function的参数为 Route 类型,可以通过 Route 对象的 RouteSettings 对象做页面判断。
- 使用
-