fix(ui): 拖动松手从手指位置平滑契合到目标 tab
- 拖动状态与位置状态分离 - 松手先恢复过渡但保留手指位置,下一帧再吸附到目标 tab,避免从旧位置跳变
This commit is contained in:
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[777],{7914:function(n,e,t){var a=t(7842),c=t(5544),r=t(118),u=t(758),i=t.n(u),o=t(6540),s=t(1457),l=t(9703),f=t(6606),h=t(8199);function g(n,e,t){return Number.isFinite(n)?Math.min(t,Math.max(e,n)):e}function v(n){return g(n,0,100)}function d(n,e){return!Number.isFinite(n)||e<=0?0:g(Math.floor(v(n)/100*e),0,e-1)}function m(n,e,t){return Math.abs(e-n)>=t}function p(n){var e=n/750;return{left:36*e,width:Math.max(0,n-72*e)}}var b=t(4848),x=[{pagePath:"/pages/index/index",label:"\u9996\u9875",icon:(0,h.uq)("/icon/\u9996\u9875.svg"),iconActive:(0,h.uq)("/icon/\u9996\u9875-fill.svg")},{pagePath:"/pages/shop/index",label:"\u5546\u54c1",icon:(0,h.uq)("/icon/\u5546\u54c1.svg"),iconActive:(0,h.uq)("/icon/\u5546\u54c1-fill.svg")},{pagePath:"/pages/designList/index",label:"\u8bbe\u8ba1\u6e05\u5355",icon:(0,h.uq)("/icon/\u8c03\u8272\u76d8.svg"),iconActive:(0,h.uq)("/icon/\u8c03\u8272\u76d8-fill.svg")},{pagePath:"/pages/orders/index",label:"\u8ba2\u5355",icon:(0,h.uq)("/icon/\u5305\u88f9.svg"),iconActive:(0,h.uq)("/icon/\u5305\u88f9-fill.svg")},{pagePath:"/pages/profile/index",label:"\u6211\u7684",icon:(0,h.uq)("/icon/\u4e2a\u4eba.svg"),iconActive:(0,h.uq)("/icon/\u4e2a\u4eba-fill.svg")}],C=100/x.length,S=12;function P(n){var e=x.findIndex(function(e){return n.endsWith(e.pagePath)});return x[e>=0?e:0].pagePath}function q(){var n=i().getCurrentInstance().router,e=n&&n.path?n.path:"pages/index/index";return P(e)}function N(){var n=(0,o.useState)((0,s.eW)((0,s.O4)())),e=(0,c.A)(n,2),t=e[0],a=e[1],u=(0,o.useState)(q),h=(0,c.A)(u,2),g=h[0],N=h[1],T=(0,o.useState)(null),w=(0,c.A)(T,2),j=w[0],A=w[1],M=(0,o.useRef)(null),E=(0,o.useRef)(0),I=(0,o.useRef)(null);(0,o.useEffect)(function(){var n=function(n){return a(n)};return i().eventCenter.on(l.Wo,n),function(){i().eventCenter.off(l.Wo,n)}},[]),(0,o.useEffect)(function(){var n=function(n){!n||"dark"!==n.theme&&"light"!==n.theme||((0,s.Rq)(n.theme),"auto"===(0,s.O4)()&&a(n.theme))},e=i();return e.onThemeChange&&e.onThemeChange(n),function(){e.offThemeChange&&e.offThemeChange(n)}},[]),(0,o.useEffect)(function(){var n=function(n){var e=i().getCurrentInstance().router,t=e&&e.path?e.path:"pages/index/index";N(P(n||t))};return i().eventCenter.on("tabBarChange",n),function(){i().eventCenter.off("tabBarChange",n)}},[]),(0,o.useEffect)(function(){(0,f.G)(t)},[t]);var W=function(n){N(P(n)),i().switchTab({url:n}),i().eventCenter.trigger("tabBarChange",n)},O=function(n){var e=n.touches&&n.touches[0];if(e){var t=p(i().getSystemInfoSync().windowWidth);I.current=t,E.current=e.clientX,M.current=null,A(null)}},R=function(n){var e=I.current,t=n.touches&&n.touches[0];if(e&&t&&(null!==M.current||m(E.current,t.clientX,S))){var a=v((t.clientX-e.left)/e.width*100);M.current=a,A(a)}},X=function(n){var e=I.current;if(e){var t;if(null!==M.current)t=d(M.current,x.length);else{var a=n.changedTouches&&n.changedTouches[0],c=a?a.clientX:E.current;t=d(v((c-e.left)/e.width*100),x.length)}var r=x[t];r&&r.pagePath!==g&&W(r.pagePath),I.current=null,E.current=0,M.current=null,A(null)}},k=function(){I.current=null,E.current=0,M.current=null,A(null)},y=Math.max(0,x.findIndex(function(n){return n.pagePath===g}));return(0,b.jsxs)(r.Ss,{className:"tab-bar-root theme-".concat(t),children:[(0,b.jsx)(r.Ss,{className:"tab-bar-gradient"}),(0,b.jsxs)(r.Ss,{className:"custom-tab-bar theme-".concat(t),onTouchStart:O,onTouchMove:R,onTouchEnd:X,onTouchCancel:k,catchMove:!0,children:[(0,b.jsx)(r.Ss,{className:"tab-bar-caustic"}),(0,b.jsx)(r.Ss,{className:"tab-bar-rim"}),(0,b.jsx)(r.Ss,{className:"tab-indicator ".concat(null!==j?"dragging":""),style:{left:"".concat(null===j?(y+.5)*C:j,"%")}}),x.map(function(n){var e=n.pagePath===g;return(0,b.jsxs)(r.Ss,{className:"tab-item ".concat(e?"active":""),children:[(0,b.jsx)(r._V,{className:"tab-icon-img ".concat(e?"active":""),src:e?n.iconActive:n.icon,mode:"aspectFit"}),(0,b.jsx)(r.EY,{className:"tab-label",children:n.label})]},n.pagePath)})]})]})}Component((0,a.createComponentConfig)(N,"custom-tab-bar/index"))}},function(n){var e=function(e){return n(n.s=e)};n.O(0,[907,96,76],function(){return e(7914)});n.O()}]);
|
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[777],{7914:function(e,n,t){var a=t(7842),c=t(5544),r=t(118),u=t(758),i=t.n(u),o=t(6540),s=t(1457),l=t(9703),f=t(6606),h=t(8199);function g(e,n,t){return Number.isFinite(e)?Math.min(t,Math.max(n,e)):n}function v(e){return g(e,0,100)}function d(e,n){return!Number.isFinite(e)||n<=0?0:g(Math.floor(v(e)/100*n),0,n-1)}function m(e,n,t){return Math.abs(n-e)>=t}function p(e){var n=e/750;return{left:36*n,width:Math.max(0,e-72*n)}}var b=t(4848),x=[{pagePath:"/pages/index/index",label:"\u9996\u9875",icon:(0,h.uq)("/icon/\u9996\u9875.svg"),iconActive:(0,h.uq)("/icon/\u9996\u9875-fill.svg")},{pagePath:"/pages/shop/index",label:"\u5546\u54c1",icon:(0,h.uq)("/icon/\u5546\u54c1.svg"),iconActive:(0,h.uq)("/icon/\u5546\u54c1-fill.svg")},{pagePath:"/pages/designList/index",label:"\u8bbe\u8ba1\u6e05\u5355",icon:(0,h.uq)("/icon/\u8c03\u8272\u76d8.svg"),iconActive:(0,h.uq)("/icon/\u8c03\u8272\u76d8-fill.svg")},{pagePath:"/pages/orders/index",label:"\u8ba2\u5355",icon:(0,h.uq)("/icon/\u5305\u88f9.svg"),iconActive:(0,h.uq)("/icon/\u5305\u88f9-fill.svg")},{pagePath:"/pages/profile/index",label:"\u6211\u7684",icon:(0,h.uq)("/icon/\u4e2a\u4eba.svg"),iconActive:(0,h.uq)("/icon/\u4e2a\u4eba-fill.svg")}],C=100/x.length,S=12;function P(e){var n=x.findIndex(function(n){return e.endsWith(n.pagePath)});return x[n>=0?n:0].pagePath}function T(){var e=i().getCurrentInstance().router,n=e&&e.path?e.path:"pages/index/index";return P(n)}function q(){var e=(0,o.useState)((0,s.eW)((0,s.O4)())),n=(0,c.A)(e,2),t=n[0],a=n[1],u=(0,o.useState)(T),h=(0,c.A)(u,2),g=h[0],q=h[1],N=(0,o.useState)(null),w=(0,c.A)(N,2),A=w[0],j=w[1],M=(0,o.useState)(!1),E=(0,c.A)(M,2),I=E[0],R=E[1],W=(0,o.useRef)(!1),k=(0,o.useRef)(null),O=(0,o.useRef)(0),X=(0,o.useRef)(null);(0,o.useEffect)(function(){var e=function(e){return a(e)};return i().eventCenter.on(l.Wo,e),function(){i().eventCenter.off(l.Wo,e)}},[]),(0,o.useEffect)(function(){var e=function(e){!e||"dark"!==e.theme&&"light"!==e.theme||((0,s.Rq)(e.theme),"auto"===(0,s.O4)()&&a(e.theme))},n=i();return n.onThemeChange&&n.onThemeChange(e),function(){n.offThemeChange&&n.offThemeChange(e)}},[]),(0,o.useEffect)(function(){var e=function(e){var n=i().getCurrentInstance().router,t=n&&n.path?n.path:"pages/index/index";q(P(e||t))};return i().eventCenter.on("tabBarChange",e),function(){i().eventCenter.off("tabBarChange",e)}},[]),(0,o.useEffect)(function(){(0,f.G)(t)},[t]);var y=function(e){q(P(e)),i().switchTab({url:e}),i().eventCenter.trigger("tabBarChange",e)},B=function(e){var n=e.touches&&e.touches[0];if(n){var t=p(i().getSystemInfoSync().windowWidth);X.current=t,O.current=n.clientX,W.current=!1,R(!1),k.current=null,j(null)}},F=function(e){var n=X.current,t=e.touches&&e.touches[0];if(n&&t){if(!W.current){if(!m(O.current,t.clientX,S))return;W.current=!0,R(!0)}var a=v((t.clientX-n.left)/n.width*100);k.current=a,j(a)}},J=function(e){var n=X.current;if(n){var t;if(W.current){var a;t=d(null!==(a=k.current)&&void 0!==a?a:0,x.length),W.current=!1,R(!1),i().nextTick(function(){k.current=null,j(null)})}else{var c=e.changedTouches&&e.changedTouches[0],r=c?c.clientX:O.current;t=d(v((r-n.left)/n.width*100),x.length)}var u=x[t];u&&u.pagePath!==g&&y(u.pagePath),X.current=null,O.current=0}},G=function(){X.current=null,O.current=0,W.current=!1,R(!1),k.current=null,j(null)},L=Math.max(0,x.findIndex(function(e){return e.pagePath===g}));return(0,b.jsxs)(r.Ss,{className:"tab-bar-root theme-".concat(t),children:[(0,b.jsx)(r.Ss,{className:"tab-bar-gradient"}),(0,b.jsxs)(r.Ss,{className:"custom-tab-bar theme-".concat(t),onTouchStart:B,onTouchMove:F,onTouchEnd:J,onTouchCancel:G,catchMove:!0,children:[(0,b.jsx)(r.Ss,{className:"tab-bar-caustic"}),(0,b.jsx)(r.Ss,{className:"tab-bar-rim"}),(0,b.jsx)(r.Ss,{className:"tab-indicator ".concat(I?"dragging":""),style:{left:"".concat(null===A?(L+.5)*C:A,"%")}}),x.map(function(e){var n=e.pagePath===g;return(0,b.jsxs)(r.Ss,{className:"tab-item ".concat(n?"active":""),children:[(0,b.jsx)(r._V,{className:"tab-icon-img ".concat(n?"active":""),src:n?e.iconActive:e.icon,mode:"aspectFit"}),(0,b.jsx)(r.EY,{className:"tab-label",children:e.label})]},e.pagePath)})]})]})}Component((0,a.createComponentConfig)(q,"custom-tab-bar/index"))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[907,96,76],function(){return n(7914)});e.O()}]);
|
||||||
@@ -41,6 +41,8 @@ export default function CustomTabBar() {
|
|||||||
// 微信自定义 tabBar 不会自动感知 page 路由变化,用 state 保持选中态响应式
|
// 微信自定义 tabBar 不会自动感知 page 路由变化,用 state 保持选中态响应式
|
||||||
const [activeTab, setActiveTab] = useState<string>(currentTabPath)
|
const [activeTab, setActiveTab] = useState<string>(currentTabPath)
|
||||||
const [dragPercent, setDragPercent] = useState<number | null>(null)
|
const [dragPercent, setDragPercent] = useState<number | null>(null)
|
||||||
|
const [isDragging, setIsDragging] = useState<boolean>(false)
|
||||||
|
const isDraggingRef = useRef<boolean>(false)
|
||||||
const dragPercentRef = useRef<number | null>(null)
|
const dragPercentRef = useRef<number | null>(null)
|
||||||
const startXRef = useRef<number>(0)
|
const startXRef = useRef<number>(0)
|
||||||
const barFrameRef = useRef<{ left: number; width: number } | null>(null)
|
const barFrameRef = useRef<{ left: number; width: number } | null>(null)
|
||||||
@@ -105,6 +107,8 @@ export default function CustomTabBar() {
|
|||||||
const frame = getTabBarFrame(Taro.getSystemInfoSync().windowWidth)
|
const frame = getTabBarFrame(Taro.getSystemInfoSync().windowWidth)
|
||||||
barFrameRef.current = frame
|
barFrameRef.current = frame
|
||||||
startXRef.current = touch.clientX
|
startXRef.current = touch.clientX
|
||||||
|
isDraggingRef.current = false
|
||||||
|
setIsDragging(false)
|
||||||
dragPercentRef.current = null
|
dragPercentRef.current = null
|
||||||
setDragPercent(null)
|
setDragPercent(null)
|
||||||
}
|
}
|
||||||
@@ -113,8 +117,10 @@ export default function CustomTabBar() {
|
|||||||
const frame = barFrameRef.current
|
const frame = barFrameRef.current
|
||||||
const touch = e.touches && e.touches[0]
|
const touch = e.touches && e.touches[0]
|
||||||
if (!frame || !touch) return
|
if (!frame || !touch) return
|
||||||
if (dragPercentRef.current === null && !hasDragged(startXRef.current, touch.clientX, DRAG_THRESHOLD_PX)) {
|
if (!isDraggingRef.current) {
|
||||||
return
|
if (!hasDragged(startXRef.current, touch.clientX, DRAG_THRESHOLD_PX)) return
|
||||||
|
isDraggingRef.current = true
|
||||||
|
setIsDragging(true)
|
||||||
}
|
}
|
||||||
const nextPercent = clampPercent(((touch.clientX - frame.left) / frame.width) * 100)
|
const nextPercent = clampPercent(((touch.clientX - frame.left) / frame.width) * 100)
|
||||||
dragPercentRef.current = nextPercent
|
dragPercentRef.current = nextPercent
|
||||||
@@ -125,8 +131,14 @@ export default function CustomTabBar() {
|
|||||||
const frame = barFrameRef.current
|
const frame = barFrameRef.current
|
||||||
if (!frame) return
|
if (!frame) return
|
||||||
let targetIndex: number
|
let targetIndex: number
|
||||||
if (dragPercentRef.current !== null) {
|
if (isDraggingRef.current) {
|
||||||
targetIndex = tabIndexAtPercent(dragPercentRef.current, TABS.length)
|
targetIndex = tabIndexAtPercent(dragPercentRef.current ?? 0, TABS.length)
|
||||||
|
isDraggingRef.current = false
|
||||||
|
setIsDragging(false)
|
||||||
|
Taro.nextTick(() => {
|
||||||
|
dragPercentRef.current = null
|
||||||
|
setDragPercent(null)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
const touch = e.changedTouches && e.changedTouches[0]
|
const touch = e.changedTouches && e.changedTouches[0]
|
||||||
const x = touch ? touch.clientX : startXRef.current
|
const x = touch ? touch.clientX : startXRef.current
|
||||||
@@ -136,13 +148,13 @@ export default function CustomTabBar() {
|
|||||||
if (target && target.pagePath !== activeTab) switchTab(target.pagePath)
|
if (target && target.pagePath !== activeTab) switchTab(target.pagePath)
|
||||||
barFrameRef.current = null
|
barFrameRef.current = null
|
||||||
startXRef.current = 0
|
startXRef.current = 0
|
||||||
dragPercentRef.current = null
|
|
||||||
setDragPercent(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelDrag = () => {
|
const cancelDrag = () => {
|
||||||
barFrameRef.current = null
|
barFrameRef.current = null
|
||||||
startXRef.current = 0
|
startXRef.current = 0
|
||||||
|
isDraggingRef.current = false
|
||||||
|
setIsDragging(false)
|
||||||
dragPercentRef.current = null
|
dragPercentRef.current = null
|
||||||
setDragPercent(null)
|
setDragPercent(null)
|
||||||
}
|
}
|
||||||
@@ -163,7 +175,7 @@ export default function CustomTabBar() {
|
|||||||
<View className='tab-bar-caustic' />
|
<View className='tab-bar-caustic' />
|
||||||
<View className='tab-bar-rim' />
|
<View className='tab-bar-rim' />
|
||||||
<View
|
<View
|
||||||
className={`tab-indicator ${dragPercent !== null ? 'dragging' : ''}`}
|
className={`tab-indicator ${isDragging ? 'dragging' : ''}`}
|
||||||
style={{ left: dragPercent === null ? `${(activeIndex + 0.5) * TAB_CENTER_STEP}%` : `${dragPercent}%` }}
|
style={{ left: dragPercent === null ? `${(activeIndex + 0.5) * TAB_CENTER_STEP}%` : `${dragPercent}%` }}
|
||||||
/>
|
/>
|
||||||
{TABS.map((tab) => {
|
{TABS.map((tab) => {
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import path from 'node:path'
|
||||||
import { clampPercent, getTabBarFrame, hasDragged, tabIndexAtPercent } from '../src/custom-tab-bar/tabBarGeometry.js'
|
import { clampPercent, getTabBarFrame, hasDragged, tabIndexAtPercent } from '../src/custom-tab-bar/tabBarGeometry.js'
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
|
||||||
test('maps a drag percent to the tab under the finger', () => {
|
test('maps a drag percent to the tab under the finger', () => {
|
||||||
assert.equal(tabIndexAtPercent(10, 5), 0)
|
assert.equal(tabIndexAtPercent(10, 5), 0)
|
||||||
assert.equal(tabIndexAtPercent(30, 5), 1)
|
assert.equal(tabIndexAtPercent(30, 5), 1)
|
||||||
@@ -28,3 +33,11 @@ test('derives the floating bar frame from window width', () => {
|
|||||||
assert.deepEqual(getTabBarFrame(375), { left: 18, width: 339 })
|
assert.deepEqual(getTabBarFrame(375), { left: 18, width: 339 })
|
||||||
assert.deepEqual(getTabBarFrame(750), { left: 36, width: 678 })
|
assert.deepEqual(getTabBarFrame(750), { left: 36, width: 678 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('enables the slide transition before snapping the indicator to the target', () => {
|
||||||
|
const source = readFileSync(path.join(root, 'src/custom-tab-bar/index.tsx'), 'utf8')
|
||||||
|
assert.match(
|
||||||
|
source,
|
||||||
|
/isDraggingRef\.current = false\s+setIsDragging\(false\)\s+Taro\.nextTick\(\(\) => \{[\s\S]*?setDragPercent\(null\)/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user