Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 50 additions & 24 deletions packages/webpack-plugin/lib/runtime/components/react/mpx-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,19 @@ const Image = forwardRef<HandlerRef<RNImage, ImageProps>, ImageProps>((props, re
const [imageWidth, setImageWidth] = useState(0)
const [imageHeight, setImageHeight] = useState(0)
const [ratio, setRatio] = useState(0)
const [loaded, setLoaded] = useState(!isLayoutMode)
// 布局模式也要首屏挂载真实 Image,不能等待 getSize;部分 iOS WebP 的 getSize 可能不回调
const [loaded, setLoaded] = useState(true)

const state = useRef<ImageState>({
viewWidth,
viewHeight
})
// id 标识当前 src 的尺寸请求批次,避免旧图片的异步回调覆盖新图片尺寸
const sizeRequest = useRef({ id: 0, src })
if (sizeRequest.current.src !== src) {
// src 改变即开启新批次,之前 getSize/onLoad 捕获的 id 会自动失效
sizeRequest.current = { id: sizeRequest.current.id + 1, src }
}

function setViewSize (viewWidth: number, viewHeight: number, ratio: number) {
// 在特定模式下可预测 view 的变化,在onLayout触发时能以此避免重复render
Expand All @@ -272,6 +279,28 @@ const Image = forwardRef<HandlerRef<RNImage, ImageProps>, ImageProps>((props, re
}
}

function resolveImageSize (width: number, height: number, request: number) {
// 忽略无效尺寸和旧批次结果
if (!width || !height || request !== sizeRequest.current.id) return
// ref 同步保存结果,保证 onLayout 先后顺序不同也能读取最新原图尺寸
state.current.imageWidth = width
state.current.imageHeight = height
state.current.ratio = height / width

// widthFix 依赖容器宽度,heightFix 依赖容器高度,裁剪模式同时依赖宽高
if (isWidthFixMode
? state.current.viewWidth
: isHeightFixMode
? state.current.viewHeight
: state.current.viewWidth && state.current.viewHeight) {
setRatio(state.current.ratio)
setImageWidth(width)
setImageHeight(height)
setViewSize(state.current.viewWidth!, state.current.viewHeight!, state.current.ratio)
setLoaded(true)
}
}

const modeStyle: ImageStyle = useMemo(() => {
if (noMeetCalcRule(isSvg, mode, viewWidth, viewHeight, ratio)) return {}
switch (mode) {
Expand Down Expand Up @@ -413,10 +442,10 @@ const Image = forwardRef<HandlerRef<RNImage, ImageProps>, ImageProps>((props, re
)
}

const onImageLoad = (evt: NativeSyntheticEvent<ImageLoadEventData>) => {
const onImageLoad = (evt: NativeSyntheticEvent<ImageLoadEventData>, request: number) => {
evt.persist()
const triggerLoad = (width: number, height: number) => {
bindload!(
bindload && bindload(
getCustomEvent(
'load',
evt,
Expand All @@ -428,9 +457,15 @@ const Image = forwardRef<HandlerRef<RNImage, ImageProps>, ImageProps>((props, re
)
)
}
const { source } = evt.nativeEvent
if (source && source.width && source.height) {
triggerLoad(source.width, source.height)
// RN Image 尺寸在 nativeEvent.source;FastImage 尺寸直接在 nativeEvent 上
const nativeEvent = evt.nativeEvent as any
const source = nativeEvent.source
const width = (source && source.width) || nativeEvent.width || 0
const height = (source && source.height) || nativeEvent.height || 0
// 可见 Image 的 onLoad 是 getSize 不返回时的尺寸兜底
if (isLayoutMode) resolveImageSize(width, height, request)
if (width && height) {
triggerLoad(width, height)
return
}
getImageSize(src, triggerLoad)
Expand All @@ -452,28 +487,16 @@ const Image = forwardRef<HandlerRef<RNImage, ImageProps>, ImageProps>((props, re

useEffect(() => {
if (!isSvg && isLayoutMode) {
// 捕获本次 effect 对应的批次,src 切换后旧回调会被 resolver 丢弃
const request = sizeRequest.current.id
getImageSize(
src,
(width: number, height: number) => {
state.current.imageWidth = width
state.current.imageHeight = height
state.current.ratio = !width ? 0 : height / width

if (isWidthFixMode
? state.current.viewWidth
: isHeightFixMode
? state.current.viewHeight
: state.current.viewWidth && state.current.viewHeight) {
setRatio(state.current.ratio)
setImageWidth(width)
setImageHeight(height)
setViewSize(state.current.viewWidth!, state.current.viewHeight!, state.current.ratio!)

setLoaded(true)
}
resolveImageSize(width, height, request)
},
() => {
setLoaded(true)
// getSize 失败不影响图片展示,真实尺寸仍可由 Image onLoad 回填
if (request === sizeRequest.current.id) setLoaded(true)
}
)
}
Expand Down Expand Up @@ -527,6 +550,8 @@ const Image = forwardRef<HandlerRef<RNImage, ImageProps>, ImageProps>((props, re
}

function renderBaseImage () {
// onLoad 必须携带创建该 Image 时的批次,不能在回调时读取最新 id
const request = sizeRequest.current.id
const baseImageStyle = isCropMode
? extendObject(
{ transformOrigin: 'left top', width: imageWidth, height: imageHeight },
Expand All @@ -538,7 +563,8 @@ const Image = forwardRef<HandlerRef<RNImage, ImageProps>, ImageProps>((props, re
{
source: imageSource,
resizeMode: resizeMode,
onLoad: bindload && onImageLoad,
// 布局模式即使未传 bindload,也要监听 onLoad 获取原图尺寸
onLoad: (isLayoutMode || bindload) ? (evt: NativeSyntheticEvent<ImageLoadEventData>) => onImageLoad(evt, request) : undefined,
onError: binderror && onImageError,
style: baseImageStyle
},
Expand Down
67 changes: 52 additions & 15 deletions packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import useAnimationHooks, { AnimationType } from './animationHooks/index'
import type { AnimationProp } from './animationHooks/utils'
import { ExtendedViewStyle } from './types/common'
import useNodesRef, { HandlerRef } from './useNodesRef'
import { parseUrl, percentRegExp, splitStyle, splitProps, useTransformStyle, wrapChildren, useLayout, renderImage, pickStyle, extendObject, useHover, useTextPassThrough } from './utils'
import { parseUrl, percentRegExp, splitStyle, splitProps, useTransformStyle, wrapChildren, useLayout, renderImage, pickStyle, extendObject, useHover, useTextPassThrough, isIOS } from './utils'
import { TextPassThroughContextValue } from './context'
import { error, warn, hasOwn } from '@mpxjs/utils'
import * as perf from '@mpxjs/perf'
Expand Down Expand Up @@ -276,6 +276,8 @@ function backgroundPosition (imageProps: ImageProps, preImageInfo: PreImageInfo,
function backgroundSize (imageProps: ImageProps, preImageInfo: PreImageInfo, imageSize: Size, layoutInfo: Size) {
const { sizeList, type } = preImageInfo
if (!sizeList) return
// background-size 已负责计算最终宽高,普通图片内部只需铺满该矩形,避免 Android FastImage 再按 cover 二次裁剪 安卓会把图片放大
if (type === 'image') imageProps.resizeMode = 'stretch'
const { width: layoutWidth, height: layoutHeight } = layoutInfo || {}
const { width: imageSizeWidth, height: imageSizeHeight } = imageSize || {}
const [width, height] = sizeList
Expand Down Expand Up @@ -694,6 +696,12 @@ function useWrapImage (imageStyle?: ExtendedViewStyle, innerStyle?: Record<strin
const [version, setVersion] = useState(0)
const sizeInfo = useRef<Size | null>(null)
const layoutInfo = useRef<Size | null>(null)
// id 标识当前背景图的尺寸请求批次,避免切换后旧图回调污染 contain 计算
const imageRequest = useRef({ id: 0, src })
if (imageRequest.current.src !== src) {
// src 改变即让旧 getSize/onLoad 回调失效
imageRequest.current = { id: imageRequest.current.id + 1, src }
}
const sizeCacheRef = useRef<Map<string, Size>>(new Map())
// sizeInfo / layoutInfo / setVersion 都是稳定引用,闭包整个生命周期只分配一次
const { bumpVersion, setImageSize, setLayoutInfo } = useMemo(() => ({
Expand All @@ -709,6 +717,19 @@ function useWrapImage (imageStyle?: ExtendedViewStyle, innerStyle?: Record<strin
return true
}
}), [])

function resolveBackgroundImageSize (width: number, height: number, request: number, cache = false) {
// 只接收当前 src 的有效原图尺寸
if (!width || !height || request !== imageRequest.current.id) return
// 仅缓存 getSize 的原图尺寸,避免 Android FastImage onLoad 的临时布局尺寸污染缓存
if (cache && src) sizeCacheRef.current.set(src, { width, height })
const imageSizeChanged = setImageSize(width, height)
if (!needLayout || layoutInfo.current) {
imageSizeChanged && bumpVersion()
setShow(true)
}
}

useEffect(() => {
sizeInfo.current = null
if (type === 'linear') {
Expand All @@ -726,26 +747,18 @@ function useWrapImage (imageStyle?: ExtendedViewStyle, innerStyle?: Record<strin
}

if (needImageSize) {
const request = imageRequest.current.id
const cached = sizeCacheRef.current.get(src)
if (cached) {
const imageSizeChanged = setImageSize(cached.width, cached.height)
if (!needLayout || layoutInfo.current) {
imageSizeChanged && bumpVersion()
setShow(true)
}
resolveBackgroundImageSize(cached.width, cached.height, request)
return
}
let cancelled = false
// 仅 iOS 先挂载背景 Image 走 onLoad 兜底;非 iOS 保持原 getSize 流程
if (isIOS) setShow(true)
Image.getSize(src, (width, height) => {
// cache 仍然填上,避免下次同 src 再发一次请求
sizeCacheRef.current.set(src, { width, height })
if (cancelled) return
const imageSizeChanged = setImageSize(width, height)
// 1. 当需要绑定onLayout 2. 获取到布局信息
if (!needLayout || layoutInfo.current) {
imageSizeChanged && bumpVersion()
setShow(true)
}
resolveBackgroundImageSize(width, height, request, true)
})
return () => { cancelled = true }
}
Expand Down Expand Up @@ -780,10 +793,34 @@ function useWrapImage (imageStyle?: ExtendedViewStyle, innerStyle?: Record<strin
const backgroundProps: ViewProps = extendObject({ key: 'backgroundImage' }, needLayout ? { onLayout } : {},
{ style: extendObject({}, inheritStyle(innerStyle), StyleSheet.absoluteFillObject, { overflow: 'hidden' as const }) }
)
const request = imageRequest.current.id
const onBackgroundImageLoad = (evt: NativeSyntheticEvent<any>) => {
// getSize 已返回时不再让 FastImage onLoad 覆盖原图尺寸
if (sizeInfo.current) return
// RN Image 尺寸在 nativeEvent.source;FastImage 尺寸直接在 nativeEvent 上
const nativeEvent = evt.nativeEvent
const source = nativeEvent.source
resolveBackgroundImageSize(
(source && source.width) || nativeEvent.width || 0,
(source && source.height) || nativeEvent.height || 0,
request
)
}
const runtimeImageProps: ImageProps = isIOS && type === 'image' && needImageSize
? extendObject({}, imageProps, {
// src 变化时重建背景 Image,确保缓存图片也触发当前批次的 onLoad
key: src,
onLoad: onBackgroundImageLoad,
// 原图尺寸未知时用同一个背景 Image 透明启动;拿到尺寸后重新计算 contain 并恢复正常样式
style: sizeInfo.current
? imageProps.style
: extendObject({}, imageProps.style, { width: 1, height: 1, opacity: 0 })
})
: imageProps as ImageProps

return createElement(View, backgroundProps,
show && type === 'linear' && createElement(LinearGradient, extendObject({ useAngle: true }, imageProps as LinearImageProps)),
show && type === 'image' && renderImage(imageProps, enableFastImage)
show && type === 'image' && renderImage(runtimeImageProps, enableFastImage)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,8 @@
.mpx-scroll-view
overflow hidden
position relative
.mpx-scroll-view-content
flex-shrink 0 // 解决内容区域被挤压导致的滚动异常问题,在scroll-view开启增强特性时会有子元素使用flex布局的场景

.mpx-pull-down-wrapper
position: absolute
Expand Down
Loading