fix: correct home mask threshold after early scroll
This commit is contained in:
@@ -261,7 +261,6 @@
|
|||||||
z-index: 40;
|
z-index: 40;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
will-change: opacity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-light .home-top-mask {
|
.theme-light .home-top-mask {
|
||||||
|
|||||||
+22
-14
@@ -1,6 +1,6 @@
|
|||||||
import { View, Text, Image, Input, Button, Swiper, SwiperItem } from '@tarojs/components'
|
import { View, Text, Image, Input, Button, Swiper, SwiperItem } from '@tarojs/components'
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { CATEGORIES } from '../../utils/productConfig'
|
import { CATEGORIES } from '../../utils/productConfig'
|
||||||
import { assetUrl } from '../../utils/asset'
|
import { assetUrl } from '../../utils/asset'
|
||||||
@@ -8,7 +8,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
|||||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||||
import { getTopMaskProgress } from '../../utils/homeTopMask'
|
import { getTopMaskProgress, getTopMaskStartY } from '../../utils/homeTopMask'
|
||||||
|
|
||||||
// 成品展示配图(从 img 里取对应实物图)
|
// 成品展示配图(从 img 里取对应实物图)
|
||||||
const SHOWCASE_LIST = [
|
const SHOWCASE_LIST = [
|
||||||
@@ -72,11 +72,15 @@ export default function Index() {
|
|||||||
const [searchKey, setSearchKey] = useState('')
|
const [searchKey, setSearchKey] = useState('')
|
||||||
|
|
||||||
const [topMaskOpacity, setTopMaskOpacity] = useState(0)
|
const [topMaskOpacity, setTopMaskOpacity] = useState(0)
|
||||||
const titleMetricsRef = useRef({ startY: 0, rangeY: 48 })
|
const fallbackTitleMetrics = useMemo(() => ({
|
||||||
|
startY: Math.max(0, safe.headerPaddingTop - safe.statusBarHeight),
|
||||||
|
rangeY: Math.max(48, safe.headerPaddingTop * 0.45)
|
||||||
|
}), [safe.headerPaddingTop, safe.statusBarHeight])
|
||||||
|
const titleMetricsRef = useRef(fallbackTitleMetrics)
|
||||||
const scrollFrameRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const scrollFrameRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const latestScrollTopRef = useRef(0)
|
const latestScrollTopRef = useRef(0)
|
||||||
|
|
||||||
const applyTopMaskProgress = (scrollTop: number) => {
|
const applyTopMaskProgress = useCallback((scrollTop: number) => {
|
||||||
latestScrollTopRef.current = scrollTop
|
latestScrollTopRef.current = scrollTop
|
||||||
if (scrollFrameRef.current !== null) {
|
if (scrollFrameRef.current !== null) {
|
||||||
return
|
return
|
||||||
@@ -87,29 +91,33 @@ export default function Index() {
|
|||||||
const { startY, rangeY } = titleMetricsRef.current
|
const { startY, rangeY } = titleMetricsRef.current
|
||||||
setTopMaskOpacity(getTopMaskProgress({ scrollTop: latestScrollTopRef.current, startY, rangeY }))
|
setTopMaskOpacity(getTopMaskProgress({ scrollTop: latestScrollTopRef.current, startY, rangeY }))
|
||||||
}, 16)
|
}, 16)
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
let measureTimer: ReturnType<typeof setTimeout> | null = null
|
let measureTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
titleMetricsRef.current = fallbackTitleMetrics
|
||||||
|
|
||||||
|
type HomeHeaderRect = Taro.NodesRef.BoundingClientRectCallbackResult
|
||||||
|
|
||||||
const measureTitle = () => {
|
const measureTitle = () => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
Taro.createSelectorQuery()
|
Taro.createSelectorQuery()
|
||||||
.select('.home-header')
|
.select('.home-header')
|
||||||
.boundingClientRect((rect: any) => {
|
.boundingClientRect((rect: HomeHeaderRect | HomeHeaderRect[]) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
if (!rect || typeof rect.bottom !== 'number' || typeof rect.height !== 'number') {
|
if (!rect || Array.isArray(rect) || typeof rect.bottom !== 'number' || typeof rect.height !== 'number') {
|
||||||
const fallbackRange = Math.max(48, safe.headerPaddingTop * 0.45)
|
titleMetricsRef.current = fallbackTitleMetrics
|
||||||
titleMetricsRef.current = {
|
|
||||||
startY: Math.max(0, safe.headerPaddingTop - safe.statusBarHeight),
|
|
||||||
rangeY: fallbackRange
|
|
||||||
}
|
|
||||||
applyTopMaskProgress(latestScrollTopRef.current)
|
applyTopMaskProgress(latestScrollTopRef.current)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const startY = Math.max(0, rect.bottom - safe.statusBarHeight - 8)
|
const startY = getTopMaskStartY({
|
||||||
|
viewportBottom: rect.bottom,
|
||||||
|
scrollTop: latestScrollTopRef.current,
|
||||||
|
statusBarHeight: safe.statusBarHeight
|
||||||
|
})
|
||||||
const rangeY = Math.max(32, rect.height * 0.7)
|
const rangeY = Math.max(32, rect.height * 0.7)
|
||||||
titleMetricsRef.current = { startY, rangeY }
|
titleMetricsRef.current = { startY, rangeY }
|
||||||
applyTopMaskProgress(latestScrollTopRef.current)
|
applyTopMaskProgress(latestScrollTopRef.current)
|
||||||
@@ -132,7 +140,7 @@ export default function Index() {
|
|||||||
}
|
}
|
||||||
scrollFrameRef.current = null
|
scrollFrameRef.current = null
|
||||||
}
|
}
|
||||||
}, [safe.headerPaddingTop, safe.statusBarHeight])
|
}, [applyTopMaskProgress, fallbackTitleMetrics])
|
||||||
|
|
||||||
Taro.usePageScroll((e) => {
|
Taro.usePageScroll((e) => {
|
||||||
applyTopMaskProgress(e.scrollTop)
|
applyTopMaskProgress(e.scrollTop)
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Convert a viewport-relative header bottom into a document-relative mask
|
||||||
|
* threshold. Selector queries report coordinates relative to the current
|
||||||
|
* viewport, so the scroll offset at measurement time must be added back.
|
||||||
|
*/
|
||||||
|
export function getTopMaskStartY({ viewportBottom, scrollTop, statusBarHeight }) {
|
||||||
|
const safeViewportBottom = Number.isFinite(viewportBottom) ? viewportBottom : 0
|
||||||
|
const safeScrollTop = Number.isFinite(scrollTop) ? scrollTop : 0
|
||||||
|
const safeStatusBarHeight = Number.isFinite(statusBarHeight) ? statusBarHeight : 0
|
||||||
|
|
||||||
|
return Math.max(0, safeViewportBottom + safeScrollTop - safeStatusBarHeight - 8)
|
||||||
|
}
|
||||||
|
|
||||||
export function getTopMaskProgress({ scrollTop, startY, rangeY }) {
|
export function getTopMaskProgress({ scrollTop, startY, rangeY }) {
|
||||||
const safeScrollTop = Number.isFinite(scrollTop) ? scrollTop : 0
|
const safeScrollTop = Number.isFinite(scrollTop) ? scrollTop : 0
|
||||||
const start = Math.max(0, Number.isFinite(startY) ? startY : 0)
|
const start = Math.max(0, Number.isFinite(startY) ? startY : 0)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import { getTopMaskProgress } from '../src/utils/homeTopMask.js'
|
import { getTopMaskProgress, getTopMaskStartY } from '../src/utils/homeTopMask.js'
|
||||||
|
|
||||||
test('returns 0 before the title reaches the status-bar area', () => {
|
test('returns 0 before the title reaches the status-bar area', () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -28,3 +28,32 @@ test('clamps invalid values into a stable progress range', () => {
|
|||||||
assert.equal(getTopMaskProgress({ scrollTop: 90, startY: 80, rangeY: 0 }), 1)
|
assert.equal(getTopMaskProgress({ scrollTop: 90, startY: 80, rangeY: 0 }), 1)
|
||||||
assert.equal(getTopMaskProgress({ scrollTop: Number.NaN, startY: 80, rangeY: 40 }), 0)
|
assert.equal(getTopMaskProgress({ scrollTop: Number.NaN, startY: 80, rangeY: 40 }), 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('converts a viewport-relative title bottom to a document-relative start Y', () => {
|
||||||
|
assert.equal(
|
||||||
|
getTopMaskStartY({ viewportBottom: 148, scrollTop: 0, statusBarHeight: 44 }),
|
||||||
|
96
|
||||||
|
)
|
||||||
|
|
||||||
|
// The same header, measured after the user has already scrolled, must keep
|
||||||
|
// the same document-relative threshold instead of collapsing to 0.
|
||||||
|
assert.equal(
|
||||||
|
getTopMaskStartY({ viewportBottom: 48, scrollTop: 100, statusBarHeight: 44 }),
|
||||||
|
96
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('clamps the document-relative start Y at zero', () => {
|
||||||
|
assert.equal(
|
||||||
|
getTopMaskStartY({ viewportBottom: 20, scrollTop: 0, statusBarHeight: 44 }),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('treats non-finite threshold inputs as zero without producing NaN', () => {
|
||||||
|
assert.equal(
|
||||||
|
getTopMaskStartY({ viewportBottom: Number.NaN, scrollTop: 80, statusBarHeight: 44 }),
|
||||||
|
28
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user