匠心千刃 是张风捷特烈通过 Flutter 打造的 全平台 工具产品。基于 fx 应用框架和 tolyui 视图框架构建的软件应用。
匠心千刃中对于需要大量计算的场景,将使用 Rust
语言来实现。本文我们将从图片亮度调节这个功能需求,继续认识 Flutter&Rust 开发。
1. 交互效果和知识点
交互效果如下所示,可以选择一张图片,通过拖动底部的 Slider 或者在输入框中输入数字,来改变图片的亮度。
该工具中包含的知识点有下面几条:
- [1]. 通过 rust 提供异步方法,实现图片亮度变换。
- [2]. 了解 字节数组 在 Dart 和 Rust 间的传递方式。
- [3]. Slider 和输入框数据绑定,调节数值驱动图片转换。
- [4]. 由于滑块事件触发非常频繁,异步任务可能耗时较长,有必要进行防抖处理。
2.异步处理和数据传递
上面的图片大小约 1.7 MB 在处理过程中,每次转换耗时约为 120ms
。如果转换函数是同步执行的,那么拖动时界面需要在转换完成后才可以更新,Slider 将会卡顿。异步处理,就像网络请求一样,任务完成之后通知 UI 更新,任务期间可以展示原来的数据,并添加 loading 示意任务状态(如上图左上角)。
我们知道图片资源可以从 文件
、网络
、assets
中加载,其实底层本质上都是将资源数据对应的 字节数组 进行解码,进行绘制渲染,呈现在屏幕上。
虽然 Dart 和 Rust 间可以通过文件名
称来传递资源的磁盘位置,但是这比较局限。一方面,拖动事件触发频率比较高,就会有一些无意义的文件读取和存储的过程。另一方面,仅传入磁盘文件,对于网络图片就无法处理。
图片之所以能呈现,完全取决于 字节数组,所以在处理过程中,最好直接传递内存中的字节数字。下面是过程的示意图:
Dart 读取文件之后,可以得到字节数组 (Uint8List), 通过 Image.memory 构造展示内存中的数据:
Uint8List? _raw;
void _onTapSelect() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.image);
if (result != null && result.files.isNotEmpty) {
String? path = result.files.first.path;
if (path != null) {
_raw = await File(path).readAsBytes();
}
}
}
Image.memory(widget.raw!)
3. Rust 图形处理与桥接
对于 Rust 端而言,它接收字节数组作为输入,进行数据处理之后将结果字节数组返回,作为 输出。如下的 brightness
作为桥接函数,其中 factor 是亮度的百分率。 分率为 1
表示原图,越小则亮度越暗,越大亮度越高。
根据入参的字节数组通过 load_from_memory
加载为 DynamicImage
对象,亮度的具体处理封装为 adjust_brightness
函数进行处理。最终将结果图像,输出为字节数组返回即可:
pub fn brightness(image_bytes: Vec<u8>, factor: f32) -> Vec<u8> {
let img = image::load_from_memory(&image_bytes).unwrap();
let adjusted_img = adjust_brightness(&img, factor);
let mut buf: Vec<u8> = Vec::new();
let mut cursor = Cursor::new(&mut buf);
adjusted_img.write_to(&mut cursor, image::ImageFormat::Png).unwrap();
buf
}
adjust_brightness
函数会遍历每一个像素点,将对应的 rgb 分量乘以 factor, 形成新的像素点,放入到返回的图像中:
use image::{DynamicImage, GenericImageView, Rgba, RgbaImage};
pub fn adjust_brightness(img: &DynamicImage, factor: f32) -> RgbaImage {
let (width, height) = img.dimensions();
let mut output = RgbaImage::new(width, height);
for (x, y, pixel) in img.pixels() {
let Rgba(data) = pixel;
let r = (data[0] as f32 * factor).min(255.0) as u8;
let g = (data[1] as f32 * factor).min(255.0) as u8;
let b = (data[2] as f32 * factor).min(255.0) as u8;
output.put_pixel(x, y, Rgba([r, g, b, data[3]]));
}
output
}
此时,通过 flutter_rust_bridge_codegen generate 命令,就可以自动生成 Flutter 端的 Dart 函数。在界面交互过程中
4. 界面交互与任务执行
界面交互中,有三个触发亮度调节的时机:
- [1]. 图片选择之后,触发一次任务。
- [2]. 拖动进度条时,值变化的事件会触发任务。
- [3]. 输入框提交时间时, 触发一次任务。
当前需求中的核心状态量有如下四个,其中 factor 的值由 Slider 和 输入框来更新,同时它也决定着这两者值的呈现。_raw
是原始图片字节数组,由选择的文件得到(当然网络图片也可以)。_result
是结果图片字节数组,它的内容将根据当前的 factor
通过 rust 函数得到。
Uint8List? _raw;
Uint8List? _result;
double factor = 1;
final TextEditingController _ctrl = TextEditingController();
在 onChangeValue 函数中,根据新的 value 值来更新这些决定界面表现的核心状态变量。然后只需要在需要改变亮度的那三个时机中触发 onChangeValue
即可:
void onChangeValue(double value) async {
factor = value;
setState(() {});
if (_raw == null) return;
_result = (await brightness(imageBytes: _raw!, factor: factor));
setState(() {});
}
5. 执行中与防抖处理 Debounce
Slider 的变化事件触发很频繁,而图片相关的计算任务相对耗时,没有非要每次变动都触发一次任务的执行。我们可以进行防抖处理,至于防抖这里就不展开介绍了,感兴趣的可以看一下我的异步专栏中的 《探索 Stream 的转换原理与拓展》
我们的目的是在 onChangeValue
连续触发时,间隔小于指定时长(如50ms),忽略该事件。直至某次事件后 50 ms 没有再触发 onChangeValue
。说明用户在这个点上停留了一会,可能是期望的事件。期间这些连续的短暂变化可能只是中间过程,没必要实时触发一般任务,对于网络请求来说也是如此。我们可以通过 stream_transform 对 Stream 拓展轻松实现防抖的效果:
由于 debounce 在状态类中非常常用,可以封装一个 mixin 简化使用和复用。如下所示,DebounceMixin
依赖 State 类,其中主要通过 StreamController
维护 Stream 元素的产出,初始化状态 initState 时转换控制器中的 stream 触发 onEvent 回调。
mixin DebounceMixinextends StatefulWidget, E> on State {
StreamSubscription? _subscription;
final StreamController _streamCtrl = StreamController();
@override
void initState() {
_subscription = _streamCtrl.stream.debounce(duration).listen(onEvent);
super.initState();
}
@override
void dispose() {
_subscription?.cancel();
_streamCtrl.close();
super.dispose();
}
void emit(E data)=> _streamCtrl.add(data);
FutureOr<void> onEvent(E data);
Duration get duration => const Duration(milliseconds: 50);
}
使用时先让状态类混入 DebounceMixin 第二泛型是监听事件中回调的数据类型,这里是亮度分率,类型是 double. 然后实现 onEvent 回调处理图片亮度变化异步任务; onChangeValue 这个频繁触发的事件中,维护界面展示的状态数据,通过 emit
将 value 加入到 stream,将在满足时长的条件下,触发 onEvent
:
class _BrightnessPageState extends State<BrightnessPage>
with DebounceMixin<BrightnessPage,double> {
@override
void onEvent(double data) async{
_result = await brightness(imageBytes: _raw!, factor: factor);
setState(() {});
}
void onChangeValue(double value) async {
factor = value;
desc = "$valueDisplay%";
_ctrl.text = valueDisplay;
setState(() {});
emit(value);
}
}
如果异步任务处理的时间比较长,可以在界面上展示一下 loading 状态告知用户状态处理中。如下面顶部工具栏的左侧:
可以增加一个 ValueNotifier
的 loading 状态,使用 ValueListenableBuilder
组件监听它进行 局部构建。根据回调中 value 值确定构建的内容:
ValueNotifier<bool> loading = ValueNotifier(false);
@override
void onEvent(double data) async {
if (loading.value || _raw == null) return;
loading.value = true;
_result = (await brightness(imageBytes: _raw!, factor: factor));
loading.value = false;
setState(() {});
}
Widget _buildLeading() {
return ValueListenableBuilder(
valueListenable: loading,
builder: (_, value, __) {
if (!value) return _buildIndicator();
return const Padding(
padding: EdgeInsets.only(left: 8.0),
child: CupertinoActivityIndicator(radius: 8),
);
});
}
尾声
到这里,图片亮度调节的功能就完成了。随着匠心千刃的后续研发,会遇到更多 Dart 和 Rust 桥接的知识。未来匠心千刃也会作为一个工具软件,供用户随意使用。更多精彩内容,敬请期待~
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22496,转载请注明出处。
评论0