Flutter 仿探探编辑页图片展示控件

383 阅读3分钟


PS:页面所有图片均来自涂鸦智能平台

功能点

  • 布局:Flow
  • 拖拽排序:Draggable + DragTarget

代码少,逻辑比较简单,注释也比较详细,我就直接上代码了

暴露给使用者的属性

class TTAlbumLayout extends StatefulWidget {

  TTAlbumLayout(
    this.imagePaths,
    {
      this.onMoveCompleted,
      this.onTap,
      this.onLongPress,
      this.onAddImageTap,
      this.size,
      double spacing,
      double margin,
    }
  ): _margin = margin == null ? 10 : margin,
     _spacing = spacing == null ? 10 : spacing;

  // 图片地址,布局最多只有6个位置,
  // imagePaths.length > 6,取前6个值
  final List<String> imagePaths;

  // 布局长宽
  final double size;

  // Item与Item之间的间距,横向纵向一样的值
  final double _spacing;

  // 外边距,Item距离布局四边的距离
  final double _margin;

  // 拽拽成功回调,返回所拖拽Item拖拽前和拖拽后的索引
  final OnMoveCompleted onMoveCompleted;

  // 点击回调,返回当前点击Item的索引
  final ValueChanged<int> onTap;

  // 长按回调,返回当前长按Item的索引
  final ValueChanged<int> onLongPress;

  // 添加图片回调,无返回值
  final OnAddImageTap onAddImageTap;

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

_TTAlbumLayoutState

class _TTAlbumLayoutState extends State<TTAlbumLayout> {

  List<String> get _imagePaths => widget.imagePaths;
  // 正在拖拽的Item当前的位置
  int _moveIndex = -1;

  @override
  Widget build(BuildContext context) {

    // 布局长宽,不设置或者设置的值小于
    // (widget.margin*2 + widget.spacing*2)*2,
    // 则使用屏幕宽度作为布局的长宽
    double _size = widget.size;
    if (widget.size == null || widget.size < (widget._margin*2 + widget._spacing*2)*2) {
      _size = MediaQuery.of(context).size.width;
    }

    return Flow(
      delegate: _FlowDelegate(
        size: _size,
        spacing: widget._spacing,
        margin: widget._margin,
      ),
      children: _buildItems(
        _imagePaths,
        _size,
        widget._spacing,
        widget._margin,
      ),
    );
  }

  // 创建Item,先计算好每个Item的长宽
  List<Widget> _buildItems(List<String> imagePaths, double size, double spacing, double margin) {
    List<Widget> widgets = List();
    for (var i = 0; i < 6; i++) {
      // 小Item的长宽
      var imageWidth = (size - margin - margin - spacing * 2)*1/3;
      if (i == 0) {
        // 第一个大Item的长宽
        imageWidth = (size - margin - margin - spacing) - imageWidth;
      }
      // 填充Item,当需要展示图片小于6张时,多余的位置填充添加按钮
      if (imagePaths.length > 0 && i < imagePaths.length) {
        widgets.add(
            _buildDraggableWidget(i, imageWidth, imagePaths[i])
        );
      } else {
        widgets.add(
            _buildAddImageWidget(i, imageWidth)
        );
      }
    }
    return widgets;
  }

  // 拖拽
  Widget _buildDraggableWidget(int index, double imageWidth, String imagePath) {
    return Draggable(
      data: index,
      child: _buildDragTargetWidget(index, imageWidth, imagePath),
      feedback: _buildImageWidget(index, index == 0 ? imageWidth/4 : imageWidth/2, imagePath),
      childWhenDragging: _moveIndex == index ? Container(width: imageWidth, height: imageWidth,) : null,
      onDragStarted: () {
//        PrintUtil.log({"拖动":"开始"});
        setState(() {
          _moveIndex = index;
        });
      },
      onDragCompleted: () {
//        PrintUtil.log({"拖动":"成功"});
      },
      onDragEnd: (details) {
//        PrintUtil.log({"拖动":"结束 details = $details"});
        setState(() {
          _moveIndex = -1;
        });
      },
      onDraggableCanceled: (velocity, offset) {
//        PrintUtil.log({"拖动":"取消 velocity = $velocity, offset = $offset"});
      },
    );
  }

  // 拖拽事件接收
  Widget _buildDragTargetWidget(int index, double imageWidth, String imagePath) {
    return DragTarget(
      builder: (context, candidateData, rejectedData) {
        return _buildImageWidget(index, imageWidth, imagePath);
      },
      onWillAccept: (int moveIndex) {
//        PrintUtil.log({"拖拽接收":"正在移动 $moveIndex, 经过 $index"});
        _moveIndex = index;
        sortItem(moveIndex, index);
        return true;
      },
      onAccept: (int moveIndex) {
//        PrintUtil.log({"拖拽接收":"正在移动 $moveIndex, 松手 $index"});
        if (widget.onMoveCompleted != null) {
          widget.onMoveCompleted(moveIndex, index);
        }
      },
      onLeave: (int moveIndex) {
        _moveIndex = moveIndex;
        sortItem(index, moveIndex);
//        PrintUtil.log({"拖拽接收":"正在移动 $moveIndex, 经过并已离开 $index"});
      },
    );
  }

  // 图片展示Item
  Widget _buildImageWidget(int index, double imageWidth, String imagePath) {
    return index == _moveIndex ? Container(width: imageWidth, height: imageWidth,) : GestureDetector(
      onTap: () {
//        PrintUtil.log({"点击":"查看图片$index"});
        if (widget.onTap != null) {
          widget.onTap(index);
        }
      },
      onLongPress: () {
//        PrintUtil.log({"长按":"删除图片$index"});
        if (widget.onLongPress != null) {
          widget.onLongPress(index);
        }
      },
      child: Material(
        color: Colors.lightGreen,
        child: Image.network(imagePath, width: imageWidth,
          height: imageWidth,
          fit: BoxFit.contain,),
      ),
    );
  }

  // 添加图片Item
  Widget _buildAddImageWidget(int index, double imageWidth) {
    return GestureDetector(
      onTap: () {
//        PrintUtil.log({"点击":"添加图片$index"});
        if (widget.onAddImageTap != null) {
          widget.onAddImageTap();
        }
      },
      child: Container(
        alignment: Alignment.center,
        color: Colors.lightGreen,
        width: imageWidth,
        height: imageWidth,
        child: Icon(
          Icons.add,
          color: Colors.black54,
          size: imageWidth/2,
        ),
      ),
    );
  }

  // Item拖拽排序
  void sortItem(int moveIndex, int toIndex) {
    setState(() {
      String value = _imagePaths[moveIndex];
      _imagePaths.removeAt(moveIndex);
      _imagePaths.insert(toIndex, value);
    });
  }
}

_FlowDelegate

Flow 通过 FlowDelegate 设置Item的位置和设置 Flow 布局的长宽

class _FlowDelegate extends FlowDelegate {

  _FlowDelegate(
    {
      this.size,
      this.spacing,
      this.margin,
    }
  );

  final double size;
  final double spacing;
  final double margin;

  @override
  void paintChildren(FlowPaintingContext context) {
    for (int i = 0; i < context.childCount; i++) {
      if (i == 0) {
        context.paintChild(i,
          transform: new Matrix4.translationValues(
            margin,
            margin,
            0.0,
          ),
        );
      } else if (i == 1) {
        context.paintChild(i,
          transform: new Matrix4.translationValues(
            context.getChildSize(0).width + margin + spacing,
            margin,
            0.0,
          ),
        );
      } else if (i == 2) {
        context.paintChild(i,
          transform: new Matrix4.translationValues(
            context.getChildSize(0).width + margin + spacing,
            margin + context.getChildSize(1).width + spacing,
            0.0,
          ),
        );
      } else if (i == 3) {
        context.paintChild(i,
          transform: new Matrix4.translationValues(
            margin + context.getChildSize(0).width + spacing,
            margin + context.getChildSize(0).width + spacing,
            0.0,
          ),
        );
      } else if (i == 4) {
        context.paintChild(i,
          transform: new Matrix4.translationValues(
            margin + context.getChildSize(3).width + spacing,
            margin + context.getChildSize(0).width + spacing,
            0.0,
          ),
        );
      } else if (i == 5) {
        context.paintChild(i,
          transform: new Matrix4.translationValues(
            margin,
            margin + context.getChildSize(0).width + spacing,
            0.0,
          ),
        );
      }
    }
  }

  @override
  bool shouldRepaint(FlowDelegate oldDelegate) {
    return oldDelegate != this;
  }

  // 是否需要重新布局
  @override
  bool shouldRelayout(FlowDelegate oldDelegate) {
    return super.shouldRelayout(oldDelegate);
  }

  // 设置Flow的尺寸
  @override
  Size getSize(BoxConstraints constraints) {
    return constraints.constrain(Size(this.size, this.size));
//    Size _preSize = super.getSize(constraints);
//    Size _size = Size(_preSize.width, _preSize.width);
//    return _size;
  }

  // 设置每个child的布局约束条件
  @override
  BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
    return super.getConstraintsForChild(i, constraints);
  }

}

使用

List<String> imagePaths = [
  "https://images.tuyacn.com/smart/rule/cover/starry.png",
  "https://images.tuyacn.com/smart/rule/cover/house5.png",
  "https://images.tuyacn.com/smart/rule/cover/work.png",
  "https://images.tuyacn.com/smart/rule/cover/house6.png",
];

TTAlbumLayout(
    imagePaths,
//  size: 300,
//  margin: 10,
//  spacing: 10,
    onMoveCompleted: (int moveIndex, int toIndex) {
        Toast.text(context, "拖拽排序成功:moveIndex = $moveIndex, toIndex = $toIndex");
        // 注意:数据源在拖拽排序成功之后已经改变,所以这里不需要再手动去修改
//      String value = imagePaths[moveIndex];
//      imagePaths.removeAt(moveIndex);
//      imagePaths.insert(toIndex, value);
     },
     onTap: (int index) {
         Toast.text(context, "点击查看图片$index");
     },
     onLongPress: (int index) {
         Toast.text(context, "长按删除图片$index");
     },
     onAddImageTap: () {
         Toast.text(context, "点击添加图片");
         setState(() {
             imagePaths.add("https://images.tuyacn.com/smart/rule/cover/house.png");
         });
     },
)