浏览代码

feat(picker): 惯性滚动与性能优化 (#1453)

* fix: marge主分支 (#1)

* doc: calendar 文档修改 (#1410)

* feat: 添加range组件、calendar组件在线文档

* fix: 文档调整

* fix: 重构calendar组件

* feat: 日历组件重构,文档修改,功能完善

* fix: 格式化

* fix: 代码格式化调整。

* fix: 去除无用代码

* fix: 文档调整

* fix:  文档调整

* fix: taro  demo 样式修改

* feat: range组件功能完善,新增 竖向操作,刻度展示。

* fix: 冲突解决

* feat: taro功能新增,兼容处理,文档修改

* feat: 添加range组件,jdt主题色

* fix: 修改组件初始化逻辑

* feat: 新增h5 日期多选功能

* feat: taro版本添加 日期多选功能

* fix: 修复多选,无法选中开头结尾日期问题

* fix: 文档修改,添加en-US 文档

* fix: 文档完善

Co-authored-by: lkjh3214 <13121007159@163.com>
Co-authored-by: love_forever <1039168735@qq.com>

* feat: imagepreview 部分功能补齐 (#1412)

* feat: image新增单元测试

* feat: ellipsis添加单元测试

* feat: imagepreview 添加

* fix: popop单元测试修改

* docs: 添加版本号

* feat: support highlight for JetBrains web-types

* test(imagepreview): edit snap

* fix(image): dts edit import

* docs(input): demo和md国际化文案修改 (#1414)

* fix: 抽离 input  ConfirmTextType

* feat: input、switch国际化

* feat: category、address国际化

* feat: taro升级maxlength问题

* fix: 国际化增加默认字段

* fix: blank

* fix: input组件国际化文案修改

* style: add ellipsis add sass

* docs(elevator): 增加吸顶props

* feat: input组件新增input slot插槽 (#1418)

* fix: 抽离 input  ConfirmTextType

* feat: input、switch国际化

* feat: category、address国际化

* feat: taro升级maxlength问题

* fix: 国际化增加默认字段

* fix: blank

* fix: input组件国际化文案修改

* feat: input组件新增input slot插槽

* release: v3.1.22

* Update README.md

add alipay img

* Update README.md

* docs: changelog 3.1.22

Co-authored-by: lkjh3214 <305624531@qq.com>
Co-authored-by: lkjh3214 <13121007159@163.com>
Co-authored-by: love_forever <1039168735@qq.com>
Co-authored-by: richard1015 <51844712@qq.com>
Co-authored-by: ailululu <912429321@qq.com>
Co-authored-by: snandy <zhouyrt@gmail.com>

* feat: imagepreview重复问题修改

* fix: picker 组件重影问题修改

* fix: 科技样式同步

* feat: picker组件惯性滚动优化

* feat: picker组件惯性滚动优化

Co-authored-by: lkjh3214 <305624531@qq.com>
Co-authored-by: lkjh3214 <13121007159@163.com>
Co-authored-by: love_forever <1039168735@qq.com>
Co-authored-by: richard1015 <51844712@qq.com>
Co-authored-by: ailululu <912429321@qq.com>
Co-authored-by: snandy <zhouyrt@gmail.com>
yangxiaolu1993 3 年之前
父节点
当前提交
c2029dc2ef

+ 101 - 62
src/packages/__VUE/picker/Column.vue

@@ -1,6 +1,6 @@
 <template>
   <view class="nut-picker__list" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
-    <view class="nut-picker-roller" ref="roller" :style="touchRollerStyle">
+    <view class="nut-picker-roller" ref="roller" :style="touchRollerStyle" @transitionend="stopMomentum">
       <template v-for="(item, index) in column" :key="item.value ? item.value : index">
         <view
           class="nut-picker-roller-item"
@@ -14,23 +14,16 @@
     </view>
     <view class="nut-picker-roller-mask"></view>
 
-    <!-- <view class="nut-picker-content">
-      <view class="nut-picker-list-panel" ref="list" :style="touchListStyle">
-        <view
-          :class="['nut-picker-item', 'nut-picker-item-ref', item.className]"
-          v-for="(item, index) in column"
-          :key="item.value ? item.value : index"
-          >{{ item.text }}
-        </view>
-        <view class="nut-picker-placeholder" v-if="column && column.length === 1"></view>
-      </view>
-    </view> -->
+    <view class="nut-picker-content"
+      ><view class="nut-picker-list-panel" ref="list" :style="touchListStyle"></view
+    ></view>
   </view>
 </template>
 <script lang="ts">
 import { reactive, ref, watch, computed, toRefs, onMounted, PropType } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 import { PickerColumnOption, PickerOption, TouchParams } from './types';
+import { useTouch } from '@/packages/utils/useTouch';
 const { create } = createComponent('picker-column');
 
 export default create({
@@ -54,6 +47,8 @@ export default create({
 
   emits: ['click', 'change'],
   setup(props, { emit }) {
+    const touch: any = useTouch();
+
     const wrapper = ref();
     const state = reactive({
       touchParams: {
@@ -64,9 +59,10 @@ export default create({
         lastY: 0,
         lastTime: 0
       },
+
       currIndex: 1,
       transformY: 0,
-      scrollDistance: 0,
+      scrollDistance: 0, // 滚动的距离
       lineSpacing: 36,
       rotation: 20,
       timer: null
@@ -76,62 +72,96 @@ export default create({
     const list = ref(null);
     const listItem = ref(null);
 
+    const moving = ref(false); // 是否处于滚动中
     const touchDeg = ref(0);
     const touchTime = ref(0);
-    const touchTranslateY = ref(0);
-    const touchListStyle = computed(() => {
+
+    // 触发惯性滑动条件:
+    // 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_TIME` 且 move
+    // 距离大于 `MOMENTUM_DISTANCE` 时,执行惯性滑动
+    const INERTIA_TIME = 300;
+    const INERTIA_DISTANCE = 15;
+
+    const touchRollerStyle = computed(() => {
       return {
-        transition: `transform ${touchTime.value}ms cubic-bezier(0.19, 1, 0.22, 1)`,
-        transform: `translate3d(0, ${state.scrollDistance}px, 0)`
+        transition: `transform ${touchTime.value}ms cubic-bezier(0.17, 0.89, 0.45, 1)`,
+        transform: `rotate3d(1, 0, 0, ${touchDeg.value})`
       };
     });
 
-    const touchRollerStyle = computed(() => {
+    const touchListStyle = computed(() => {
       return {
-        transition: `transform ${touchTime.value}ms cubic-bezier(0.19, 1, 0.22, 1)`,
-        transform: `rotate3d(1, 0, 0, ${touchDeg.value})`
+        transition: `transform ${touchTime.value}ms cubic-bezier(0.17, 0.89, 0.45, 1)`,
+        transform: `translate3d(0, ${state.scrollDistance}px, 0)`
       };
     });
 
+    const setRollerStyle = (index: number) => {
+      return `transform: rotate3d(1, 0, 0, ${-state.rotation * index}deg) translate3d(0px, 0px, 104px)`;
+    };
+
     const onTouchStart = (event: TouchEvent) => {
-      event.preventDefault();
-      let changedTouches = event.changedTouches[0];
-      state.touchParams.startY = changedTouches.pageY;
-      state.touchParams.startTime = event.timeStamp || Date.now();
+      touch.start(event);
+
+      if (moving.value) {
+        const { transform } = window.getComputedStyle(list.value as any);
+        state.scrollDistance = +transform.slice(7, transform.length - 1).split(', ')[5];
+      }
+
+      state.touchParams.startY = touch.deltaY.value;
+      state.touchParams.startTime = Date.now();
       state.transformY = state.scrollDistance;
     };
 
     const onTouchMove = (event: TouchEvent) => {
-      event.preventDefault();
-      let changedTouches = event.changedTouches[0];
-      (state.touchParams as TouchParams).lastY = changedTouches.pageY;
-      (state.touchParams as TouchParams).lastTime = event.timeStamp || Date.now();
+      touch.move(event);
+
+      if ((touch as any).isVertical) {
+        moving.value = true;
+        preventDefault(event, true);
+      }
+
+      (state.touchParams as TouchParams).lastY = touch.deltaY.value;
+      const now = Date.now();
       let move = state.touchParams.lastY - state.touchParams.startY;
 
       setMove(move);
+
+      if (now - (state.touchParams as TouchParams).startTime > INERTIA_TIME) {
+        (state.touchParams as TouchParams).startTime = now;
+        state.touchParams.startY = (state.touchParams as TouchParams).lastY;
+      }
     };
 
     const onTouchEnd = (event: TouchEvent) => {
-      event.preventDefault();
-
-      let changedTouches = event.changedTouches[0];
-      state.touchParams.lastY = changedTouches.pageY;
-      state.touchParams.lastTime = event.timeStamp || Date.now();
+      state.touchParams.lastY = touch.deltaY.value;
+      state.touchParams.lastTime = Date.now();
       let move = state.touchParams.lastY - state.touchParams.startY;
 
       let moveTime = state.touchParams.lastTime - state.touchParams.startTime;
-      console.log('touchEnd', move, moveTime);
-      if (moveTime <= 300) {
-        move = move * 2;
-        moveTime = moveTime + 1000;
-        setMove(move, 'end', moveTime);
+
+      if (moveTime <= INERTIA_TIME && Math.abs(move) > INERTIA_DISTANCE) {
+        // 惯性滚动
+        const distance = momentum(move, moveTime);
+        setMove(distance, 'end', moveTime + 1000);
+        return;
       } else {
         setMove(move, 'end');
       }
+
+      setTimeout(() => {
+        touch.reset();
+        moving.value = false;
+      }, 0);
     };
 
-    const setRollerStyle = (index: number) => {
-      return `transform: rotate3d(1, 0, 0, ${-state.rotation * index}deg) translate3d(0px, 0px, 104px)`;
+    // 惯性滚动 距离
+    const momentum = (distance: number, duration: number) => {
+      // 惯性滚动的速度
+      const speed = Math.abs(distance / duration);
+      // 惯性滚动的距离
+      distance = (speed / 0.003) * (distance < 0 ? -1 : 1);
+      return distance;
     };
 
     const isHidden = (index: number) => {
@@ -149,13 +179,12 @@ export default create({
         touchTime.value = 0;
       }
       touchDeg.value = deg as number;
-      touchTranslateY.value = translateY;
       state.scrollDistance = translateY;
     };
 
     const setMove = (move: number, type?: string, time?: number) => {
-      console.log('setMove');
       let updateMove = move + state.transformY;
+
       if (type === 'end') {
         // 限定滚动距离
         if (updateMove > 0) {
@@ -171,33 +200,24 @@ export default create({
 
         setTransform(endMove, type, time, deg);
 
-        let t = time ? time / 2 : 0;
-        (state.timer as any) = setTimeout(() => {
-          setChooseValue();
-        }, t);
+        // let t = time ? time / 2 : 0;
+        // (state.timer as any) = setTimeout(() => {
+        //   setChooseValue();
+        // }, t);
 
         state.currIndex = Math.abs(Math.round(endMove / state.lineSpacing)) + 1;
       } else {
         let deg = '0deg';
-        let degNum = 0;
-        if (updateMove < 0) {
-          degNum = (Math.abs(updateMove / state.lineSpacing) + 1) * state.rotation;
-        } else {
-          degNum = (-updateMove / state.lineSpacing + 1) * state.rotation;
-        }
+        let currentDeg = (-updateMove / state.lineSpacing + 1) * state.rotation;
 
         // picker 滚动的最大角度
         const maxDeg = (props.column.length + 1) * state.rotation;
         const minDeg = 0;
-        if (degNum > maxDeg) {
-          deg = `${maxDeg}deg`;
-        } else if (degNum < minDeg) {
-          deg = `${minDeg}deg`;
-        } else {
-          deg = `${degNum}deg`;
-          setTransform(updateMove, null, undefined, deg);
-          state.currIndex = Math.abs(Math.round(updateMove / state.lineSpacing)) + 1;
-        }
+
+        deg = Math.min(Math.max(currentDeg, minDeg), maxDeg) + 'deg';
+
+        setTransform(updateMove, null, undefined, deg);
+        state.currIndex = Math.abs(Math.round(updateMove / state.lineSpacing)) + 1;
       }
     };
 
@@ -215,6 +235,24 @@ export default create({
       setMove(-move);
     };
 
+    const preventDefault = (event: Event, isStopPropagation?: boolean) => {
+      /* istanbul ignore else */
+      if (typeof event.cancelable !== 'boolean' || event.cancelable) {
+        event.preventDefault();
+      }
+
+      if (isStopPropagation) {
+        event.stopPropagation();
+      }
+    };
+
+    // 惯性滚动结束
+    const stopMomentum = () => {
+      moving.value = false;
+
+      setChooseValue();
+    };
+
     watch(
       () => props.column,
       (val) => {
@@ -256,7 +294,8 @@ export default create({
       onTouchEnd,
       touchRollerStyle,
       touchListStyle,
-      setMove
+      setMove,
+      stopMomentum
     };
   }
 });

+ 92 - 55
src/packages/__VUE/picker/ColumnTaro.vue

@@ -1,6 +1,6 @@
 <template>
   <view class="nut-picker__list" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
-    <view class="nut-picker-roller" ref="roller" :style="touchRollerStyle">
+    <view class="nut-picker-roller" ref="roller" :style="touchRollerStyle" @transitionend="stopMomentum">
       <view
         class="nut-picker-roller-item"
         :class="{ 'nut-picker-roller-item-hidden': isHidden(index + 1) }"
@@ -12,17 +12,9 @@
       </view>
     </view>
     <view class="nut-picker-roller-mask"></view>
-    <!-- <view class="nut-picker-content">
-      <view class="nut-picker-list-panel" ref="list" :id="'list' + refRandomId" :style="touchListStyle">
-        <view
-          :class="['nut-picker-item', 'nut-picker-item-ref', item.className]"
-          v-for="(item, index) in column"
-          :key="item.value ? item.value : index"
-          >{{ item.text }}
-        </view>
-        <view class="nut-picker-placeholder" v-if="column && column.length === 1"></view>
-      </view>
-    </view> -->
+    <view class="nut-picker-content">
+      <view class="nut-picker-list-panel" ref="list" :id="'list' + refRandomId" :style="touchListStyle"> </view>
+    </view>
   </view>
 </template>
 <script lang="ts">
@@ -30,6 +22,7 @@ import { reactive, ref, watch, computed, toRefs, onMounted, PropType } from 'vue
 import { createComponent } from '@/packages/utils/create';
 import { PickerOption, TouchParams } from './types';
 import { useTaroRect } from '@/packages/utils/useTaroRect';
+import { useTouch } from '@/packages/utils/useTouch';
 const { create } = createComponent('picker-column');
 import Taro from '@tarojs/taro';
 
@@ -54,6 +47,7 @@ export default create({
 
   emits: ['click', 'change'],
   setup(props, { emit }) {
+    const touch: any = useTouch();
     const wrapper = ref<HTMLElement>();
     const itemref = ref();
     const state = reactive({
@@ -77,61 +71,96 @@ export default create({
     const list = ref(null);
     const listitem = ref(null);
 
+    const moving = ref(false); // 是否处于滚动中
     const touchDeg = ref(0);
     const touchTime = ref(0);
     const touchTranslateY = ref(0);
+
+    // 触发惯性滑动条件:
+    // 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_TIME` 且 move
+    // 距离大于 `MOMENTUM_DISTANCE` 时,执行惯性滑动
+    const INERTIA_TIME = 300;
+    const INERTIA_DISTANCE = 15;
+
     const touchListStyle = computed(() => {
       return {
-        transition: `transform ${touchTime.value}ms cubic-bezier(0.19, 1, 0.22, 1)`,
+        transition: `transform ${touchTime.value}ms cubic-bezier(0.17, 0.89, 0.45, 1)`,
         transform: `translate3d(0, ${state.scrollDistance}px, 0)`
       };
     });
 
     const touchRollerStyle = computed(() => {
       return {
-        transition: `transform ${touchTime.value}ms cubic-bezier(0.19, 1, 0.22, 1)`,
+        transition: `transform ${touchTime.value}ms cubic-bezier(0.17, 0.89, 0.45, 1)`,
         transform: `rotate3d(1, 0, 0, ${touchDeg.value})`
       };
     });
+    const setRollerStyle = (index: number) => {
+      return `transform: rotate3d(1, 0, 0, ${-state.rotation * index}deg) translate3d(0px, 0px, 104px)`;
+    };
 
     const onTouchStart = (event: TouchEvent) => {
-      event.preventDefault();
-      let changedTouches = event.changedTouches[0];
-      state.touchParams.startY = changedTouches.pageY;
-      state.touchParams.startTime = event.timeStamp || Date.now();
+      touch.start(event);
+      if (moving.value) {
+        const { transform } = window.getComputedStyle(list.value as any);
+
+        state.scrollDistance = +parseInt(transform.split(', ')[1]);
+      }
+
+      state.touchParams.startY = touch.deltaY.value;
+      state.touchParams.startTime = Date.now();
       state.transformY = state.scrollDistance;
     };
 
     const onTouchMove = (event: TouchEvent) => {
-      event.preventDefault();
-      let changedTouches = event.changedTouches[0];
-      (state.touchParams as TouchParams).lastY = changedTouches.pageY;
-      (state.touchParams as TouchParams).lastTime = event.timeStamp || Date.now();
+      touch.move(event);
+
+      if ((touch as any).isVertical) {
+        moving.value = true;
+        preventDefault(event, true);
+      }
+
+      (state.touchParams as TouchParams).lastY = touch.deltaY.value;
+      const now = Date.now();
       let move = state.touchParams.lastY - state.touchParams.startY;
 
       setMove(move);
+
+      if (now - (state.touchParams as TouchParams).startTime > INERTIA_TIME) {
+        (state.touchParams as TouchParams).startTime = now;
+        state.touchParams.startY = (state.touchParams as TouchParams).lastY;
+      }
     };
 
     const onTouchEnd = (event: TouchEvent) => {
-      event.preventDefault();
-
-      let changedTouches = event.changedTouches[0];
-      state.touchParams.lastY = changedTouches.pageY;
-      state.touchParams.lastTime = event.timeStamp || Date.now();
+      state.touchParams.lastY = touch.deltaY.value;
+      state.touchParams.lastTime = Date.now();
       let move = state.touchParams.lastY - state.touchParams.startY;
 
       let moveTime = state.touchParams.lastTime - state.touchParams.startTime;
-      if (moveTime <= 300) {
-        move = move * 2;
-        moveTime = moveTime + 1000;
-        setMove(move, 'end', moveTime);
+
+      if (moveTime <= INERTIA_TIME && Math.abs(move) > INERTIA_DISTANCE) {
+        // 惯性滚动
+        const distance = momentum(move, moveTime);
+        setMove(distance, 'end', moveTime + 1000);
+        return;
       } else {
         setMove(move, 'end');
       }
+
+      setTimeout(() => {
+        touch.reset();
+        moving.value = false;
+      }, 0);
     };
 
-    const setRollerStyle = (index: number) => {
-      return `transform: rotate3d(1, 0, 0, ${-state.rotation * index}deg) translate3d(0px, 0px, 104px)`;
+    // 惯性滚动 距离
+    const momentum = (distance: number, duration: number) => {
+      // 惯性滚动的速度
+      const speed = Math.abs(distance / duration);
+      // 惯性滚动的距离
+      distance = (speed / 0.003) * (distance < 0 ? -1 : 1);
+      return distance;
     };
 
     const isHidden = (index: number) => {
@@ -169,33 +198,24 @@ export default create({
         let deg = `${(Math.abs(Math.round(endMove / state.lineSpacing)) + 1) * state.rotation}deg`;
         setTransform(endMove, type, time, deg);
 
-        let t = time ? time / 2 : 0;
-        (state.timer as any) = setTimeout(() => {
-          setChooseValue();
-        }, t);
+        // let t = time ? time / 2 : 0;
+        // (state.timer as any) = setTimeout(() => {
+        //   setChooseValue();
+        // }, t);
 
         state.currIndex = Math.abs(Math.round(endMove / state.lineSpacing)) + 1;
       } else {
         let deg = '0deg';
-        let degNum = 0;
-        if (updateMove < 0) {
-          degNum = (Math.abs(updateMove / state.lineSpacing) + 1) * state.rotation;
-        } else {
-          degNum = (-updateMove / state.lineSpacing + 1) * state.rotation;
-        }
+        let currentDeg = (-updateMove / state.lineSpacing + 1) * state.rotation;
 
         // picker 滚动的最大角度
         const maxDeg = (props.column.length + 1) * state.rotation;
         const minDeg = 0;
-        if (degNum > maxDeg) {
-          deg = `${maxDeg}deg`;
-        } else if (degNum < minDeg) {
-          deg = `${minDeg}deg`;
-        } else {
-          deg = `${degNum}deg`;
-          setTransform(updateMove, null, undefined, deg);
-          state.currIndex = Math.abs(Math.round(updateMove / state.lineSpacing)) + 1;
-        }
+
+        deg = Math.min(Math.max(currentDeg, minDeg), maxDeg) + 'deg';
+
+        setTransform(updateMove, null, undefined, deg);
+        state.currIndex = Math.abs(Math.round(updateMove / state.lineSpacing)) + 1;
       }
     };
 
@@ -213,9 +233,25 @@ export default create({
       setMove(-move);
     };
 
+    const preventDefault = (event: Event, isStopPropagation?: boolean) => {
+      /* istanbul ignore else */
+      if (typeof event.cancelable !== 'boolean' || event.cancelable) {
+        event.preventDefault();
+      }
+
+      if (isStopPropagation) {
+        event.stopPropagation();
+      }
+    };
+
+    // 惯性滚动结束
+    const stopMomentum = () => {
+      moving.value = false;
+      console.log('滚动结束');
+      setChooseValue();
+    };
+
     const getReference = async () => {
-      // const refe = await useTaroRect(list, Taro);
-      // state.lineSpacing = refe.height / props.column.length;
       modifyStatus(true);
     };
 
@@ -270,7 +306,8 @@ export default create({
       touchRollerStyle,
       touchListStyle,
       setMove,
-      refRandomId
+      refRandomId,
+      stopMomentum
     };
   }
 });

+ 1 - 0
src/packages/__VUE/picker/index.scss

@@ -69,6 +69,7 @@
     height: 252px;
     overflow: hidden;
     text-align: center;
+    -webkit-overflow-scrolling: touch;
   }
 
   &-roller {

+ 4 - 1
src/packages/__VUE/picker/index.taro.vue

@@ -159,7 +159,10 @@ export default create({
     };
 
     const close = () => {
-      emit('close');
+      emit('close', {
+        selectedValue: defaultValues.value,
+        selectedOptions: selectedOptions.value
+      });
       emit('update:visible', false);
     };
 

+ 4 - 1
src/packages/__VUE/picker/index.vue

@@ -167,7 +167,10 @@ export default create({
     };
 
     const close = () => {
-      emit('close');
+      emit('close', {
+        selectedValue: defaultValues.value,
+        selectedOptions: selectedOptions.value
+      });
       emit('update:visible', false);
     };
 

+ 8 - 2
src/sites/mobile-taro/vue/src/dentry/pages/picker/index.vue

@@ -12,7 +12,7 @@
     ></nut-cell>
     <nut-picker
       v-model:visible="show"
-      :columns="columns"
+      :columns="columsNum"
       title="城市选择"
       @change="change"
       @confirm="(options) => confirm('index', options)"
@@ -118,6 +118,7 @@ export default {
   setup() {
     const selectedValue = ref(['ZheJiang']);
     const asyncValue = ref<string[]>([]);
+    const columsNum = ref([]);
     const columns = ref([
       { text: '南京市', value: 'NanJing' },
       { text: '无锡市', value: 'WuXi' },
@@ -245,6 +246,10 @@ export default {
     };
 
     onMounted(() => {
+      for (let i = 1; i < 60; i++) {
+        columsNum.value.push({ text: i, value: i });
+      }
+
       setTimeout(() => {
         asyncColumns.value = [
           { text: '南京市', value: 'NanJing' },
@@ -289,7 +294,8 @@ export default {
       asyncColumns,
       effectColumns,
       showEffect,
-      alwaysFun
+      alwaysFun,
+      columsNum
     };
   }
 };