1、需求概述:

一套代码,到处运行,节约开发人力成本+方便统一编码方式、方便维护(番外:在一年多前自己对Flutter-dart也略有涉猎,不过尚未完成独立完整的app项目),本着这样的目的和尝新的心态,在加上公司的业务和技术栈规划,直接入手撸了一份Flutter Android pad的项目。项目中有一个功能,需求如下:

a、展示一张底图,在底图的基础上,可以根据位置,动态添加logo图片或者接口返回的Mark图片,有点类似底图上的打点,根据位置打mark点,之后形成的多图层可以支持整体手势缩小放大、移动;

b、底部有4个按钮,对应4个Slider进度条,咱们可以选择按钮,拖动进度条,动态改变,上面我们底图+N*Mark 点的聚合图层的  对比度、亮度、饱和度同时支持黑白色图片的反转;

2、技术实现:

1)、如果在Android原生上,咱们怎么来实现,通过学习得知,Android原生上咱们可以通过自定义view或者surfaceView 来绘制聚合图层,之后通过android上的 ColorMatrix颜色变换矩阵在ondraw方法中,给paint设置colorMatrix 色值筛选器来实现对图片bitmap的控制转换,通过ontouch捕捉手势事件,控制图层的缩放移动;

2)、那么在Flutter上,怎么实现呢?才开始一脸蒙圈,通过学习得知,Flutter上有两个组件:

a.InteractiveViewer :

 官方解释,InteractiveViewer可以包含多个子widget,同时支持多个子组件一起平移、缩放:

flutter的GestureDetector class这个类的onForcePressEndonForcePressPeakonF flutter interactiveviewer_ide

InteractiveViewer class - widgets library - Dart API

b、ColorFilter:色值过滤器,有点类似于Android上给paint设置颜色过滤:

 

flutter的GestureDetector class这个类的onForcePressEndonForcePressPeakonF flutter interactiveviewer_sed_02

ColorFilter class - dart:ui library - Dart API

c、那么好,在Flutter上的工具都有了,下面就开始具体需求实现吧;

3、实际操作

1)首先我们要定义一个widget,用来做首页展示:

具体代码如下:

import 'package:f_umt_app/widget/ImageFilter.dart';
import 'package:flutter/material.dart';

///图片处理界面
///扫描功能,针对原始图片 黑白反转、 对比度、亮度调节、放大缩小功能预研
class PhotoTransUtilWidget extends StatefulWidget {

  @override
  State<StatefulWidget> createState() {
    return PhotoTransUtilWidgetState();
  }

}

class PhotoTransUtilWidgetState extends State<PhotoTransUtilWidget> with TickerProviderStateMixin {

  TransformationController _transformationController = TransformationController(); //可动态添加,缩放InteractiveViewer组件的控制器

  double _sliderValue = 0.0;
  int sliderType = 0;

  bool _sliderIsShow = true;
  String _sliderNotice = "";


  double minSliderValue = 0;
  double maxSliderValue = 1;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          "图片处理界面",
          style: TextStyle(color: Colors.red, fontSize: 20.0),
        ),
      ),
      body: ConstrainedBox(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          mainAxisSize: MainAxisSize.max,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              mainAxisSize: MainAxisSize.min,
              children: [
                RaisedButton(
                  child: Text("对比度"),
                  onPressed: () {
                    setState(() {
                      _sliderNotice = "请动态改变对比度:";
                      _sliderIsShow = false;

                      _sliderValue = 0;
                      sliderType = 0;
                      minSliderValue = 0;
                      maxSliderValue = 1;
                    });
                  },
                ),
                FlatButton(
                  child: Text("亮度"),
                  onPressed: () {
                    setState(() {
                      _sliderNotice = "请动态改变亮度:";
                      _sliderIsShow = false;

                      _sliderValue = 0;
                      sliderType = 1;
                      minSliderValue = 0;
                      maxSliderValue = 1;
                    });
                  },
                ),
                OutlineButton(
                  child: Text("放大,缩小"),
                  onPressed: () {
                    setState(() {
                      _sliderNotice = "请动态改变图片大小:";
                      _sliderIsShow = false;

                      _sliderValue = 1.0; //缩放倍数默认为1.0
                      sliderType = 2;
                      minSliderValue = 0.5;
                      maxSliderValue = 2.5;
                    });
                  },
                ),
                TextButton(
                  child: Text("黑白反转"),
                  onPressed: () {
                    setState(() {
                      _sliderIsShow = false;
                      _sliderNotice = "点击图片反转:";

                      _sliderValue = 0.75;
                      sliderType = 3;
                      minSliderValue = 0.75;
                      maxSliderValue = 1;
                    });
                  },
                ),
              ],
            ),
            Expanded(
              child: InteractiveViewer(
                scaleEnabled: true,
                maxScale: 5,
                minScale: 0.3,
                child: ConstrainedBox(
                  constraints: BoxConstraints(minWidth: double.infinity),
                  child: Stack(
                    alignment: Alignment.center,
                    fit: StackFit.expand,
                    children: [
                      ImageFilter(
                        hue: sliderType == 3 && _sliderValue!=0.75 ? _sliderValue : 0,
                        brightness: sliderType == 1 ? _sliderValue : 0,
                        contrast: sliderType == 0 ? _sliderValue : 0,
                        child: Image(
                          image: AssetImage("images/person.jpg"),
                          fit: BoxFit.fill,
                        ),
                      ),
                      Positioned(
                        width: 60.0,
                        height: 60.0,
                        top: 20.0,
                        right: 50.0,
                        child: ImageFilter(
                          hue: sliderType == 3 && _sliderValue!=0.75 ? _sliderValue : 0,
                          brightness: sliderType == 1 ? _sliderValue : 0,
                          contrast: sliderType == 0 ? _sliderValue : 0,
                          child: Image(
                            image: AssetImage("images/icon1.jpg"),
                            fit: BoxFit.fill,
                          ),
                        ),
                      ),
                      Positioned(
                          width: 60.0,
                          height: 60.0,
                          bottom: 0,
                          left: 20.0,
                          child: ImageFilter(
                            hue: sliderType == 3 && _sliderValue!=0.75 ? _sliderValue : 0,
                            brightness: sliderType == 1 ? _sliderValue : 0,
                            contrast: sliderType == 0 ? _sliderValue : 0,
                            child: Image(
                              image: AssetImage("images/icon2.jpg"),
                              fit: BoxFit.fill,
                            ),
                          )),
                    ],
                  ),
                ),
                transformationController: _transformationController,
              ),
            ),

            //slider bar
            Offstage(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                mainAxisSize: MainAxisSize.max,
                children: [
                  Text(
                    _sliderNotice,
                    style: TextStyle(color: Colors.red, fontSize: 25.0),
                  ),
                  Slider(
                    value: _sliderValue,
                    onChanged: (value) {
                      setState(() {
                        print("当前选中的值$value");
                        _sliderValue = value;
                      });
                      _scaleImage();
                    },
                    min: minSliderValue,
                    max: maxSliderValue,
                  ),
                ],
              ),
              offstage: _sliderIsShow,
            )
          ],
        ),
        constraints: BoxConstraints(minWidth: double.infinity),
      ),
    );
  }

  _scaleImage() async {
    //在现有的基础上进行缩放
    if (sliderType == 2) {
      var matrix = Matrix4.identity();
      matrix.scale(_sliderValue);
      _transformationController.value = matrix;
    }
  }
}

2)接着我们定义一个图片色值处理的自定义widget  


ImageFilter


import 'package:flutter/material.dart';

import 'ColorFilterGenerator.dart';

class ImageFilter extends StatefulWidget {

  double? brightness, contrast, hue;//明暗度,对比度,色相
  Widget? child;

  ImageFilter({Key? key, this.brightness, this.contrast, this.hue, this.child}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return StateImageFilter();
  }

}

class StateImageFilter extends State<ImageFilter> {
  @override
  Widget build(BuildContext context) {
    return ColorFiltered(
        colorFilter: ColorFilter.matrix(
            ColorFilterGenerator.brightnessAdjustMatrix(
              value: widget.brightness!,
            )
        ),
        child: ColorFiltered(
            colorFilter: ColorFilter.matrix(
                ColorFilterGenerator.contrastAdjustMatrix(
                  value: widget.contrast!,
                )
            ),
            child: ColorFiltered(
              colorFilter: ColorFilter.matrix(
                  ColorFilterGenerator.hueAdjustMatrix(
                    value: widget.hue!,
                  )
              ),
              child: widget.child!,
            )
        )
    );
  }

}

这里有两处需要解释一下:

flutter的GestureDetector class这个类的onForcePressEndonForcePressPeakonF flutter interactiveviewer_Text_03

InteractiveViewer中的children可以支持放多个图片,咱们可以将我们的多图聚合封装到一个stack层叠布局中,多图的image组件通过Positioned  widget来定位动态添加;

添加的imge需要支持黑白反转、对比度、饱和度、亮度动态处理功能的图片,用ImageFilter包裹;

这里为了方便起见,我就放3张图片,第一张ImageFiler是底图、后面两张ImageFilter是模拟的打点mark图;

3)封装矩阵变换色值计算工具类:ColoFilterGenerator:

import 'dart:math';

///对比度,亮度、饱和度、黑白色值反转,矩阵计算公式
class ColorFilterGenerator {
  static const List<double> DELTA_INDEX = <double>[
    0,
    0.01,
    0.02,
    0.04,
    0.05,
    0.06,
    0.07,
    0.08,
    0.1,
    0.11,
    0.12,
    0.14,
    0.15,
    0.16,
    0.17,
    0.18,
    0.20,
    0.21,
    0.22,
    0.24,
    0.25,
    0.27,
    0.28,
    0.30,
    0.32,
    0.34,
    0.36,
    0.38,
    0.40,
    0.42,
    0.44,
    0.46,
    0.48,
    0.5,
    0.53,
    0.56,
    0.59,
    0.62,
    0.65,
    0.68,
    0.71,
    0.74,
    0.77,
    0.80,
    0.83,
    0.86,
    0.89,
    0.92,
    0.95,
    0.98,
    1.0,
    1.06,
    1.12,
    1.18,
    1.24,
    1.30,
    1.36,
    1.42,
    1.48,
    1.54,
    1.60,
    1.66,
    1.72,
    1.78,
    1.84,
    1.90,
    1.96,
    2.0,
    2.12,
    2.25,
    2.37,
    2.50,
    2.62,
    2.75,
    2.87,
    3.0,
    3.2,
    3.4,
    3.6,
    3.8,
    4.0,
    4.3,
    4.7,
    4.9,
    5.0,
    5.5,
    6.0,
    6.5,
    6.8,
    7.0,
    7.3,
    7.5,
    7.8,
    8.0,
    8.4,
    8.7,
    9.0,
    9.4,
    9.6,
    9.8,
    10.0
  ];

  ///色值调节,黑白反转
  static List<double> hueAdjustMatrix({double? value}) {
    value = value!;

    if (value == 0)
      return [
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
      ];
    return List<double>.from(<double>[
      //R  G   B    A  Const
      -value, 0, 0, 0, 255, //
      0, -value, 0, 0, 255, //
      0, 0, -value, 0, 255, //
      0, 0, 0, 1, 0, //
    ]).map((i) => i.toDouble()).toList();
  }

  ///亮度调节
  static List<double> brightnessAdjustMatrix({double? value}) {
    if (value! <= 0)
      value = value * 255;
    else
      value = value * 100;

    if (value == 0)
      return [
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
      ];

    return List<double>.from(<double>[
      1,
      0,
      0,
      0,
      value,
      0,
      1,
      0,
      0,
      value,
      0,
      0,
      1,
      0,
      value,
      0,
      0,
      0,
      1,
      0
    ]).map((i) => i.toDouble()).toList();
  }

  ///饱和度
  static List<double> saturationAdjustMatrix({double? value}) {
    value = value! * 100;

    if (value == 0)
      return [
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
      ];

    double x =
        ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble();

    double lumR = 0.3086;
    double lumG = 0.6094;
    double lumB = 0.082;

    return List<double>.from(<double>[
      (lumR * (1 - x)) + x,
      lumG * (1 - x),
      lumB * (1 - x),
      0,
      0,
      lumR * (1 - x),
      (lumG * (1 - x)) + x,
      lumB * (1 - x),
      0,
      0,
      lumR * (1 - x),
      lumG * (1 - x),
      (lumB * (1 - x)) + x,
      0,
      0,
      0,
      0,
      0,
      1,
      0,
    ]).map((i) => i.toDouble()).toList();
  }

  ///对比度
  static List<double> contrastAdjustMatrix({double? value}) {
    value = value! * 100;

    if (value == 0)
      return [
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
        0,
        0,
        0,
        0,
        1,
        0,
      ];

    double x;

    if (value < 0) {
      x = 127 + value / 100 * 127;
    } else {
      x = value % 1;
      if (x == 0) {
        x = DELTA_INDEX[value.toInt()];
      } else {
        //x = DELTA_INDEX[(p_val<<0)]; // this is how the IDE does it.
        x = DELTA_INDEX[(value.toInt() << 0)] * (1 - x) +
            DELTA_INDEX[(value.toInt() << 0) + 1] *
                x; // use linear interpolation for more granularity.
      }
      x = x * 127 + 127;
    }

    return List<double>.from(<double>[
      x / 127,
      0,
      0,
      0,
      0.5 * (127 - x),
      0,
      x / 127,
      0,
      0,
      0.5 * (127 - x),
      0,
      0,
      x / 127,
      0,
      0.5 * (127 - x),
      0,
      0,
      0,
      1,
      0,
    ]).map((i) => i.toDouble()).toList();
  }
}

代码解释:

 

flutter的GestureDetector class这个类的onForcePressEndonForcePressPeakonF flutter interactiveviewer_Text_04

在自定义的ImageFilter中,我们需要动态改变ImageFilter中的child的色值背景、透明度等;

Flutter官方提供的ColorFiltered里面有colorFilter属性,可以通过ColorFilter.matrix来变换矩阵,动态设置色值;matrix需要传递一个List<double> matrix,那么我们就来构造一个double的集合来创造一个矩阵;这里类似于Android上的ColorMatrix矩阵变换,有不懂的童鞋,可以参考这边博文:

4、总结:

通过上面的编码,在InteractiveViewer中动态添加多图片,形成多图层,支持手势的动态放大缩小平移,同时也支持代码控制InteractiveViewer的动态缩放功能(InteractiveViewer通过transformationController属性实现代码控制界面动态的缩放、平移);

通过ColorFilter组件的色值矩阵变换,来控制多张图片的亮度、饱和度、对比度,黑白色值的反转;

最终实现产品需求;

5、一些建议:

如果做过android或者ios原生开发的童鞋,如果学习跨平台,使用了Flutter进行开发,Flutter上不好实现的效果,或者没有做过的效果,可以参考Android 或ios 原生app的实现方法,因为毕竟Flutter也是最终打包成apk文件,到Android硬件设备上运行,画布canvs paint画笔 color等方法和设计应该有共通之处,高级ui 类似Android的自定义view篇;