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
这个枚举,各个具体的枚举值含义,我直接从官方的注释中复制了出来,方便大家查阅。
-
BoxFit.fill
-
BoxFit.contain
-
BoxFit.cover
-
BoxFit.fitWidth
-
BoxFit.fitHeight
-
BoxFit.none
-
BoxFit.scaleDown
问题
对于长图,只想显示图片的中间部分、顶部、底部、甚至任意区域位置,该如何实现。如果仅仅使用BoxFit
枚举,恐怕不能实现产品经理那天马行空的想法。
要实现这个需求,相信有部分人已经知道方法了。 没错,就是绘制。通过自定义CustomPainter
,然后使用Flutter Canvas
对象的canvas.drawImageRect()
方法来实现。
直接开写
1. 自定义裁剪对象
对于这个小工具,我们反着来写,可能更加容易理解。首先我们定义一个类XHImagClipper
继承自CustomPainter
。
需求是对超长图进行自定义裁剪显示,很容易想到,我们需要哪些数据,至于数据怎么提供,我们下面再说。
- 需要
Image
对象,包含原图片的宽高信息, 这个Image
来自dart:ui
中的内置对象 - 需要知道裁剪的起始位置,我们用
(x, y)
表示, 范围都是[0,1]
, 比如对于y=0
表示居顶;y=1
表示居底;y=0.5
表示中心位置。 - 需要裁剪的比例,用
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
矩形,就算完成了需求。
- 对于超长图片的逻辑。
- 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
使用。然后里面的width
和height
就是需要图片卡片的宽高。
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"),
),
)
效果如下图,稍微再润色一下,就可以轻松实现前文中的效果了。
就这样,这篇就到此结束了~
转载自:https://juejin.cn/post/7205546336081952828