先不卖关子,上动图
三大功能
- 生成时间块
- 对整块时间块进行平移
- 通过上下拉动把手增减时间
布局
- 整个布局分红、绿、蓝三块。
- 红色部分并不重要,这边用
position: sticky去实现,当然要注意一下浏览器兼容性。 - 绿色部分用了flex分成左右两块,本人按照半小时为一格,将一天画成48格。然后总体内用
overflow: auto去实现整体上下滚动。 - 蓝色部分是整体的
position: absolute去计算距离顶部的高度和时间块的高度,宽度固定。 - 那么布局就完事了。
js基础
在实现代码之前,首先要了解移动端的触摸事件。
- touchstart
- touchmove
- touchend
这部分需要自行恶补,可以参照鼠标额mousedown、mousemove、mouseup,但还是有区别的。
mousedown、mousemove、mouseup中默认的
$event对象是鼠标当前指向的对象,而移动端的touch事件默认的$event对象永远都是touchstart指向的对象。
生成时间块
这里参照了vue-fullcalendar的操作。长按点击开始touchstart,通过触摸滑动的Y轴距离算出当前时间块的高度和距离顶部的高度。
<div v-for="item in gridArr" :key="item" :ref="(el) => setItemRef(el, item)" @touchstart="gridTouchStart(item, $event)"></div>
// 半小时一块 一天划分为48块
const config = { length: 48 };
// 生成右侧块状
const gridArr = ref<number[]>([]);
for (let i = 0; i < config.length; i++) {
gridArr.value.push(i);
}
const timeRef = ref<{ [key: number]: any }>({});
// 为每个块状添加ref响应式
const setItemRef = (el: any, item: number): void => {
if (el) {
timeRef.value[item] = el;
}
};
// generate grid
const startIdx = ref<number>(-1);
const endIdx = ref<number>(-1);
// 记录当前是否在滑动中的参数
const isMove = ref<boolean>(false);
const gridTouchStart = (item: number, e: any) => {
// 禁止双指点击
if (e.touches && e.touches.length > 1) {
return;
}
console.log('start');
let startMoveY: number;
// 写了一个类防抖,只有长按才能生成时间块
let loop: NodeJS.Timeout | undefined;
let sliderLoop: NodeJS.Timeout | undefined;
// 将当前触摸的块变成想要的颜色
timeRef.value[item].style.backgroundColor = '#C7E0F4';
// 记录当前时间块距离顶部的高度
startMoveY = e.touches[0].clientY;
// 长按0.2s+触摸响应时间后,将背景色清空,生成时间块(为了区别用户是滑动外部滚动还是想要生成时间块)
loop = setTimeout(() => {
console.log('slider start');
isMove.value = true;
timeRef.value[item].style.backgroundColor = 'white';
startIdx.value = item;
endIdx.value = item;
}, 200);
const gridTouchMove = (e: any) => {
// 如果还在长按周期清楚loop
if (!isMove.value) {
clearTimeout(loop);
loop = undefined;
} else {
// 禁止下滑时间冒泡到外部,影响到外部滚动条滚动
e.preventDefault();
if (sliderLoop) {
clearInterval(sliderLoop);
sliderLoop = undefined;
}
// 获取外部滚动条
const scrollObject = document.querySelector('div[data-is-scrollable]');
// 获取红框部分高度
const calendarRects = document.getElementsByClassName('campscomp-header-calendar')[0].getBoundingClientRect();
// 获取当前滚动部分的高度
const bodyHeight = document.getElementsByClassName('campslayout-cotent-bottom')[0].getBoundingClientRect().top - calendarRects.top - calendarRects.height;
// 接下来是判断鼠标是否在上下端,如果在上端就滚动条往上移,下端也一样。
if (e.targetTouches[0].clientY - calendarRects.top - calendarRects.height < 44 && scrollObject && scrollObject.scrollTop > 0) {
sliderLoop = setInterval(() => {
scrollObject &&
scrollObject.scrollTo({
top: scrollObject.scrollTop - 1,
behavior: 'smooth'
});
startMoveY += 1;
}, 10);
} else if (e.targetTouches[0].clientY > bodyHeight + calendarRects.top - 44 && scrollObject && scrollObject.scrollTop < bodyHeight) {
sliderLoop = setInterval(() => {
scrollObject &&
scrollObject.scrollTo({
top: scrollObject.scrollTop + 1,
behavior: 'smooth'
});
startMoveY -= 1;
}, 10);
}
// 判断当前离顶端高度,减去之前初始离顶端的高度获取到差值,除以一格的高度,得出当前移动了几格
const newIdx = item + Math.floor((e.touches[0].clientY - startMoveY) / 22);
// 判断向上还是向下移动
if (Math.floor((e.touches[0].clientY - startMoveY) / 22) < 0) {
startIdx.value = item > 0 ? newIdx : 0;
endIdx.value = item;
} else {
startIdx.value = item;
endIdx.value = item < 47 ? newIdx : 0;
}
}
};
const gridTouchEnd = () => {
console.log('end');
if (loop) {
clearTimeout(loop);
loop = undefined;
clearInterval(sliderLoop);
sliderLoop = undefined;
}
isMove.value = false;
// 将背景颜色置白
timeRef.value[item].style.backgroundColor = 'white';
const result = duplicateTime();
if (!result) {
startIdx.value = -1;
endIdx.value = -1;
} else {
// cloneSelectId.value = selectId.value
}
// 结束时注销事件
document.removeEventListener('touchmove', gridTouchMove);
document.removeEventListener('touchend', gridTouchEnd);
};
// 注册touchmove事件
document.addEventListener('touchend', gridTouchEnd);
// 禁止下滑时间冒泡到外部,影响到外部滚动条滚动
document.addEventListener('touchmove', gridTouchMove, { passive: false });
};
对整块时间块进行平移
其中sliderStyle为计算属性,通过上面定义的startIdx和endIdx的值来计算当前时间块出现的位置。
其中的平移也是相同的计算方法,保存初始值,在move的时候计算距离来进行滑动。
<!-- template -->
<div v-if="isSliderShow" :style="sliderStyle" @touchstart="rectTouchStart($event)">
<!-- 自定义内容 -->
</div>
// 设置一个参数 判断是否正在滑动
const isSlider = ref<boolean>(false);
const sliderTopBtn = ref();
const sliderBottomBtn = ref();
const rectTouchStart = (e: any) => {
// 判断当前点击事件是否包括把手(注意新版本Chrome已经不兼容path了,可以用composedPath代替)
if (e?.path.indexOf(sliderTopBtn.value) > -1 || e?.path.indexOf(sliderBottomBtn.value) > -1) {
return;
}
// 禁止双指点击
if (e.touches && e.touches.length > 1) {
return;
}
// 写了一个类防抖,只有长按才能生成时间块
let loop: NodeJS.Timeout | undefined;
let startPageY = 0;
loop = setTimeout(() => {
loop = undefined;
isSlider.value = true;
}, 200);
// 获取当前点击下的pageY,也就是离页面顶部的距离
startPageY = e.touches[0].pageY;
const moveStartIdx = startIdx.value;
const moveEndIdx = endIdx.value;
let sliderLoop: NodeJS.Timeout | undefined;
const rectTouchMove = (e: any) => {
if (!isSlider.value) {
clearTimeout(loop);
loop = undefined;
} else {
// 禁止下滑时间冒泡到外部,影响到外部滚动条滚动
e.preventDefault();
if (sliderLoop) {
clearInterval(sliderLoop);
sliderLoop = undefined;
}
// 获取外部滚动条 同上
const scrollObject = document.querySelector('div[data-is-scrollable]');
const calendarRects = document.getElementsByClassName('campscomp-header-calendar')[0].getBoundingClientRect();
const bodyHeight = document.getElementsByClassName('campslayout-cotent-bottom')[0].getBoundingClientRect().top - calendarRects.top - calendarRects.height;
if (e.targetTouches[0].clientY - calendarRects.top - calendarRects.height < 44) {
sliderLoop = setInterval(() => {
scrollObject &&
scrollObject.scrollTo({
top: scrollObject.scrollTop - 1,
behavior: 'smooth'
});
startPageY += 1;
}, 10);
} else if (e.targetTouches[0].clientY > bodyHeight + calendarRects.top - 44) {
sliderLoop = setInterval(() => {
scrollObject &&
scrollObject.scrollTo({
top: scrollObject.scrollTop + 1,
behavior: 'smooth'
});
startPageY -= 1;
}, 10);
}
const endPageY = e.targetTouches[0].pageY;
const moveIndex = Math.round((endPageY - startPageY) / 22);
如果移动超过第一格或者大于最后一格就返回
if (moveStartIdx + moveIndex < 0 || moveEndIdx + moveIndex > 47) {
return;
}
startIdx.value = moveStartIdx + moveIndex;
endIdx.value = moveEndIdx + moveIndex;
}
};
const rectTouchEnd = () => {
isSlider.value = false;
clearTimeout(loop);
clearInterval(sliderLoop);
loop = undefined;
sliderLoop = undefined;
document.removeEventListener('touchmove', rectTouchMove);
document.removeEventListener('touchend', rectTouchEnd);
};
// 禁止下滑时间冒泡到外部,影响到外部滚动条滚动
document.addEventListener('touchmove', rectTouchMove, { passive: false });
document.addEventListener('touchend', rectTouchEnd);
};
const sliderStyle = computed(() => {
const res: StyleValue = {
top: '0',
height: '0'
// opacity: (isMove.value || isSlider.value || isMoveSlider.value) ? 0.88 : 1
};
const startRef = timeRef.value[startIdx.value];
const endRef = timeRef.value[endIdx.value];
res.top = startRef.offsetTop + 'px';
res.height = Math.abs(startRef.offsetTop - endRef.offsetTop) + startRef.clientHeight + 'px';
return res;
});
通过上下拉动把手增减时间块高度
在上面的基础上先画两个把手,通过position: absolute居中吸附到顶部和尾部,然后这里写了两个div是为了增加触摸面积,可以将父级的长宽设置的多一点。原理和上一样,我就不一一介绍了。
<div v-if="isSliderShow" :style="sliderStyle" @touchstart="rectTouchStart($event)">
<!-- 增加滑块面积 -->
<div ref="sliderTopBtn" @touchstart="handleSliderBtn($event, 'top')">
<div class="campscomp-slider-btn"></div>
</div>
<div ref="sliderBottomBtn" @touchstart="handleSliderBtn($event, 'bottom')">
<div class="campscomp-slider-btn"></div>
</div>
</div>
// Handle Slider Start
const handleSliderBtn = (e: any, direction: 'top' | 'bottom') => {
if (e.touches && e.touches.length > 1) {
return;
}
let silderStartX: number = e.touches[0].clientY;
// deep clone 深拷贝一份数据
const startIndex = deepClone(startIdx.value);
const endIndex = deepClone(endIdx.value);
let sliderLoop: NodeJS.Timeout | undefined;
const mouseover = (el: any) => {
el.preventDefault();
if (sliderLoop) {
clearInterval(sliderLoop);
sliderLoop = undefined;
}
const scrollObject = document.querySelector('div[data-is-scrollable]');
const calendarRects = document.getElementsByClassName('campscomp-header-calendar')[0].getBoundingClientRect();
const bodyHeight = document.getElementsByClassName('campslayout-cotent-bottom')[0].getBoundingClientRect().top - calendarRects.top - calendarRects.height;
if (el.targetTouches[0].clientY - calendarRects.top - calendarRects.height < 44 && scrollObject && scrollObject.scrollTop > 0) {
sliderLoop = setInterval(() => {
scrollObject &&
scrollObject.scrollTo({
top: scrollObject.scrollTop - 1,
behavior: 'smooth'
});
silderStartX += 1;
}, 10);
} else if (el.targetTouches[0].clientY > bodyHeight + calendarRects.top - 44 && scrollObject && scrollObject.scrollTop < bodyHeight) {
sliderLoop = setInterval(() => {
scrollObject &&
scrollObject.scrollTo({
top: scrollObject.scrollTop + 1,
behavior: 'smooth'
});
silderStartX -= 1;
}, 10);
}
const clientX = el.touches[0].clientY;
if (direction === 'top') {
const resultIndx = startIndex + Math.round((clientX - silderStartX) / 22);
if (resultIndx <= endIdx.value && resultIndx >= 0 && resultIndx <= config.length! - 1) {
startIdx.value = resultIndx;
}
} else {
const resultIndx = endIndex + Math.round((clientX - silderStartX) / 22);
if (resultIndx >= startIdx.value && resultIndx >= 0 && resultIndx <= config.length! - 1) {
endIdx.value = resultIndx;
}
}
};
const mouseup = (): void => {
isSlider.value = false;
clearInterval(sliderLoop);
sliderLoop = undefined;
duplicateTime(startIndex, endIndex);
document.removeEventListener('touchmove', mouseover);
document.removeEventListener('touchend', mouseup);
};
// Disable outer border scrolling => el.preventDefault();
document.addEventListener('touchmove', mouseover, { passive: false });
document.addEventListener('touchend', mouseup);
};
将公共部分抽离一下,再润色一下页面就完事了。 Pc端会更加简单一些。
最后
好久没写文章了,最近逛了沸点,发现有兄弟在问移动端该怎么实现,让我新增了积极性,也希望我写的代码能被看懂吧。毕业一年小菜鸡的代码,属实又臭又长。