Flutter&Rust#05 | 图片亮度调节

Flutter&rust1.png

匠心千刃 是张风捷特烈通过 Flutter 打造的 全平台 工具产品。基于 fx 应用框架和 tolyui 视图框架构建的软件应用。
匠心千刃中对于需要大量计算的场景,将使用 Rust 语言来实现。本文我们将从图片亮度调节这个功能需求,继续认识 Flutter&Rust 开发。


1. 交互效果和知识点

交互效果如下所示,可以选择一张图片,通过拖动底部的 Slider 或者在输入框中输入数字,来改变图片的亮度。

无标题项目.gif

该工具中包含的知识点有下面几条:

  • [1]. 通过 rust 提供异步方法,实现图片亮度变换。
  • [2]. 了解 字节数组 在 Dart 和 Rust 间的传递方式。
  • [3]. Slider 和输入框数据绑定,调节数值驱动图片转换。
  • [4]. 由于滑块事件触发非常频繁,异步任务可能耗时较长,有必要进行防抖处理。

2.异步处理和数据传递

上面的图片大小约 1.7 MB 在处理过程中,每次转换耗时约为 120ms 。如果转换函数是同步执行的,那么拖动时界面需要在转换完成后才可以更新,Slider 将会卡顿。异步处理,就像网络请求一样,任务完成之后通知 UI 更新,任务期间可以展示原来的数据,并添加 loading 示意任务状态(如上图左上角)。

我们知道图片资源可以从 文件网络assets 中加载,其实底层本质上都是将资源数据对应的 字节数组 进行解码,进行绘制渲染,呈现在屏幕上。
虽然 Dart 和 Rust 间可以通过文件名称来传递资源的磁盘位置,但是这比较局限。一方面,拖动事件触发频率比较高,就会有一些无意义的文件读取和存储的过程。另一方面,仅传入磁盘文件,对于网络图片就无法处理。
图片之所以能呈现,完全取决于 字节数组,所以在处理过程中,最好直接传递内存中的字节数字。下面是过程的示意图:

image.png

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 函数。在界面交互过程中

image.png


4. 界面交互与任务执行

界面交互中,有三个触发亮度调节的时机:

  • [1]. 图片选择之后,触发一次任务。
  • [2]. 拖动进度条时,值变化的事件会触发任务。
  • [3]. 输入框提交时间时, 触发一次任务。

image.png

当前需求中的核心状态量有如下四个,其中 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 拓展轻松实现防抖的效果:

image.png

由于 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 状态告知用户状态处理中。如下面顶部工具栏的左侧:

image.png

可以增加一个 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

评论0

显示验证码
没有账号?注册  忘记密码?