-
Flutter 利用OverlayEntry实现Toast(进阶)
-
Flutter Dio二次封装
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 imagePaths;
// 布局长宽
final double size;
// Item与Item之间的间距,横向纵向一样的值
final double _spacing;
// 外边距,Item距离布局四边的距离
final double _margin;
// 拽拽成功回调,返回所拖拽Item拖拽前和拖拽后的索引
final OnMoveCompleted onMoveCompleted;
// 点击回调,返回当前点击Item的索引
final ValueChanged onTap;
// 长按回调,返回当前长按Item的索引
final ValueChanged onLongPress;
// 添加图片回调,无返回值
final OnAddImageTap onAddImageTap;
@override
_TTAlbumLayoutState createState() => _TTAlbumLayoutState();
}
_TTAlbumLayoutState
class _TTAlbumLayoutState extends State {
List 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 _buildItems(List imagePaths, double size, double spacing, double margin) {
List 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 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");
});
},
)