yunUI组件库解析:图片上传与排序组件yImgPro
Posted 恪愚
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了yunUI组件库解析:图片上传与排序组件yImgPro相关的知识,希望对你有一定的参考价值。
yunUI是笔者开源的微信小程序功能库。目前其中包含了一些复杂的功能组件。方便使用。未来它将分为组件、样式、js三者合为一体,但分别提供。
本文所用代码皆来源于组件库中的yImgPro组件。详细代码可至github查看。地址: yunUI 。
组件库已经发到npm上了!地址:yun-ui-micro
欢迎大家点 star !
最近有想法对组件库按照新思路进行重构,各位有什么急切需要或常见使用的组件也欢迎提出!一起共建!
场景如下:
首先分析此需求。有两点:
- 拖动时排序
- 拖动后排序
单从性能上看,第二个是有优势的。但是从用户体验上看,无疑要选择第一种方案。
除非你的需求是“不能拖动排序”。你可以放心的选择第二种方案。第二种方案在笔者的功能库中也有组件:yImg。本文思路是第一种。
区别于之前写的第二种思路的文章,第一种思路对布局和样式影响很重的点在于:拖动。
拖动时排序意味着这个元素被拖动时不能在原来位置上有“保留”。这很关键,因为我们可以利用“保留”点对第二种方案进行改造使之像第一种效果看齐,但体验上仍有差距。
所以,笔者选择了“定位”position
。所有的元素都是定位的,这样拖动时只需要更改transform
和z-index
即可达到效果。记住这一点,这会带来bug,虽然很好解决。
我的wxml决定这样写:
<view class="container">
<view class="item-wrap" style="height: itemWrapHeight px;">
<view
class="item cur == index? 'cur':'' curZ == index? 'zIndex':'' itemTransition ? 'itemTransition':''"
wx:for="list" wx:key="index" id="itemindex" data-key="item.key" data-index="index"
style="transform: translate3d(index === cur ? tranX : item.tranXpx, index === cur ? tranY: item.tranYpx, 0px);"
bind:longpress="longPress" catch:touchmove="touchMove" catch:touchend="touchEnd">
<view class="info" style="width: imgShape.siderpx; height: imgShape.siderpx; padding: 0 imgShape.pdrpx imgShape.pdrpx 0;">
<image mode="aspectFill" src="item.data"></image>
<i class="iconfont icon-delete" wx:if="showMenuImg" bind:tap="onDelImage" data-index="item.key"></i>
</view>
</view>
<view class="item-sel selectphoto"
style="transform: translate3d(selSite.tranXpx, selSite.tranYpx, 0px); width: imgShape.side - imgShape.pdrpx; height: imgShape.side - imgShape.pdrpx;" hidden="!canSelPhoto" bind:tap="onChooseImage">
<i class="iconfont icon-jiashang"></i>
</view>
</view>
</view>
这个组件被变量控制的样式比yImg组件多了许多,但结构上精简了不少。我们来分析下:
首先,因为定位,子元素脱离文档流,所以我们需要动态为item-wrap
元素赋height
。
然后是子元素的宽高和padding也是动态的,没事,我们会在初始化时动态的获取它 —— 顺便添加一些我们需要的数据。
初始化发生在用户选择完图片以后。这个函数中干了几件事:
- 初始化数据。将
数组-字符串
变成数组-对象
,存储初始顺序、未来可能的移动距离、图片本身 - 获取子项
item
的宽高(为偏移做准备)、计算当前整个区域高度(所以每次选择完图片都要调用此函数) - 获取图片区域
item-wrap
的初始信息(位置!)
init()
// 遍历数据源增加扩展项, 以用作排序使用
let list = this.data.listData.map((item, index) =>
let data =
key: index,
tranX: 0,
tranY: 0,
data: item
return data
);
this.setData(
list: list,
itemTransition: false
);
// 获取每一项的宽高等属性
this.createSelectorQuery().select(".item").boundingClientRect((res) =>
let rows;
let len = this.data.list.length;
if(len == MAX_IMG_NUM)
rows = Math.ceil(len / this.data.columns);
else
rows = Math.ceil((len + 1) / this.data.columns);
this.item = res;
let itemWrapHeight = rows * res.height;
this.getPosition(this.data.list, false);
let obj = list[list.length - 1]
let tranX = res.width * ((obj.key + 1) % this.data.columns);
let tranY = Math.floor((obj.key + 1) / this.data.columns) * res.height;
this.setData(
itemWrapHeight,
selSite:
tranX,
tranY
)
let query = wx.createSelectorQuery().in(this);
query.select('.item-wrap').boundingClientRect((res) =>
this.itemWrap = res;
)
// 需要的是“距离文档流顶部的距离”。所以咱们需要这片区域已经在页面上滚动了多少了,把这个值加上
if(this.properties.scrollOffset)
this.itemWrap.top += this.properties.scrollOffset;
else
query.selectViewport().scrollOffset((res) =>
let _wrap = this.itemWrap.top + res.scrollTop;
this.itemWrap.top = _wrap;
)
query.exec()
).exec();
,
这里尤其需要注意的是:获取图片区域信息时用的 API 只能获取“当前元素距离屏幕顶部的距离”。而实际大多数情况我们需要的是“当前元素距离文档流顶部的距离”。这两者在一个非常重要的场景下会有大幅偏差 —— 当文档流发生滚动时!
所以笔者采用selectViewport().scrollOffset
API 来获得文档流的滚动偏差。并在后面长按甚至拖动过程中将这个偏差“抹去”。
如果你在使用笔者的组件库,并且遇到了“当前组件并不是一开始就在页面上出现而是动态展示”的场景,那么您也可以通过参数将这个“偏差”传入组件。这一点在README的使用说明中也有说明。
因为图片是 position
的,所以哪怕在初始时他们的位置也是计算得到的 —— getPosition
函数。我们根据列数获取每个元素的偏移距离,响应式到他们的transform
上:
getPosition(data, vibrate = true)
let list = data.map((item, index) =>
item.tranX = this.item.width * (item.key % this.data.columns);
item.tranY = Math.floor(item.key / this.data.columns) * this.item.height;
return item
);
this.setData(
list: list
);
if (!vibrate) return;
let listData = [];
list.forEach((item) =>
listData[item.key] = item.data
);
this.setData(
listData,
itemTransition: true
)
,
接下来就是长按事件了。
在本组件中,少于两张图片则长按只有删除功能,一定程度上减少性能消耗。
长按时,我们需要拿到当前元素的位置,并且和整体区域位置结合获取“中心点”,并将中心点移动到点击位置处。这也就是我们说的“是否跟手”。并且这样图片的偏移也能提醒用户当前点的是这张图片:
longPress(e)
if(this.data.list.length < 2)
this.setData(
showMenuImg: true
);
wx.vibrateShort();
return;
this.setData(
touch: true
);
this.startX = e.changedTouches[0].pageX
this.startY = e.changedTouches[0].pageY
let index = e.currentTarget.dataset.index;
this.tranX = this.startX - this.item.width / 2 - this.itemWrap.left;
this.tranY = this.startY - this.item.height / 2 - this.itemWrap.top;
this.setData(
cur: index,
curZ: index,
tranX: this.tranX,
tranY: this.tranY,
showMenuImg: true
);
wx.vibrateShort();
,
长安之后是拖动。这是我们的核心事件。因为拖动排序,所以我们不仅需要计算当前元素的偏移,还需要计算元素偏移后和“路过元素”的位置关系 —— 临界点判断。
touchMove(e)
if (!this.data.touch) return;
let tranX = e.touches[0].pageX - this.startX + this.tranX,
tranY = e.touches[0].pageY - this.startY + this.tranY;
this.setData(
tranX: tranX,
tranY: tranY,
showMenuImg: false
);
let originKey = e.currentTarget.dataset.key;
let endKey = this.calculateMoving(tranX, tranY);
// 防止拖拽过程中发生乱序问题
if (originKey == endKey || this.originKey == originKey) return;
this.originKey = originKey;
this.insert(originKey, endKey);
,
calculateMoving
函数就是做这个的:
calculateMoving(tranX, tranY)
let rows = Math.ceil(this.data.list.length / this.data.columns) - 1,
i = Math.round(tranX / this.item.width),
j = Math.round(tranY / this.item.height);
i = i > (this.data.columns - 1) ? (this.data.columns - 1) : i;
i = i < 0 ? 0 : i;
j = j < 0 ? 0 : j;
j = j > rows ? rows : j;
let endKey = i + this.data.columns * j;
endKey = endKey >= this.data.list.length ? this.data.list.length - 1 : endKey;
return endKey
,
在拖动过程中,每次知道要偏移到哪,也就是抢占哪个元素的位置后,需要根据拖动元素的key 和 “目标元素”的 key 去重新计算每一项的新的key:
insert(origin, end)
let list;
if (origin < end)
list = this.data.list.map((item) =>
if (item.key > origin && item.key <= end)
item.key = item.key - 1;
else if (item.key == origin)
item.key = end;
return item
);
this.getPosition(list);
else if (origin > end)
list = this.data.list.map((item) =>
if (item.key >= end && item.key < origin)
item.key = item.key + 1;
else if (item.key == origin)
item.key = end;
return item
);
this.getPosition(list);
,
最后在“松手”时要去把所有在拖动过程中发生变化的变量给恢复初始值:
touchEnd()
if (!this.data.touch)
return;
else
this.setData(
showMenuImg: true
)
this.triggerMsg(this.data.listData, "sort-img")
this.clearData();
,
clearData()
this.originKey = -1;
this.setData(
touch: false,
cur: -1,
tranX: 0,
tranY: 0
);
// 延迟清空
setTimeout(() =>
this.setData(
curZ: -1,
)
, 300)
,
除此之外,还有删除事件。
删除也有两种方案:
-
硬删除。删除指定元素后将数组重新初始化init
- 有过渡效果的删除。从前到后计算删除元素位置,再从后到前将后一个元素的
data
赋值给前一个元素。但是其余key、tranX、tranY不变。最后len - 1
第二种方式相当于自己重新算了一遍。从一个地方可以看出两者的区别:需不需要自己计算“上传图片按钮的位置”!在笔者的组件中,也提供了参数可以选择使用哪种方式删除。
nDelImage(event)
//...
this.aniDelItem(cacheList, event, listData, list)
,
2023/2/18更新
第一种方式好像其实没必要存在,还增加代码量。应该没人对删除动画“反感”吧。(其实是第二种方式终于被我优化好了,之前一直被一个点迷住了突然豁然开朗)
- 考虑到一些因素,我们先将数组
sort
一下,找到要删除的元素,从后往前遍历将后一个元素的data
和key
替换掉前一个元素的 - 考虑到性能,用对象以
key-x轴位移, y轴位移
缓存关键信息 - 遍历源数组,一一对应缓存,用一个新数组接收缓存信息和源数组剩余信息的对象。这就是新的数组了(如果没有最后两步而是直接赋值,含义就变成了“源数组所有数据先
sort
再删除”,再加上图片元素是靠位移信息排列的,显示上无论在删除元素前还是后的元素都会有一个动画效果。这就偏离了本意)
aniDelItem(cacheList, event, listData, sourceList=[])
let cacheNow = -1;
// 缓存
let _item =
key: -1,
data: ""
let cacheMoka =
cacheList.sort(this.sortBy("key"));
// 正向遍历,获取关键节点
for(以上是关于yunUI组件库解析:图片上传与排序组件yImgPro的主要内容,如果未能解决你的问题,请参考以下文章