介绍
浏览知乎时,发现它的首页列表在滚动时,某个item会根据滚动位置展示图片的不同部分,如果搭配一些特制的广告图,会造成一种视觉上的穿透效果。
大致如下:
此为实现的Demo效果
下方是要展示的原图
图片信息: 1080*1920 大小 2.15m
因为还研究别的,所以图片刻意用的大体积的。
这个效果还是很有意思的,闲来无事,就实现一下试试。
实现
我们的用于显示图片的widget就叫:DrawImageItem
此为Demo,所以代码写的有些随意,见谅。
基础页面结构
整个页面就是一个listview,总共30个item且分别在第5、第10 插入咱们的DrawImageItem。
///用于取到list的相关信息
final GlobalKey key = GlobalKey();
Widget listView(Size size){
return ListView(
padding: EdgeInsets.all(0),
key: key,
controller: controller,
children: List.generate(30, (index){
return (index == 10 || index == 5) ? specialOne(size): Container(
width: size.width,height: size.height/6,
color: index % 2 == 0 ? Colors.blue : Colors.red,
);
}),
);
}
Widget specialOne(Size size){
return Container(
width: size.width,height: size.height/6,
child: DrawImageItem(size: size, controller: controller,viewPortHeight: size.height/6,
parentKey: key,),
);
}
下面我们看一下DrawImageItem的实现。
DrawImageItem
我们在页面里传入了一部分值:
class DrawImageItem extends StatefulWidget{
//这个size实际上有些狭隘了,如果封装成工具的话,最好取自parent
//不过这是demo,所以随意一些
//整个页面的尺寸 用于计算与图片的比例
final Size size;
//list view 的控制器
final ScrollController controller;
//item 高度
final double viewPortHeight;
//list view 的 key
final GlobalKey parentKey;
const DrawImageItem({Key key, @required this.size,@required this.controller,this.viewPortHeight
,this.parentKey}) : super(key: key);
@override
State<StatefulWidget> createState() {
return DrawImageItemState(size,controller,viewPortHeight,parentKey);
}
}
接着我们看一下 它的state。
DrawImageItemState
除了接收外面传进来的值外,还实例了一个图片:
//因为是demo,所以直接在这里实例化一个。
final Image image = Image.asset('assets/lemon.png');
因为flutter是逻辑像素,与图片像素并不相等,所以我们需要计算一下它们之间的比例。
///屏幕/图片
double widthRatio;
double heightRatio;
另外,我们绘制图片的部分用到的方法是:
// 参数
//1、图片源(ui.Image)
//2、图片上的截取部分
//3、绘制到屏幕上的位置
//4、画笔
canvas.drawImageRect(_image, srcRect, dstRect, myPaint);
//在CustomPainter中,后面会介绍
因此我们需要创建一些变量:
//_image
ui.Image uiImage;
///屏幕/图片
double widthRatio;
double heightRatio;
///image
Rect srcRect ;
///view
Rect dstRect ;
ok,所需要的变量都创建完毕了,我们现在在DrawImageItemState的initState方法中初始化它们:
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final ImageStream newStream = image.image.resolve(createLocalImageConfiguration(context));
newStream.addListener(ImageStreamListener((image,_){
if(image?.image != null){
uiImage = image.image;
widthRatio = uiImage.width / size.width;
heightRatio = uiImage.height / size.height;
initRect();
initListener();
setState(() {
});
}
}));
});
通过上面的方法,我们拿到了图片的ui.Image和对应的比例,然后我们看initRect() :
void initRect(){
final RenderBox renderBox = context.findRenderObject() as RenderBox;
Offset globalPos = renderBox.localToGlobal(Offset.zero,ancestor: parentKey.currentContext.findRenderObject());
//截取图片区域的rect
//这里要注意将逻辑像素转成图片像素
//同时注意上边界和下边界的闲置
//(咱们item宽度等于屏幕,所以就不管了)
srcRect = Rect.fromLTWH(
globalPos.dx,
math.min(globalPos.dy*heightRatio, uiImage.height.floorToDouble() - (viewPortHeight*widthRatio)),
size.width * widthRatio,
viewPortHeight * heightRatio);
//绘制区域的rect,
dstRect = Rect.fromLTWH(
0,
0,
size.width,//宽和高都是外面传进来的。
viewPortHeight);
}
这样 srcRect和dstRect初始化了,接下来看initListener():
//此方法主要是监听list的滚动,并刷新截取区域
void initListener(){
controller.addListener(() {
if(mounted && context != null){
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final Offset dstOffset = renderBox.localToGlobal(Offset.zero,ancestor: parentKey.currentContext.findRenderObject());
//这里要对截取rect的偏移量进行一个换算
final Offset realOffset = dstOffset.dy <= 0
? Offset(dstOffset.dx,0)
: (dstOffset.dy * heightRatio) >= uiImage.height
? Offset(dstOffset.dx,uiImage.height.toDouble()) :dstOffset;
//生成 截取图片的目标区域
srcRect = Rect.fromLTWH(
realOffset.dx,
math.min(realOffset.dy*heightRatio, uiImage.height.floorToDouble() - (viewPortHeight*widthRatio)),
size.width * widthRatio,
viewPortHeight * heightRatio);
setState(() {
});
}
});
}
这样我们的初始化工作就完成了,下面开始看一下页面布局:
@override
Widget build(BuildContext context) {
return uiImage == null ?
Center(child: Text('loading'),)
: CustomPaint(
painter: MyPaint(uiImage,srcRect,dstRect),
);
方法很简单,因为uiImage的获取是个异步,所以我先随便放一个占位widget,之后取到uiImage后,我们通过CustomPaint来进行图片的显示,具体内容则是在MyPaint(uiImage,srcRect,dstRect)中。
MyPaint
因为我们的数据在外层处理完毕,MyPaint(uiImage,srcRect,dstRect) 内部代码就相对简单,代码如下:
它主要接收三个参数,图片、图片截取区域、绘制目标区域。
class MyPaint extends CustomPainter{
final ui.Image _image;
final Rect srcRect ;
final Rect dstRect ;
MyPaint(this._image, this.srcRect, this.dstRect);
final Paint myPaint = Paint()..isAntiAlias = true;
@override
void paint(Canvas canvas, Size size) {
//通过这个方法,我们将截取的区域绘制到目标区域
canvas.drawImageRect(_image, srcRect, dstRect, myPaint);
}
@override
bool shouldRepaint(MyPaint oldDelegate) {
// TODO: implement shouldRepaint
return srcRect != oldDelegate.srcRect || dstRect != oldDelegate.dstRect;
}
}
至此整个功能就完成了,谢谢大家的阅读。