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,同时支持多个子组件一起平移、缩放:
InteractiveViewer class - widgets library - Dart API
b、ColorFilter:色值过滤器,有点类似于Android上给paint设置颜色过滤:
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!,
)
)
);
}
}
这里有两处需要解释一下:
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();
}
}
代码解释:
在自定义的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篇;