提交 6e8dfd97 编写于 作者: D DCloud_LXH

refactor: movable-area、movable-view

上级 ad51f136
...@@ -9,8 +9,8 @@ import Icon from './icon/index' ...@@ -9,8 +9,8 @@ import Icon from './icon/index'
import Image from './image/index' import Image from './image/index'
import Input from './input/index' import Input from './input/index'
import Label from './label/index' import Label from './label/index'
// import MovableArea from './movable-area/index.vue' import MovableArea from './movable-area/index'
import MovableView from './movable-view/index.vue' import MovableView from './movable-view/index'
import Navigator from './navigator/index.vue' import Navigator from './navigator/index.vue'
// import PickerView from './picker-view/index.vue' // import PickerView from './picker-view/index.vue'
// import PickerViewColumn from './picker-view-column/index.vue' // import PickerViewColumn from './picker-view-column/index.vue'
...@@ -39,7 +39,7 @@ export { ...@@ -39,7 +39,7 @@ export {
Image, Image,
Input, Input,
Label, Label,
// MovableArea, MovableArea,
MovableView, MovableView,
Navigator, Navigator,
// PickerView, // PickerView,
......
import {
ref,
defineComponent,
ExtractPropTypes,
reactive,
Ref,
onMounted,
VNode,
markRaw,
provide,
} from 'vue'
import { useAttrs, withWebEvent } from '@dcloudio/uni-components'
import { initScrollBounce, disableScrollBounce } from '../../helpers/scroll'
import ResizeSensor from '../resize-sensor/index'
const props = {
scaleArea: {
type: Boolean,
default: false,
},
}
type Props = ExtractPropTypes<typeof props>
export interface MovableViewContext {
rootRef: Ref<HTMLElement | null>
setParent: Function
_endScale: Function
_setScale: Function
}
export type AddMovableViewContext = (context: MovableViewContext) => void
export type RemoveMovableViewContext = (context: MovableViewContext) => void
export default /*#__PURE__*/ defineComponent({
inheritAttrs: false,
name: 'MovableArea',
props,
setup(props, { slots }) {
const rootRef = ref(null)
const _isMounted = ref(false)
let { setContexts, events: movableAreaEvents } = useMovableAreaState(
props,
rootRef
)
const { $listeners, $attrs, $excludeAttrs } = useAttrs()
const _listeners = $listeners.value
let events = ['onTouchstart', 'onTouchmove', 'onTouchend']
events.forEach((event) => {
let existing = (_listeners as any)[event]
let ours =
movableAreaEvents[
`_${event}` as '_onTouchstart' | '_onTouchmove' | '_onTouchend'
]
;(_listeners as any)[event] = existing
? [].concat(existing, ours as any)
: ours
})
onMounted(() => {
movableAreaEvents._resize()
initScrollBounce()
_isMounted.value = true
})
let movableViewItems: VNode[] = []
const originMovableViewContexts: MovableViewContext[] = []
function updateMovableViewContexts() {
const contexts: MovableViewContext[] = []
for (let index = 0; index < movableViewItems.length; index++) {
const movableViewItem = movableViewItems[index]
const movableViewContext = originMovableViewContexts.find(
(context) => movableViewItem.el === context.rootRef.value
)
if (movableViewContext) {
contexts.push(markRaw(movableViewContext))
}
}
setContexts(contexts)
}
const addMovableViewContext: AddMovableViewContext = (
movableViewContext
) => {
originMovableViewContexts.push(movableViewContext)
updateMovableViewContexts()
}
const removeMovableViewContext: RemoveMovableViewContext = (
movableViewContext
) => {
const index = originMovableViewContexts.indexOf(movableViewContext)
if (index >= 0) {
originMovableViewContexts.splice(index, 1)
updateMovableViewContexts()
}
}
provide('_isMounted', _isMounted)
provide('movableAreaRootRef', rootRef)
provide('addMovableViewContext', addMovableViewContext)
provide('removeMovableViewContext', removeMovableViewContext)
return () => {
const defaultSlots = slots.default && slots.default()
movableViewItems = defaultSlots || []
return (
<uni-movable-area
ref={rootRef}
{...$attrs.value}
{...$excludeAttrs.value}
{..._listeners}
>
{/* @ts-ignore */}
<ResizeSensor onReize={movableAreaEvents._resize}></ResizeSensor>
{movableViewItems}
</uni-movable-area>
)
}
},
})
type GapV = {
x: number | null
y: number | null
}
function calc(e: GapV) {
return Math.sqrt(e.x! * e.x! + e.y! * e.y!)
}
function useMovableAreaState(props: Props, rootRef: Ref<HTMLElement | null>) {
const width = ref(0)
const height = ref(0)
const gapV: GapV = reactive({
x: null,
y: null,
})
const pinchStartLen = ref<number | null>(null)
let _scaleMovableView: MovableViewContext | null = null
let movableViewContexts: MovableViewContext[] = []
function _updateScale(e: number) {
if (e && e !== 1) {
if (props.scaleArea) {
movableViewContexts.forEach(function (item) {
item._setScale(e)
})
} else {
if (_scaleMovableView) {
_scaleMovableView._setScale(e)
}
}
}
}
function _find(target: TouchEvent['target'], items = movableViewContexts) {
let root = rootRef.value
function get(
node: EventTarget | HTMLElement | null
): MovableViewContext | null {
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (node === item.rootRef.value) {
return item
}
}
if (node === root || node === document.body || node === document) {
return null
}
return get((node as HTMLElement).parentNode)
}
return get(target)
}
const _onTouchstart = withWebEvent((t: TouchEvent) => {
disableScrollBounce({
disable: true,
})
let i = t.touches
if (i) {
if (i.length > 1) {
let r = {
x: i[1].pageX - i[0].pageX,
y: i[1].pageY - i[0].pageY,
}
pinchStartLen.value = calc(r)
gapV.x = r.x
gapV.y = r.y
if (!props.scaleArea) {
let touch0 = _find(i[0].target)
let touch1 = _find(i[1].target)
_scaleMovableView = touch0 && touch0 === touch1 ? touch0 : null
}
}
}
})
const _onTouchmove = withWebEvent((t: TouchEvent) => {
let n = t.touches
if (n) {
if (n.length > 1) {
t.preventDefault()
let i = {
x: n[1].pageX - n[0].pageX,
y: n[1].pageY - n[0].pageY,
}
if (gapV.x !== null && pinchStartLen.value && pinchStartLen.value > 0) {
let r = calc(i) / pinchStartLen.value
_updateScale(r)
}
gapV.x = i.x
gapV.y = i.y
}
}
})
const _onTouchend = withWebEvent((e: TouchEvent) => {
disableScrollBounce({
disable: false,
})
let t = e.touches
if (!(t && t.length)) {
if (e.changedTouches) {
gapV.x = 0
gapV.y = 0
pinchStartLen.value = null
if (props.scaleArea) {
movableViewContexts.forEach(function (item) {
item._endScale()
})
} else {
if (_scaleMovableView) {
_scaleMovableView._endScale()
}
}
}
}
})
function _resize() {
_getWH()
movableViewContexts.forEach(function (item, index) {
item.setParent()
})
}
function _getWH() {
let style = window.getComputedStyle(rootRef.value!)
let rect = rootRef.value!.getBoundingClientRect()
width.value =
rect.width -
['Left', 'Right'].reduce(function (all, item) {
const LEFT = ('border' + item + 'Width') as keyof CSSStyleDeclaration
const RIGHT = ('padding' + item) as keyof CSSStyleDeclaration
return (
all +
parseFloat((style[LEFT] as unknown) as string) +
parseFloat((style[RIGHT] as unknown) as string)
)
}, 0)
height.value =
rect.height -
['Top', 'Bottom'].reduce(function (all, item) {
const TOP = ('border' + item + 'Width') as keyof CSSStyleDeclaration
const BOTTOM = ('padding' + item) as keyof CSSStyleDeclaration
return (
all +
parseFloat((style[TOP] as unknown) as string) +
parseFloat((style[BOTTOM] as unknown) as string)
)
}, 0)
}
provide('movableAreaWidth', width)
provide('movableAreaHeight', height)
return {
setContexts(contexts: MovableViewContext[]) {
movableViewContexts = contexts
},
events: {
_onTouchstart,
_onTouchmove,
_onTouchend,
_resize,
},
}
}
<script>
import {disableScrollBounce} from '../../helpers/disable-scroll-bounce'
import {
deepClone
} from 'uni-shared'
function calc (e) {
return Math.sqrt(e.x * e.x + e.y * e.y)
}
export default {
name: 'MovableArea',
props: {
scaleArea: {
type: Boolean,
default: false
}
},
data () {
return {
width: 0,
height: 0,
items: []
}
},
created: function () {
this.gapV = {
x: null,
y: null
}
this.pinchStartLen = null
},
mounted: function () {
this._resize()
},
methods: {
_resize () {
this._getWH()
this.items.forEach(function (item, index) {
item.componentInstance.setParent()
})
},
_find (target, items = this.items) {
var root = this.$el
function get (node) {
for (let i = 0; i < items.length; i++) {
const item = items[i]
if (node === item.componentInstance.$el) {
return item
}
}
if (node === root || node === document.body || node === document) {
return null
}
return get(node.parentNode)
}
return get(target)
},
_touchstart (t) {
disableScrollBounce({
disable: true
})
var i = t.touches
if (i) {
if (i.length > 1) {
var r = {
x: i[1].pageX - i[0].pageX,
y: i[1].pageY - i[0].pageY
}
this.pinchStartLen = calc(r)
this.gapV = r
if (!this.scaleArea) {
var touch0 = this._find(i[0].target)
var touch1 = this._find(i[1].target)
this._scaleMovableView = touch0 && touch0 === touch1 ? touch0 : null
}
}
}
},
_touchmove (t) {
var n = t.touches
if (n) {
if (n.length > 1) {
t.preventDefault()
var i = {
x: n[1].pageX - n[0].pageX,
y: n[1].pageY - n[0].pageY
}
if (this.gapV.x !== null && this.pinchStartLen > 0) {
var r = calc(i) / this.pinchStartLen
this._updateScale(r)
}
this.gapV = i
}
}
},
_touchend (e) {
disableScrollBounce({
disable: false
})
var t = e.touches
if (!(t && t.length)) {
if (e.changedTouches) {
this.gapV.x = 0
this.gapV.y = 0
this.pinchStartLen = null
if (this.scaleArea) {
this.items.forEach(function (item) {
item.componentInstance._endScale()
})
} else {
if (this._scaleMovableView) {
this._scaleMovableView.componentInstance._endScale()
}
}
}
}
},
_updateScale (e) {
if (e && e !== 1) {
if (this.scaleArea) {
this.items.forEach(function (item) {
item.componentInstance._setScale(e)
})
} else {
if (this._scaleMovableView) {
this._scaleMovableView.componentInstance._setScale(e)
}
}
}
},
_getWH () {
var style = window.getComputedStyle(this.$el)
var rect = this.$el.getBoundingClientRect()
this.width = rect.width - ['Left', 'Right'].reduce(function (all, item) {
return all + parseFloat(style['border' + item + 'Width']) + parseFloat(style['padding' + item])
}, 0)
this.height = rect.height - ['Top', 'Bottom'].reduce(function (all, item) {
return all + parseFloat(style['border' + item + 'Width']) + parseFloat(style['padding' + item])
}, 0)
}
},
render (createElement) {
var items = []
const $slots = this.$slots.default && deepClone(this.$slots.default, createElement)
if ($slots) {
$slots.forEach(vnode => {
if (vnode.componentOptions && vnode.componentOptions.tag === 'v-uni-movable-view') {
items.push(vnode)
}
})
}
this.items = items
var $listeners = Object.assign({}, this.$listeners)
var events = ['touchstart', 'touchmove', 'touchend']
events.forEach(event => {
var existing = $listeners[event]
var ours = this[`_${event}`]
$listeners[event] = existing ? [].concat(existing, ours) : ours
})
return createElement('uni-movable-area', {
on: $listeners
}, [createElement('v-uni-resize-sensor', {
on: {
resize: this._resize
}
}), $slots])
}
}
</script>
\ No newline at end of file
import {
ref,
defineComponent,
ExtractPropTypes,
Ref,
onMounted,
inject,
computed,
watch,
onUnmounted,
} from 'vue'
import { initScrollBounce, disableScrollBounce } from '../../helpers/scroll'
import { useTouchtrack, TouchtrackEvent } from '../../helpers/useTouchtrack'
import ResizeSensor from '../resize-sensor/index'
import {
useCustomEvent,
CustomEventTrigger,
EmitEvent,
} from '../../helpers/useEvent'
import type {
MovableViewContext,
AddMovableViewContext,
RemoveMovableViewContext,
} from '../movable-area/index'
import { Decline, Friction, STD } from './utils'
const props = {
direction: {
type: String,
default: 'none',
},
inertia: {
type: [Boolean, String],
default: false,
},
outOfBounds: {
type: [Boolean, String],
default: false,
},
x: {
type: [Number, String],
default: 0,
},
y: {
type: [Number, String],
default: 0,
},
damping: {
type: [Number, String],
default: 20,
},
friction: {
type: [Number, String],
default: 2,
},
disabled: {
type: [Boolean, String],
default: false,
},
scale: {
type: [Boolean, String],
default: false,
},
scaleMin: {
type: [Number, String],
default: 0.5,
},
scaleMax: {
type: [Number, String],
default: 10,
},
scaleValue: {
type: [Number, String],
default: 1,
},
animation: {
type: [Boolean, String],
default: true,
},
}
type Props = ExtractPropTypes<typeof props>
type RootRef = Ref<HTMLElement | null>
export default /*#__PURE__*/ defineComponent({
name: 'MovableView',
props,
emits: ['change', 'scale'],
setup(props, { slots, emit }) {
const rootRef: RootRef = ref(null)
const trigger = useCustomEvent<EmitEvent<typeof emit>>(rootRef, emit)
const { setParent } = useMovableViewState(props, trigger, rootRef)
return () => {
return (
<uni-movable-view ref={rootRef}>
{/* @ts-ignore */}
<ResizeSensor onResize={setParent}></ResizeSensor>
{slots.default && slots.default()}
</uni-movable-view>
)
}
},
})
let requesting = false
function _requestAnimationFrame(e: Function) {
if (!requesting) {
requesting = true
requestAnimationFrame(function () {
e()
requesting = false
})
}
}
function p(t: HTMLElement, n: HTMLElement): number {
if (t === n) {
return 0
}
let i = t.offsetLeft
return t.offsetParent ? (i += p(t.offsetParent as HTMLElement, n)) : 0
}
function f(t: HTMLElement, n: HTMLElement): number {
if (t === n) {
return 0
}
let i = t.offsetTop
return t.offsetParent ? (i += f(t.offsetParent as HTMLElement, n)) : 0
}
function v(a: number, b: number) {
return +((1000 * a - 1000 * b) / 1000).toFixed(1)
}
function g(e: Friction | STD, t: Function, n: Function) {
type R = {
id: number
cancelled: boolean
}
let i = function (e: R) {
if (e && e.id) {
cancelAnimationFrame(e.id)
}
if (e) {
e.cancelled = true
}
}
let r: R = {
id: 0,
cancelled: false,
}
function fn(n: R, i: Friction | STD, r: Function, o: Function) {
if (!n || !n.cancelled) {
r(i)
let a = e.done()
if (!a) {
if (!n.cancelled) {
n.id = requestAnimationFrame(fn.bind(null, n, i, r, o))
}
}
if (a && o) {
o(i)
}
}
}
fn(r, e, t, n)
return {
cancel: i.bind(null, r),
model: e,
}
}
function _getPx(val: Props['x'] | Props['y']) {
if (/\d+[ur]px$/i.test(val as string)) {
return uni.upx2px(parseFloat(val as string))
}
return Number(val) || 0
}
type ReturnType_g = ReturnType<typeof g> | null
type ScaleOffset = {
x: number
y: number
}
type MoveDirection = 'htouchmove' | 'vtouchmove'
function useMovableViewState(
props: Props,
trigger: CustomEventTrigger,
rootRef: RootRef
) {
const movableAreaWidth: Ref<number> = inject('movableAreaWidth', ref(0))
const movableAreaHeight: Ref<number> = inject('movableAreaHeight', ref(0))
const _isMounted: Ref<boolean> = inject('_isMounted', ref(false))
const movableAreaRootRef: RootRef = inject('movableAreaRootRef')!
const addMovableViewContext: AddMovableViewContext = inject(
'addMovableViewContext',
() => {}
)
const removeMovableViewContext: RemoveMovableViewContext = inject(
'removeMovableViewContext',
() => {}
)
const xSync = ref(_getPx(props.x))
const ySync = ref(_getPx(props.y))
const scaleValueSync = ref(Number(props.scaleValue) || 1)
const width = ref(0)
const height = ref(0)
const minX = ref(0)
const minY = ref(0)
const maxX = ref(0)
const maxY = ref(0)
let _SFA: ReturnType_g = null
let _FA: ReturnType_g = null
const _offset: ScaleOffset = {
x: 0,
y: 0,
}
const _scaleOffset: ScaleOffset = {
x: 0,
y: 0,
}
let _scale = 1
let _oldScale = 1
let _translateX = 0
let _translateY = 0
let _isScaling = false
let _isTouching = false
let __baseX: number
let __baseY: number
let _checkCanMove: boolean | null = null
let _firstMoveDirection: MoveDirection | null = null
const _declineX = new Decline()
const _declineY = new Decline()
const __touchInfo = {
historyX: [0, 0],
historyY: [0, 0],
historyT: [0, 0],
}
const dampingNumber = computed(() => {
let val = Number(props.damping)
return isNaN(val) ? 20 : val
})
const frictionNumber = computed(() => {
let val = Number(props.friction)
return isNaN(val) || val <= 0 ? 2 : val
})
const scaleMinNumber = computed(() => {
let val = Number(props.scaleMin)
return isNaN(val) ? 0.5 : val
})
const scaleMaxNumber = computed(() => {
let val = Number(props.scaleMax)
return isNaN(val) ? 10 : val
})
const xMove = computed(
() => props.direction === 'all' || props.direction === 'horizontal'
)
const yMove = computed(
() => props.direction === 'all' || props.direction === 'vertical'
)
const _STD = new STD(
1,
(9 * Math.pow(dampingNumber.value, 2)) / 40,
dampingNumber.value
)
const _friction = new Friction(1, frictionNumber.value)
watch(
() => props.x,
(val) => {
xSync.value = _getPx(val)
}
)
watch(
() => props.y,
(val) => {
ySync.value = _getPx(val)
}
)
watch(xSync, (val) => {
_setX(val)
})
watch(ySync, (val) => {
_setY(val)
})
watch(
() => props.scaleValue,
(val) => {
scaleValueSync.value = Number(val) || 0
}
)
watch(scaleValueSync, (val) => {
_setScaleValue(val)
})
watch(scaleMinNumber, () => {
_setScaleMinOrMax()
})
watch(scaleMaxNumber, () => {
_setScaleMinOrMax()
})
function _setX(val: number) {
if (xMove.value) {
if (val + _scaleOffset.x === _translateX) {
return _translateX
} else {
if (_SFA) {
_SFA.cancel()
}
_animationTo(val + _scaleOffset.x, ySync.value + _scaleOffset.y, _scale)
}
}
return val
}
function _setY(val: number) {
if (yMove.value) {
if (val + _scaleOffset.y === _translateY) {
return _translateY
} else {
if (_SFA) {
_SFA.cancel()
}
_animationTo(xSync.value + _scaleOffset.x, val + _scaleOffset.y, _scale)
}
}
return val
}
function _setScaleMinOrMax() {
if (!props.scale) {
return false
}
_updateScale(_scale, true)
_updateOldScale(_scale)
}
function _setScaleValue(scale: number) {
if (!props.scale) {
return false
}
scale = _adjustScale(scale)
_updateScale(scale, true)
_updateOldScale(scale)
return scale
}
function __handleTouchStart() {
if (!_isScaling) {
if (!props.disabled) {
disableScrollBounce({
disable: true,
})
if (_FA) {
_FA.cancel()
}
if (_SFA) {
_SFA.cancel()
}
__touchInfo.historyX = [0, 0]
__touchInfo.historyY = [0, 0]
__touchInfo.historyT = [0, 0]
if (xMove.value) {
__baseX = _translateX
}
if (yMove.value) {
__baseY = _translateY
}
rootRef.value!.style.willChange = 'transform'
_checkCanMove = null
_firstMoveDirection = null
_isTouching = true
}
}
}
function __handleTouchMove(event: TouchtrackEvent) {
if (!_isScaling && !props.disabled && _isTouching) {
let x = _translateX
let y = _translateY
if (_firstMoveDirection === null) {
_firstMoveDirection =
Math.abs(event.detail.dx / event.detail.dy) > 1
? 'htouchmove'
: 'vtouchmove'
}
if (xMove.value) {
x = event.detail.dx + __baseX
__touchInfo.historyX.shift()
__touchInfo.historyX.push(x)
if (!yMove.value && _checkCanMove === null) {
_checkCanMove = Math.abs(event.detail.dx / event.detail.dy) < 1
}
}
if (yMove.value) {
y = event.detail.dy + __baseY
__touchInfo.historyY.shift()
__touchInfo.historyY.push(y)
if (!xMove.value && _checkCanMove === null) {
_checkCanMove = Math.abs(event.detail.dy / event.detail.dx) < 1
}
}
__touchInfo.historyT.shift()
__touchInfo.historyT.push(event.detail.timeStamp)
if (!_checkCanMove) {
event.preventDefault()
let source = 'touch'
if (x < minX.value) {
if (props.outOfBounds) {
source = 'touch-out-of-bounds'
x = minX.value - _declineX.x(minX.value - x)
} else {
x = minX.value
}
} else if (x > maxX.value) {
if (props.outOfBounds) {
source = 'touch-out-of-bounds'
x = maxX.value + _declineX.x(x - maxX.value)
} else {
x = maxX.value
}
}
if (y < minY.value) {
if (props.outOfBounds) {
source = 'touch-out-of-bounds'
y = minY.value - _declineY.x(minY.value - y)
} else {
y = minY.value
}
} else {
if (y > maxY.value) {
if (props.outOfBounds) {
source = 'touch-out-of-bounds'
y = maxY.value + _declineY.x(y - maxY.value)
} else {
y = maxY.value
}
}
}
_requestAnimationFrame(function () {
_setTransform(x, y, _scale, source)
})
}
}
}
function __handleTouchEnd() {
if (!_isScaling && !props.disabled && _isTouching) {
disableScrollBounce({
disable: false,
})
rootRef.value!.style.willChange = 'auto'
_isTouching = false
if (!_checkCanMove && !_revise('out-of-bounds') && props.inertia) {
const xv =
(1000 * (__touchInfo.historyX[1] - __touchInfo.historyX[0])) /
(__touchInfo.historyT[1] - __touchInfo.historyT[0])
const yv =
(1000 * (__touchInfo.historyY[1] - __touchInfo.historyY[0])) /
(__touchInfo.historyT[1] - __touchInfo.historyT[0])
_friction.setV(xv, yv)
_friction.setS(_translateX, _translateY)
const x0 = _friction.delta().x
const y0 = _friction.delta().y
let x = x0 + _translateX
let y = y0 + _translateY
if (x < minX.value) {
x = minX.value
y = _translateY + ((minX.value - _translateX) * y0) / x0
} else {
if (x > maxX.value) {
x = maxX.value
y = _translateY + ((maxX.value - _translateX) * y0) / x0
}
}
if (y < minY.value) {
y = minY.value
x = _translateX + ((minY.value - _translateY) * x0) / y0
} else {
if (y > maxY.value) {
y = maxY.value
x = _translateX + ((maxY.value - _translateY) * x0) / y0
}
}
_friction.setEnd(x, y)
_FA = g(
_friction,
function () {
let t = _friction.s()
let x = t.x
let y = t.y
_setTransform(x, y, _scale, 'friction')
},
function () {
_FA!.cancel()
}
)
}
}
}
function _getLimitXY(x: number, y: number) {
let outOfBounds = false
if (x > maxX.value) {
x = maxX.value
outOfBounds = true
} else {
if (x < minX.value) {
x = minX.value
outOfBounds = true
}
}
if (y > maxY.value) {
y = maxY.value
outOfBounds = true
} else {
if (y < minY.value) {
y = minY.value
outOfBounds = true
}
}
return {
x,
y,
outOfBounds,
}
}
function _updateOffset() {
_offset.x = p(rootRef.value!, movableAreaRootRef.value!)
_offset.y = f(rootRef.value!, movableAreaRootRef.value!)
}
function _updateWH(scale: number) {
scale = scale || _scale
scale = _adjustScale(scale)
let rect = rootRef.value!.getBoundingClientRect()
height.value = rect.height / _scale
width.value = rect.width / _scale
let _height = height.value * scale
let _width = width.value * scale
_scaleOffset.x = (_width - width.value) / 2
_scaleOffset.y = (_height - height.value) / 2
}
function _updateBoundary() {
let x = 0 - _offset.x + _scaleOffset.x
let _width =
movableAreaWidth.value - width.value - _offset.x - _scaleOffset.x
minX.value = Math.min(x, _width)
maxX.value = Math.max(x, _width)
let y = 0 - _offset.y + _scaleOffset.y
let _height =
movableAreaHeight.value - height.value - _offset.y - _scaleOffset.y
minY.value = Math.min(y, _height)
maxY.value = Math.max(y, _height)
}
function _beginScale() {
_isScaling = true
}
function _updateScale(scale: number, animat?: boolean) {
if (props.scale) {
scale = _adjustScale(scale)
_updateWH(scale)
_updateBoundary()
const limitXY = _getLimitXY(_translateX, _translateY)
const x = limitXY.x
const y = limitXY.y
if (animat) {
_animationTo(x, y, scale, '', true, true)
} else {
_requestAnimationFrame(function () {
_setTransform(x, y, scale, '', true, true)
})
}
}
}
function _updateOldScale(scale: number) {
_oldScale = scale
}
function _adjustScale(scale: number) {
scale = Math.max(0.5, scaleMinNumber.value, scale)
scale = Math.min(10, scaleMaxNumber.value, scale)
return scale
}
function _animationTo(
x: number,
y: number,
scale: number,
source?: number | string,
r?: boolean,
o?: boolean
) {
if (_FA) {
_FA.cancel()
}
if (_SFA) {
_SFA.cancel()
}
if (!xMove.value) {
x = _translateX
}
if (!yMove.value) {
y = _translateY
}
if (!props.scale) {
scale = _scale
}
let limitXY = _getLimitXY(x, y)
x = limitXY.x
y = limitXY.y
if (!props.animation) {
_setTransform(x, y, scale, source, r, o)
return
}
_STD._springX._solution = null
_STD._springY._solution = null
_STD._springScale._solution = null
_STD._springX._endPosition = _translateX
_STD._springY._endPosition = _translateY
_STD._springScale._endPosition = _scale
_STD.setEnd(x, y, scale, 1)
_SFA = g(
_STD,
function () {
let data = _STD.x()
let x = data.x
let y = data.y
let scale = data.scale
_setTransform(x, y, scale, source, r, o)
},
function () {
_SFA!.cancel()
}
)
}
function _revise(source: number | string) {
let limitXY = _getLimitXY(_translateX, _translateY)
let x = limitXY.x
let y = limitXY.y
let outOfBounds = limitXY.outOfBounds
if (outOfBounds) {
_animationTo(x, y, _scale, source)
}
return outOfBounds
}
function _setTransform(
x: number,
y: number,
scale: number,
source: string | number = '',
r?: boolean,
o?: boolean
) {
if (!(x !== null && x.toString() !== 'NaN' && typeof x === 'number')) {
x = _translateX || 0
}
if (!(y !== null && y.toString() !== 'NaN' && typeof y === 'number')) {
y = _translateY || 0
}
x = Number(x.toFixed(1))
y = Number(y.toFixed(1))
scale = Number(scale.toFixed(1))
if (!(_translateX === x && _translateY === y)) {
if (!r) {
trigger('change', {} as Event, {
x: v(x, _scaleOffset.x),
y: v(y, _scaleOffset.y),
source: source,
})
}
}
if (!props.scale) {
scale = _scale
}
scale = _adjustScale(scale)
scale = +scale.toFixed(3)
if (o && scale !== _scale) {
trigger('scale', {} as Event, {
x: x,
y: y,
scale: scale,
})
}
let transform =
'translateX(' +
x +
'px) translateY(' +
y +
'px) translateZ(0px) scale(' +
scale +
')'
rootRef.value!.style.transform = transform
rootRef.value!.style.webkitTransform = transform
_translateX = x
_translateY = y
_scale = scale
}
function setParent() {
if (!_isMounted.value) {
return
}
if (_FA) {
_FA.cancel()
}
if (_SFA) {
_SFA.cancel()
}
let scale = props.scale ? scaleValueSync.value : 1
_updateOffset()
_updateWH(scale)
_updateBoundary()
_translateX = xSync.value + _scaleOffset.x
_translateY = ySync.value + _scaleOffset.y
let limitXY = _getLimitXY(_translateX, _translateY)
let x = limitXY.x
let y = limitXY.y
_setTransform(x, y, scale, '', true)
_updateOldScale(scale)
}
function _endScale() {
_isScaling = false
_updateOldScale(_scale)
}
function _setScale(scale: number) {
if (scale) {
scale = _oldScale * scale
_beginScale()
_updateScale(scale)
}
}
onMounted(() => {
useTouchtrack(rootRef.value!, (event) => {
switch (event.detail.state) {
case 'start':
__handleTouchStart()
break
case 'move':
__handleTouchMove(event)
break
case 'end':
__handleTouchEnd()
}
})
setParent()
_friction.reconfigure(1, frictionNumber.value)
_STD.reconfigure(
1,
(9 * Math.pow(dampingNumber.value, 2)) / 40,
dampingNumber.value
)
rootRef.value!.style.transformOrigin = 'center'
initScrollBounce()
const context: MovableViewContext = {
rootRef,
setParent,
_endScale,
_setScale,
}
addMovableViewContext(context)
onUnmounted(() => {
removeMovableViewContext(context)
})
})
return {
setParent,
}
}
<template>
<uni-movable-view v-bind="$attrs">
<v-uni-resize-sensor @resize="setParent" />
<slot />
</uni-movable-view>
</template>
<script>
import touchtrack from '../../mixins/touchtrack'
import {
Decline,
Friction,
STD
} from './utils'
import {disableScrollBounce} from '../../helpers/disable-scroll-bounce'
var requesting = false
function _requestAnimationFrame (e) {
if (!requesting) {
requesting = true
requestAnimationFrame(function () {
e()
requesting = false
})
}
}
function p (t, n) {
if (t === n) {
return 0
}
var i = t.offsetLeft
return t.offsetParent ? (i += p(t.offsetParent, n)) : 0
}
function f (t, n) {
if (t === n) {
return 0
}
var i = t.offsetTop
return t.offsetParent ? (i += f(t.offsetParent, n)) : 0
}
function v (a, b) {
return +((1000 * a - 1000 * b) / 1000).toFixed(1)
}
function g (e, t, n) {
var i = function (e) {
if (e && e.id) {
cancelAnimationFrame(e.id)
}
if (e) {
e.cancelled = true
}
}
var r = {
id: 0,
cancelled: false
}
function fn (n, i, r, o) {
if (!n || !n.cancelled) {
r(i)
var a = e.done()
if (!a) {
if (!n.cancelled) {
(n.id = requestAnimationFrame(fn.bind(null, n, i, r, o)))
}
}
if (a && o) {
o(i)
}
}
}
fn(r, e, t, n)
return {
cancel: i.bind(null, r),
model: e
}
}
export default {
name: 'MovableView',
mixins: [touchtrack],
props: {
direction: {
type: String,
default: 'none'
},
inertia: {
type: [Boolean, String],
default: false
},
outOfBounds: {
type: [Boolean, String],
default: false
},
x: {
type: [Number, String],
default: 0
},
y: {
type: [Number, String],
default: 0
},
damping: {
type: [Number, String],
default: 20
},
friction: {
type: [Number, String],
default: 2
},
disabled: {
type: [Boolean, String],
default: false
},
scale: {
type: [Boolean, String],
default: false
},
scaleMin: {
type: [Number, String],
default: 0.5
},
scaleMax: {
type: [Number, String],
default: 10
},
scaleValue: {
type: [Number, String],
default: 1
},
animation: {
type: [Boolean, String],
default: true
}
},
data () {
return {
xSync: this._getPx(this.x),
ySync: this._getPx(this.y),
scaleValueSync: Number(this.scaleValue) || 1,
width: 0,
height: 0,
minX: 0,
minY: 0,
maxX: 0,
maxY: 0
}
},
computed: {
dampingNumber () {
var val = Number(this.damping)
return isNaN(val) ? 20 : val
},
frictionNumber () {
var val = Number(this.friction)
return isNaN(val) || val <= 0 ? 2 : val
},
scaleMinNumber () {
var val = Number(this.scaleMin)
return isNaN(val) ? 0.5 : val
},
scaleMaxNumber () {
var val = Number(this.scaleMax)
return isNaN(val) ? 10 : val
},
xMove () {
return this.direction === 'all' || this.direction === 'horizontal'
},
yMove () {
return this.direction === 'all' || this.direction === 'vertical'
}
},
watch: {
x (val) {
this.xSync = this._getPx(val)
},
xSync (val) {
this._setX(val)
},
y (val) {
this.ySync = this._getPx(val)
},
ySync (val) {
this._setY(val)
},
scaleValue (val) {
this.scaleValueSync = Number(val) || 0
},
scaleValueSync (val) {
this._setScaleValue(val)
},
scaleMinNumber () {
this._setScaleMinOrMax()
},
scaleMaxNumber () {
this._setScaleMinOrMax()
}
},
created: function () {
this._offset = {
x: 0,
y: 0
}
this._scaleOffset = {
x: 0,
y: 0
}
this._translateX = 0
this._translateY = 0
this._scale = 1
this._oldScale = 1
this._STD = new STD(1, 9 * Math.pow(this.dampingNumber, 2) / 40, this.dampingNumber)
this._friction = new Friction(1, this.frictionNumber)
this._declineX = new Decline()
this._declineY = new Decline()
this.__touchInfo = {
historyX: [0, 0],
historyY: [0, 0],
historyT: [0, 0]
}
},
mounted: function () {
this.touchtrack(this.$el, '_onTrack')
this.setParent()
this._friction.reconfigure(1, this.frictionNumber)
this._STD.reconfigure(1, 9 * Math.pow(this.dampingNumber, 2) / 40, this.dampingNumber)
this.$el.style.transformOrigin = 'center'
},
methods: {
_getPx (val) {
if (/\d+[ur]px$/i.test(val)) {
return uni.upx2px(parseFloat(val))
}
return Number(val) || 0
},
_setX: function (val) {
if (this.xMove) {
if (val + this._scaleOffset.x === this._translateX) {
return this._translateX
} else {
if (this._SFA) {
this._SFA.cancel()
}
this._animationTo(val + this._scaleOffset.x, this.ySync + this._scaleOffset.y, this._scale)
}
}
return val
},
_setY: function (val) {
if (this.yMove) {
if (val + this._scaleOffset.y === this._translateY) {
return this._translateY
} else {
if (this._SFA) {
this._SFA.cancel()
}
this._animationTo(this.xSync + this._scaleOffset.x, val + this._scaleOffset.y, this._scale)
}
}
return val
},
_setScaleMinOrMax: function () {
if (!this.scale) {
return false
}
this._updateScale(this._scale, true)
this._updateOldScale(this._scale)
},
_setScaleValue: function (scale) {
if (!this.scale) {
return false
}
scale = this._adjustScale(scale)
this._updateScale(scale, true)
this._updateOldScale(scale)
return scale
},
__handleTouchStart: function () {
if (!this._isScaling) {
if (!this.disabled) {
disableScrollBounce({
disable: true
})
if (this._FA) {
this._FA.cancel()
}
if (this._SFA) {
this._SFA.cancel()
}
this.__touchInfo.historyX = [0, 0]
this.__touchInfo.historyY = [0, 0]
this.__touchInfo.historyT = [0, 0]
if (this.xMove) {
this.__baseX = this._translateX
}
if (this.yMove) {
this.__baseY = this._translateY
}
this.$el.style.willChange = 'transform'
this._checkCanMove = null
this._firstMoveDirection = null
this._isTouching = true
}
}
},
__handleTouchMove: function (event) {
var self = this
if (!this._isScaling && !this.disabled && this._isTouching) {
let x = this._translateX
let y = this._translateY
if (this._firstMoveDirection === null) {
this._firstMoveDirection = Math.abs(event.detail.dx / event.detail.dy) > 1 ? 'htouchmove' : 'vtouchmove'
}
if (this.xMove) {
x = event.detail.dx + this.__baseX
this.__touchInfo.historyX.shift()
this.__touchInfo.historyX.push(x)
if (!this.yMove && this._checkCanMove === null) {
this._checkCanMove = Math.abs(event.detail.dx / event.detail.dy) < 1
}
}
if (this.yMove) {
y = event.detail.dy + this.__baseY
this.__touchInfo.historyY.shift()
this.__touchInfo.historyY.push(y)
if (!this.xMove && this._checkCanMove === null) {
this._checkCanMove = Math.abs(event.detail.dy / event.detail.dx) < 1
}
}
this.__touchInfo.historyT.shift()
this.__touchInfo.historyT.push(event.detail.timeStamp)
if (!this._checkCanMove) {
event.preventDefault()
let source = 'touch'
if (x < this.minX) {
if (this.outOfBounds) {
source = 'touch-out-of-bounds'
x = this.minX - this._declineX.x(this.minX - x)
} else {
x = this.minX
}
} else if (x > this.maxX) {
if (this.outOfBounds) {
source = 'touch-out-of-bounds'
x = this.maxX + this._declineX.x(x - this.maxX)
} else {
x = this.maxX
}
}
if (y < this.minY) {
if (this.outOfBounds) {
source = 'touch-out-of-bounds'
y = this.minY - this._declineY.x(this.minY - y)
} else {
y = this.minY
}
} else {
if (y > this.maxY) {
if (this.outOfBounds) {
source = 'touch-out-of-bounds'
y = this.maxY + this._declineY.x(y - this.maxY)
} else {
y = this.maxY
}
}
}
_requestAnimationFrame(function () {
self._setTransform(x, y, self._scale, source)
})
}
}
},
__handleTouchEnd: function () {
var self = this
if (!this._isScaling && !this.disabled && this._isTouching) {
disableScrollBounce({
disable: false
})
this.$el.style.willChange = 'auto'
this._isTouching = false
if (!this._checkCanMove && !this._revise('out-of-bounds') && this.inertia) {
const xv = 1000 * (this.__touchInfo.historyX[1] - this.__touchInfo.historyX[0]) / (this.__touchInfo.historyT[1] - this.__touchInfo.historyT[0])
const yv = 1000 * (this.__touchInfo.historyY[1] - this.__touchInfo.historyY[0]) / (this.__touchInfo.historyT[1] - this.__touchInfo.historyT[0])
this._friction.setV(xv, yv)
this._friction.setS(this._translateX, this._translateY)
const x0 = this._friction.delta().x
const y0 = this._friction.delta().y
let x = x0 + this._translateX
let y = y0 + this._translateY
if (x < this.minX) {
x = this.minX
y = this._translateY + (this.minX - this._translateX) * y0 / x0
} else {
if (x > this.maxX) {
x = this.maxX
y = this._translateY + (this.maxX - this._translateX) * y0 / x0
}
}
if (y < this.minY) {
y = this.minY
x = this._translateX + (this.minY - this._translateY) * x0 / y0
} else {
if (y > this.maxY) {
y = this.maxY
x = this._translateX + (this.maxY - this._translateY) * x0 / y0
}
}
this._friction.setEnd(x, y)
this._FA = g(this._friction, function () {
var t = self._friction.s()
var x = t.x
var y = t.y
self._setTransform(x, y, self._scale, 'friction')
}, function () {
self._FA.cancel()
})
}
}
},
_onTrack: function (event) {
switch (event.detail.state) {
case 'start':
this.__handleTouchStart()
break
case 'move':
this.__handleTouchMove(event)
break
case 'end':
this.__handleTouchEnd()
}
},
_getLimitXY: function (x, y) {
var outOfBounds = false
if (x > this.maxX) {
x = this.maxX
outOfBounds = true
} else {
if (x < this.minX) {
x = this.minX
outOfBounds = true
}
}
if (y > this.maxY) {
y = this.maxY
outOfBounds = true
} else {
if (y < this.minY) {
y = this.minY
outOfBounds = true
}
}
return {
x,
y,
outOfBounds
}
},
setParent: function () {
if (!this.$parent._isMounted) {
return
}
if (this._FA) {
this._FA.cancel()
}
if (this._SFA) {
this._SFA.cancel()
}
var scale = this.scale ? this.scaleValueSync : 1
this._updateOffset()
this._updateWH(scale)
this._updateBoundary()
this._translateX = this.xSync + this._scaleOffset.x
this._translateY = this.ySync + this._scaleOffset.y
var limitXY = this._getLimitXY(this._translateX, this._translateY)
var x = limitXY.x
var y = limitXY.y
this._setTransform(x, y, scale, '', true)
this._updateOldScale(scale)
},
_updateOffset: function () {
this._offset.x = p(this.$el, this.$parent.$el)
this._offset.y = f(this.$el, this.$parent.$el)
},
_updateWH: function (scale) {
scale = scale || this._scale
scale = this._adjustScale(scale)
var rect = this.$el.getBoundingClientRect()
this.height = rect.height / this._scale
this.width = rect.width / this._scale
var height = this.height * scale
var width = this.width * scale
this._scaleOffset.x = (width - this.width) / 2
this._scaleOffset.y = (height - this.height) / 2
},
_updateBoundary: function () {
var x = 0 - this._offset.x + this._scaleOffset.x
var width = this.$parent.width - this.width - this._offset.x - this._scaleOffset.x
this.minX = Math.min(x, width)
this.maxX = Math.max(x, width)
var y = 0 - this._offset.y + this._scaleOffset.y
var height = this.$parent.height - this.height - this._offset.y - this._scaleOffset.y
this.minY = Math.min(y, height)
this.maxY = Math.max(y, height)
},
_beginScale: function () {
this._isScaling = true
},
_endScale: function () {
this._isScaling = false
this._updateOldScale(this._scale)
},
_setScale: function (scale) {
if (this.scale) {
scale = this._adjustScale(scale)
scale = this._oldScale * scale
this._beginScale()
this._updateScale(scale)
}
},
_updateScale: function (scale, animat) {
var self = this
if (this.scale) {
scale = this._adjustScale(scale)
this._updateWH(scale)
this._updateBoundary()
const limitXY = this._getLimitXY(this._translateX, this._translateY)
const x = limitXY.x
const y = limitXY.y
if (animat) {
this._animationTo(x, y, scale, '', true, true)
} else {
_requestAnimationFrame(function () {
self._setTransform(x, y, scale, '', true, true)
})
}
}
},
_updateOldScale: function (scale) {
this._oldScale = scale
},
_adjustScale: function (scale) {
scale = Math.max(0.5, this.scaleMinNumber, scale)
scale = Math.min(10, this.scaleMaxNumber, scale)
return scale
},
_animationTo: function (x, y, scale, source, r, o) {
var self = this
if (this._FA) {
this._FA.cancel()
}
if (this._SFA) {
this._SFA.cancel()
}
if (!this.xMove) {
x = this._translateX
}
if (!this.yMove) {
y = this._translateY
}
if (!this.scale) {
scale = this._scale
}
var limitXY = this._getLimitXY(x, y)
x = limitXY.x
y = limitXY.y
if (!this.animation) {
this._setTransform(x, y, scale, source, r, o)
return
}
this._STD._springX._solution = null
this._STD._springY._solution = null
this._STD._springScale._solution = null
this._STD._springX._endPosition = this._translateX
this._STD._springY._endPosition = this._translateY
this._STD._springScale._endPosition = this._scale
this._STD.setEnd(x, y, scale, 1)
this._SFA = g(this._STD, function () {
var data = self._STD.x()
var x = data.x
var y = data.y
var scale = data.scale
self._setTransform(x, y, scale, source, r, o)
}, function () {
self._SFA.cancel()
})
},
_revise: function (source) {
var limitXY = this._getLimitXY(this._translateX, this._translateY)
var x = limitXY.x
var y = limitXY.y
var outOfBounds = limitXY.outOfBounds
if (outOfBounds) {
this._animationTo(x, y, this._scale, source)
}
return outOfBounds
},
_setTransform: function (x, y, scale, source = '', r, o) {
if (!(x !== null && x.toString() !== 'NaN' && typeof x === 'number')) {
x = this._translateX || 0
}
if (!(y !== null && y.toString() !== 'NaN' && typeof y === 'number')) {
y = this._translateY || 0
}
x = Number(x.toFixed(1))
y = Number(y.toFixed(1))
scale = Number(scale.toFixed(1))
if (!(this._translateX === x && this._translateY === y)) {
if (!r) {
this.$trigger('change', {}, {
x: v(x, this._scaleOffset.x),
y: v(y, this._scaleOffset.y),
source: source
})
}
}
if (!this.scale) {
scale = this._scale
}
scale = this._adjustScale(scale)
scale = +scale.toFixed(3)
if (o && scale !== this._scale) {
this.$trigger('scale', {}, {
x: x,
y: y,
scale: scale
})
}
var transform = 'translateX(' + x + 'px) translateY(' + y + 'px) translateZ(0px) scale(' + scale + ')'
this.$el.style.transform = transform
this.$el.style.webkitTransform = transform
this._translateX = x
this._translateY = y
this._scale = scale
}
}
}
</script>
\ No newline at end of file
...@@ -26,10 +26,10 @@ const addListenerToElement = function ( ...@@ -26,10 +26,10 @@ const addListenerToElement = function (
} }
) )
} }
type State = 'start' | 'move' | 'end' | 'cancel'
type TouchOrMouseEvent = TouchEvent | MouseEvent type TouchOrMouseEvent = TouchEvent | MouseEvent
type Detail = { type Detail = {
state: any state: State
x0: number x0: number
y0: number y0: number
dx: number dx: number
...@@ -67,7 +67,7 @@ export function useTouchtrack( ...@@ -67,7 +67,7 @@ export function useTouchtrack(
let y1 = 0 let y1 = 0
const fn = function ( const fn = function (
$event: TouchOrMouseEvent, $event: TouchOrMouseEvent,
state: any, state: State,
x: number, x: number,
y: number y: number
) { ) {
......
此差异已折叠。
此差异已折叠。
...@@ -52,6 +52,7 @@ export default /*#__PURE__*/ defineComponent({ ...@@ -52,6 +52,7 @@ export default /*#__PURE__*/ defineComponent({
{...$excludeAttrs.value} {...$excludeAttrs.value}
ref={rootRef} ref={rootRef}
> >
{/* @ts-ignore */}
<ResizeSensor onResize={_resize} /> <ResizeSensor onResize={_resize} />
</uni-web-view> </uni-web-view>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册