likes
comments
collection
share

Flutter长图显示,自定义显示图片的指定区域

作者站长头像
站长
· 阅读数 15

需求

话不多说,直切正题!我们经常会遇到一个需求,在在一个定高的卡片中显示长图,如下图所示。

Flutter长图显示,自定义显示图片的指定区域

假如我们要显示这个长图。如果不考虑自定义显示长图的区域,这个很好实现,没什么可说的。

Container(
  width: 300,
  height: 400,
  decoration: BoxDecoration(
      border: Border.all(color: Colors.deepOrangeAccent, width: 3)),
  child: Image.network(
    "https://fb-cdn.fanbook.mobi/fanbook/app/files/chatroom/image/9a61840fbbcf766b15f8601b66b9d63c.jpeg",
    fit: BoxFit.fitWidth,
  ),
),

关于BoxFit这个枚举,各个具体的枚举值含义,我直接从官方的注释中复制了出来,方便大家查阅。

  1. BoxFit.fill

Flutter长图显示,自定义显示图片的指定区域

  1. BoxFit.contain

Flutter长图显示,自定义显示图片的指定区域

  1. BoxFit.cover

Flutter长图显示,自定义显示图片的指定区域

  1. BoxFit.fitWidth

Flutter长图显示,自定义显示图片的指定区域

  1. BoxFit.fitHeight

Flutter长图显示,自定义显示图片的指定区域

  1. BoxFit.none

Flutter长图显示,自定义显示图片的指定区域

  1. BoxFit.scaleDown

Flutter长图显示,自定义显示图片的指定区域

问题

对于长图,只想显示图片的中间部分、顶部、底部、甚至任意区域位置,该如何实现。如果仅仅使用BoxFit枚举,恐怕不能实现产品经理那天马行空的想法。 要实现这个需求,相信有部分人已经知道方法了。 没错,就是绘制。通过自定义CustomPainter,然后使用Flutter Canvas对象的canvas.drawImageRect()方法来实现。

直接开写

1. 自定义裁剪对象

对于这个小工具,我们反着来写,可能更加容易理解。首先我们定义一个类XHImagClipper继承自CustomPainter

需求是对超长图进行自定义裁剪显示,很容易想到,我们需要哪些数据,至于数据怎么提供,我们下面再说。

  1. 需要Image对象,包含原图片的宽高信息, 这个Image来自dart:ui中的内置对象
  2. 需要知道裁剪的起始位置,我们用(x, y)表示, 范围都是[0,1], 比如对于y=0表示居顶;y=1表示居底;y=0.5表示中心位置。
  3. 需要裁剪的比例,用ratio来定义。

好了,把上面说的翻译一下,我们就实现了下面的代码。

import 'dart:ui' as ui;

/// 图片裁剪
class XHImageClipper extends CustomPainter {
  final ui.Image image;
  final double x;
  final double y;
  final double ratio;
  XHImageClipper(this.image, {this.x = 0.0, this.y = 0.0, this.ratio = 0.75});
  @override
  void paint(Canvas canvas, Size size) {
    //TODO
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    return true;
  }
}

接下来实现XHImageClipper中的核心方法paint()。就是自定义绘制部分。

小Tips

drawImageRect(Image image, Rect src, Rect dst, Paint paint)。这个是canvas的内部函数。就是把image中的区域src矩形抠出来绘制到dst矩形区域了,可以想象,如果两个矩形的宽高比不一致,图片会发生形变。有了这个知识储备,我们继续。

我们主要是用来处理超长、超宽图片的逻辑,假如我们定义图片的宽/高比,或者高/宽比超过3,就认为是超长,超宽图片,其他的图片显示图片的全部内容。

我们的目的就是把超长图片的指定区域矩形抠出来,绘制到dst矩形中,因此对于dst没有什么好说的,就是canvse的宽高。所以我们只需要计算出src矩形,就算完成了需求。

  1. 对于超长图片的逻辑。
  • x轴起点位置 final l = x * imgSourceWidth;
  • 宽度 final w = imgSourceWidth;
  • 高度final h = imgSourceWidth / ratio;
  • y轴起点位置double t = y * (imgSourceHeight - h)

源码如下,相对比较简单,直接看源码应该就理解了,不理解可以留言哈。

void paint(Canvas canvas, Size size) {
    final paint = Paint();
    final imgSourceWidth = image.width;
    final imgSourceHeight = image.height;

    Rect src = Rect.fromLTWH(
        0, 0, imgSourceWidth.toDouble(), imgSourceHeight.toDouble());
        
    //超长异形图
    if (imgSourceHeight / imgSourceWidth > 3.0) {
      debugPrint("hh:超长异形图");
      final l = x * imgSourceWidth;
      final w = imgSourceWidth;
      final h = imgSourceWidth / ratio;
      double t = y * (imgSourceHeight - h);
      src = Rect.fromLTWH(l, t, w.toDouble(), h);
    }
    //超宽异形图
    if (imgSourceWidth / imgSourceHeight > 3.0) {
      debugPrint("hh:超宽异形图");
      double t = y * imgSourceHeight;
      final h = imgSourceHeight;
      final w = imgSourceWidth * ratio;
      double l = x * (imgSourceWidth - w);
      if (l + w > imgSourceWidth) l = 0;
      src = Rect.fromLTWH(l, t, w.toDouble(), h.toDouble());
    }

    canvas.drawImageRect(
        image, src, Rect.fromLTWH(0, 0, size.width, size.height), paint);
  }

2. 下载图片数据

下载图片数据有很多方法,为了简单,我们直接用Flutter内部下载图片的方法来实现。

Future<ImageInfo> getImageInfoByProvider(ImageProvider provider) {
final Completer<ImageInfo> completer = Completer<ImageInfo>();
bool flag = false;
provider
    .resolve(const ImageConfiguration())
    .addListener(ImageStreamListener((info, _) {
  if (!flag) {
    completer.complete(info);
    flag = true;
  }
}));
return completer.future;
}

整个显示组件XHClipperImageContainer源码如下。 其实里面的x, y, 和 ratio。我们前文讲的XHImageClipper类已经解释过了,这里定义其实就是为了透传给XHImageClipper使用。然后里面的widthheight就是需要图片卡片的宽高。

class XHClipperImageContainer extends StatefulWidget {
  final double width;
  final double height;
  final double x;
  final double y;
  final double ratio;
  final ImageProvider imageProvider;

  const XHClipperImageContainer({
    Key? key,
    required this.imageProvider,
    required this.width,
    required this.height,
    this.x = 0,
    this.y = 0,
    this.ratio = 0.75,
  }) : super(key: key);

  @override
  _XHClipperImageContainerState createState() =>
      _XHClipperImageContainerState();
}

class _XHClipperImageContainerState extends State<XHClipperImageContainer> {
  XHImageClipper? clipper;

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

  Future<ImageInfo> getImageInfoByProvider(ImageProvider provider) {
    final Completer<ImageInfo> completer = Completer<ImageInfo>();
    bool flag = false;
    provider
        .resolve(const ImageConfiguration())
        .addListener(ImageStreamListener((info, _) {
      if (!flag) {
        completer.complete(info);
        flag = true;
      }
    }));
    return completer.future;
  }

  void _getImage() async {
    final imageInfo = await getImageInfoByProvider(widget.imageProvider);
    clipper = XHImageClipper(imageInfo.image,
        x: widget.x, y: widget.y, ratio: widget.ratio);
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: clipper,
      size: Size(widget.width, widget.height),
    );
  }

  @override
  void dispose() {
    clipper = null;
    super.dispose();
  }
}

3.使用

如果我想让图片距底显示在一个宽高比为0.75的卡片中,可以如下写。

Container(
  child: XHClipperImageContainer(
    width: 300,
    height: 300 * 4 / 3.0,
    x: 0,
    y: 1,
    ratio: 0.75,
    imageProvider: NetworkImage(
        "https://fb-cdn.fanbook.mobi/fanbook/app/files/chatroom/image/9a61840fbbcf766b15f8601b66b9d63c.jpeg"),
  ),
)

效果如下图,稍微再润色一下,就可以轻松实现前文中的效果了。

Flutter长图显示,自定义显示图片的指定区域

就这样,这篇就到此结束了~