阅读 2133

Flutter——仿.知乎列表的视差效果

介绍

浏览知乎时,发现它的首页列表在滚动时,某个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;
  }

}
复制代码

至此整个功能就完成了,谢谢大家的阅读。

Demo

github : Demo

系列文章

Flutter——仿网易云音乐App(基础版)

实现网易云音乐的滑动冲突处理效果

Flutter自定义View——仿高德三级联动Drawer

Flutter 自定义View——仿同花顺自选股列表

Flutter入门练习——Evenet&Method Channel协作加载大图

文章分类
Android
文章标签