如图所示,要开发一个目标时间组件;
功能要点:
- 弹窗呼出后,要能返显外部选定的初始值;
- 弹窗操作完后,要能把内部选中的值回调给外部;
交互设定:
- 类似密码箱的操作方式,上下滑动选值;
- 点击“取消”,弹窗消失;
- 点击“确认”,选中的值回调给外部;
- 点击其他区域,弹窗消失;
交互细则:
- 需要在滑动结束时自动修正对齐,使选中文字正好垂直居中;
- 修改年或者月时,日必须要被重置到1号,且日期可选项需要重新设置,防止日期错误,如2月31日;
组件切分:
- 灰色弹窗阻断区域,点击此处弹窗消失;
- 黄色回调操作,此处用以触发回调hide和confirm;
- 蓝色滚动单元,此处实现上下滑动和交互细则1,及滑动后将居中的值回调给外部,反显外部设定的选项;
- 绿色业务区域,此处负责处理基本业务:解析外部传入时间并存储到内部,设置滚动单元的可选项(如月份列表、日期列表),交互细则中的2;
一、开发蓝色滚动单元
首先确定交互方案,滑动可以用scroll实现,或者touch+translateY实现;
方案一,scroll 好处是流畅有惯性(ios),缺点是需要考虑节流;定位选中元素会比较难,需要手动监听scroll结束事件、获取scrollTop距离、计算出居中元素;
方案二,用touch+translateY好处是控制简单,但触摸结束后没有惯性(或者人工设置惯性(我没这本事),比较费事);
{{ item }}
// 【基本交互原理】
// _handleTouchStart、_handleTouchMove、_handleTouchEnd 作用
//
// _handleTouchStart 记录初始位置,用于后续参照
// _handleTouchMove 更具参照初始位置,计算滚动距离,并设置到视图
// _handleTouchEnd 交互结束后,需要对滚动距离做一次矫正,防止选中的值未对齐视窗
Ps: 布局如上,“picker-shadow” 元素高度设置为两个选项的高度,用于第一个选项和最后一个选中居中用;
Js交互逻辑
name: "scrollPicker",
props: {
options: { type: Array, default: () => [] },
content: { type: Number, default: 0 },
},
data() {
return {
remPxScale: 16,
moveScale: 10,
activeIndex: null,
startY: 0,
};
},
因为用的是vue开发,所以不熟悉的可以先看下vue入门教程;
remPxScale用以表示压缩比,我用的rem作为基本单位;
moveScale用来设定滑动放大,增加滑动速度,即我喜欢手指滑动一个像素就能滚动5个像素那么长;
activeIndex 当前选中的选项索引;
startY初始滑动位置,用在_handleTouchStart中记录;
_handleTouchMove(e) {
const currentY = this._getYPosition(e);
const nextPostion = this._calYPosition(currentY - this.startY);
this._setYPosition(nextPostion);
},
以上代码,当滑动手指时,计算滑动的距离,并计算出需要滚动的距离,即手指一动了n个像素,计算出滚动单元需要translateY多少,然后这设置到ref="scroll-container” 上;
其中核心部分为_calYPosition的计算,大家可以自己实现,我的实现如下,代码略硬各位一笑了之即可:
/**
* 计算当前需要translateY到哪个位置,单位rem
* @param deltaY NUmber 手指划过了多少像素px
* @param appendOrignal boolean 是否需要叠加处理位置 【废弃】
*
* @variation deltaRem number 手指滑动距离换算 单位rem
* @variation orignalRem number 初始translate 单位rem
* @variation maxPosition number,最大滚动距离,即不能滚动到第一个元素更上面
* @variation minPosition number,最大滚动距离,即不能滚动到最后一个元素更下面
*/
_calYPosition(deltaY, appendOrignal = true) {
const deltaRem = deltaY / (this.remPxScale * this.moveScale);
let orignalRem = 0;
if (appendOrignal) {
try {
const transformStr = this.$refs["scroll-container"].style.transform;
const moveRexg = /translateY\((-?\d*\.?\d*)rem\)$/;
orignalRem = transformStr.match(moveRexg)[1] || 0;
} catch (error) {
orignalRem = 0;
}
}
const finalPostion = Number(deltaRem) + Number(orignalRem);
const maxPosition = 0;
const minPosition = -(this.options.length - 1) * 4;
if (finalPostion < minPosition) {
return minPosition;
} else if (finalPostion > maxPosition) {
return maxPosition;
}
return finalPostion;
},
最后,滚动结束需要拉正对其,并把选中的值(即滚动到c位的值回调出去,用于下级滚动单元使用);
_handleTouchEnd(e) {
const finalY = this._getYPosition(e);
const nextPostion = this._calYPosition(finalY - this.startY);
const index = Math.round(-nextPostion / 4);
this.scrollToIndex(index);
},
…
/**
* 最终的定值函数
* @param index Number 指哪打哪,滚动到第几个选项
* @param shouldEmit boolean 是否需要触发外部回调
* 当为内部点击或者滚动时,需要回调外部,保持安全,比如选择月份后,日期需要重置,防止移除,比如2月31日
* 当为外部设置时,不需要回调外部,因为外部已经是正确的值了,而且下一级的值,外部已经处理好;
*/
scrollToIndex(index, shouldEmit = true) {
this._setYPosition(-index * 4);
this.activeIndex = index;
shouldEmit && this.$emit("pickSelect", this.options[index]);
},
二、开发绿色业务区域
绿色区域的初始值设定和回调都很简单不再赘述,讲一下如何联动各个滚动单元
在页面中,滚动单元使用如下
handleSelect('year', any)"
>
handleSelect('month', any)"
>
handleSelect('date', any)"
>
其中
options表示滚动单元的选项,比如月份为1-12等;
content表示当前选中的年月日,比如year我选中为2020;
@pickSelect表示滚动单元内部选中值后,回调到业务区域,比如我选中了2月,那么必须把2月回调给业务区域,业务区域从新计算二月有哪些日子,并强制设定日期为1号;
看下handleSelect的实现;
handleSelect(type, value) {
switch (type) {
case "year":
this.year = value;
this.month = 1;
this.date = 1;
break;
case "month":
this.month = value;
this.dates = TimeUtils.createDates(value);
this.date = 1;
break;
case "date":
this.date = value;
break;
case "hour":
this.hour = value;
break;
case "minute":
this.minute = value;
break;
}
}
如上,基本的几个日期间的约束都在代码中;
最后直接把业务组件中的year、month、date组装下丢给confirm回调就行,如果需要展示时和分,如法炮制即可;