diff --git a/packages/address-edit/test/__snapshots__/index.spec.js.snap b/packages/address-edit/test/__snapshots__/index.spec.js.snap index 9623de848..6e7aa9c0e 100644 --- a/packages/address-edit/test/__snapshots__/index.spec.js.snap +++ b/packages/address-edit/test/__snapshots__/index.spec.js.snap @@ -40,14 +40,15 @@ exports[`create a AddressEdit 1`] = `
- +
- +
- +
+
@@ -109,14 +110,15 @@ exports[`create a AddressEdit with props 1`] = `
- +
- +
- +
+
diff --git a/packages/area/test/__snapshots__/demo.spec.js.snap b/packages/area/test/__snapshots__/demo.spec.js.snap index 840df29d2..2b7e32edb 100644 --- a/packages/area/test/__snapshots__/demo.spec.js.snap +++ b/packages/area/test/__snapshots__/demo.spec.js.snap @@ -11,69 +11,70 @@ exports[`renders demo correctly 1`] = `
-
-
-
+
@@ -87,74 +88,75 @@ exports[`renders demo correctly 1`] = `
-
-
-
+
@@ -169,49 +171,50 @@ exports[`renders demo correctly 1`] = `
-
-
+
diff --git a/packages/area/test/__snapshots__/index.spec.js.snap b/packages/area/test/__snapshots__/index.spec.js.snap index 1ebee1c5b..6fa0b5ee5 100644 --- a/packages/area/test/__snapshots__/index.spec.js.snap +++ b/packages/area/test/__snapshots__/index.spec.js.snap @@ -9,23 +9,24 @@ exports[`change option 1`] = `
-
-
-
+
@@ -40,23 +41,24 @@ exports[`change option 2`] = `
-
-
-
+
@@ -71,23 +73,24 @@ exports[`change option 3`] = `
-
-
-
+
@@ -102,22 +105,23 @@ exports[`reset method 1`] = `
-
-
-
+
@@ -132,23 +136,24 @@ exports[`reset method 2`] = `
-
-
-
+
@@ -163,23 +168,24 @@ exports[`watch areaList & code 1`] = `
-
-
-
+
@@ -194,23 +200,24 @@ exports[`watch areaList & code 2`] = `
-
-
-
+
@@ -225,23 +232,24 @@ exports[`watch areaList & code 3`] = `
-
-
-
+
diff --git a/packages/area/test/index.spec.js b/packages/area/test/index.spec.js index 052f4b8d8..229fddbeb 100644 --- a/packages/area/test/index.spec.js +++ b/packages/area/test/index.spec.js @@ -68,10 +68,15 @@ test('change option', () => { const columns = wrapper.findAll('.van-picker-column'); expect(wrapper).toMatchSnapshot(); + triggerDrag(columns.at(0), 0, -100); + columns.at(0).find('ul').trigger('transitionend'); expect(wrapper).toMatchSnapshot(); + triggerDrag(columns.at(2), 0, -100); + columns.at(2).find('ul').trigger('transitionend'); expect(wrapper).toMatchSnapshot(); + expect(onChange.mock.calls[0][1]).toEqual(secondOption); }); diff --git a/packages/datetime-picker/test/__snapshots__/demo.spec.js.snap b/packages/datetime-picker/test/__snapshots__/demo.spec.js.snap index a7fd73288..162ea47d2 100644 --- a/packages/datetime-picker/test/__snapshots__/demo.spec.js.snap +++ b/packages/datetime-picker/test/__snapshots__/demo.spec.js.snap @@ -11,154 +11,155 @@ exports[`renders demo correctly 1`] = `
-
-
-
-
-
+
@@ -172,72 +173,73 @@ exports[`renders demo correctly 1`] = `
-
-
-
+
@@ -251,37 +253,38 @@ exports[`renders demo correctly 1`] = `
-
-
+
@@ -295,84 +298,85 @@ exports[`renders demo correctly 1`] = `
-
-
+
diff --git a/packages/picker/PickerColumn.js b/packages/picker/PickerColumn.js index 382482f4a..d427d54d6 100644 --- a/packages/picker/PickerColumn.js +++ b/packages/picker/PickerColumn.js @@ -1,11 +1,22 @@ import { deepClone } from '../utils/deep-clone'; import { use, isObj, range } from '../utils'; import { preventDefault } from '../utils/event'; +import { TouchMixin } from '../mixins/touch'; const DEFAULT_DURATION = 200; + +// 惯性滑动思路: +// 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_LIMIT_TIME` 且 move 距 +// 离大于 `MOMENTUM_LIMIT_DISTANCE` 时,执行惯性滑动,持续 `MOMENTUM_DURATION` +const MOMENTUM_DURATION = 1500; +const MOMENTUM_LIMIT_TIME = 300; +const MOMENTUM_LIMIT_DISTANCE = 15; + const [sfc, bem] = use('picker-column'); export default sfc({ + mixins: [TouchMixin], + props: { valueKey: String, className: String, @@ -17,10 +28,12 @@ export default sfc({ data() { return { - startY: 0, offset: 0, duration: 0, startOffset: 0, + momentumOffset: 0, + touchTimestamp: 0, + moving: false, options: deepClone(this.initialOptions), currentIndex: this.defaultIndex }; @@ -50,29 +63,73 @@ export default sfc({ methods: { onTouchStart(event) { - this.startY = event.touches[0].clientY; - this.startOffset = this.offset; + this.touchStart(event); + + if (this.moving) { + const { translateY } = this.getEleTransform(this.$refs.wrapper); + this.startOffset = Math.min(0, translateY); + } else { + this.startOffset = this.offset; + } + this.duration = 0; + this.moving = false; + this.transitionEndTrigger = null; + this.touchTimestamp = Date.now(); + this.momentumOffset = this.startOffset; }, onTouchMove(event) { preventDefault(event); - const deltaY = event.touches[0].clientY - this.startY; + this.moving = true; + this.touchMove(event); this.offset = range( - this.startOffset + deltaY, + this.startOffset + this.deltaY, -(this.count * this.itemHeight), this.itemHeight ); + + const now = Date.now(); + if (now - this.touchTimestamp > MOMENTUM_LIMIT_TIME) { + this.touchTimestamp = now; + this.momentumOffset = this.offset; + } }, onTouchEnd() { + const distance = this.offset - this.momentumOffset; + const duration = Date.now() - this.touchTimestamp; + const allowMomentum = + duration < MOMENTUM_LIMIT_TIME && + Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE; + + if (allowMomentum) { + this.momentum(distance, duration); + return; + } + if (this.offset !== this.startOffset) { this.duration = DEFAULT_DURATION; - const index = range(Math.round(-this.offset / this.itemHeight), 0, this.count - 1); + const index = this.getIndexByOffset(this.offset); this.setIndex(index, true); } }, + onTransitionEnd() { + this.moving = false; + + if (this.transitionEndTrigger) { + this.transitionEndTrigger(); + this.transitionEndTrigger = null; + } + }, + + onClickItem(e) { + const index = Number(e.currentTarget.getAttribute('data-index')); + this.duration = DEFAULT_DURATION; + this.setIndex(index, true); + }, + adjustIndex(index) { index = range(index, 0, this.count); for (let i = index; i < this.count; i++) { @@ -95,9 +152,19 @@ export default sfc({ index = this.adjustIndex(index) || 0; this.offset = -index * this.itemHeight; - if (index !== this.currentIndex) { - this.currentIndex = index; - userAction && this.$emit('change', index); + const trigger = () => { + if (index !== this.currentIndex) { + this.currentIndex = index; + userAction && this.$emit('change', index); + } + }; + + // 若有触发过 `touchmove` 事件,那应该 + // 在 `transitionend` 后再触发 `change` 事件 + if (this.moving) { + this.transitionEndTrigger = trigger; + } else { + trigger(); } }, @@ -112,7 +179,43 @@ export default sfc({ getValue() { return this.options[this.currentIndex]; - } + }, + + getIndexByOffset(offset) { + return range( + Math.round(-offset / this.itemHeight), + 0, + this.count - 1 + ); + }, + + getEleTransform(ele) { + const { transform } = window.getComputedStyle(ele); + const matrix = transform + .slice(7, transform.length - 1) + .split(', ') + .map(val => Number(val)); + + return { + scaleX: matrix[0], + skewY: matrix[1], + skewX: matrix[2], + scaleY: matrix[3], + translateX: matrix[4], + translateY: matrix[5] + }; + }, + + momentum(distance, duration) { + const speed = Math.abs(distance / duration); + + distance = this.offset + speed / 0.0015 * (distance < 0 ? -1 : 1); + + const index = this.getIndexByOffset(distance); + + this.duration = MOMENTUM_DURATION; + this.setIndex(index, true); + }, }, render(h) { @@ -125,8 +228,9 @@ export default sfc({ const baseOffset = (itemHeight * (visibleItemCount - 1)) / 2; const wrapperStyle = { - transition: `${this.duration}ms`, transform: `translate3d(0, ${this.offset + baseOffset}px, 0)`, + transitionTimingFunction: 'cubic-bezier(0.23, 1, 0.32, 1)', + transitionDuration: `${this.duration}ms`, lineHeight: `${itemHeight}px` }; @@ -143,7 +247,11 @@ export default sfc({ onTouchend={this.onTouchEnd} onTouchcancel={this.onTouchEnd} > -