FLutter 踩坑笔记

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的位置,根据左上角坐标进行限制的
 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中的 属性,使用方式如下:
      1. 父Wiget 必须为PageStorage
      1. 创建子Widget时必须有key这个属性
      1. 初始化子类对象时必须 给 key 赋值,且对象类型必须是PageStorageKey
      1. 在子Wiget中 使用 PageStorage.of(context).writeState(context, data); 保存数据
      1. 建议在didChangeDependencies 方法中使用PageStorage.of(context).readState(context); 读取保存的数据,在其他的生命周期方法如 build中读取也是可以的,在didChangeDependencies的好处时,该方法是在 widget 初始化之后直接调用的。而且每次刷新不会重复执行。
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 对象做页面判断。

猜你喜欢

转载自blog.csdn.net/genmenu/article/details/108772326