diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx index a178839506..2c6b575860 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx @@ -173,12 +173,13 @@ const isPercent = (val: string | number | undefined): val is string => typeof va const isBackgroundSizeKeyword = (val: string | number): boolean => typeof val === 'string' && /^cover|contain$/.test(val) const isNeedLayout = (preImageInfo: PreImageInfo): boolean => { - const { sizeList, backgroundPosition, type } = preImageInfo + const { sizeList, backgroundPosition, type, linearInfo } = preImageInfo const [width, height] = sizeList const bp = backgroundPosition if (type === 'linear') { - return !(Number.isFinite(width) && Number.isFinite(height)) || isPercent(bp[1]) || isPercent(bp[3]) + return ((__mpx_mode__ === 'ios' || (linearInfo?.direction && diagonalAngleMap[linearInfo.direction])) && + !(Number.isFinite(width) && Number.isFinite(height))) || isPercent(bp[1]) || isPercent(bp[3]) } // 含有百分号,center 需计算布局 @@ -284,10 +285,7 @@ function backgroundSize (imageProps: ImageProps, preImageInfo: PreImageInfo, ima const { width: layoutWidth, height: layoutHeight } = layoutInfo || {} const { width: imageSizeWidth, height: imageSizeHeight } = imageSize || {} const [width, height] = sizeList - let dimensions: { - width: NumberVal, - height: NumberVal - } | null = { width: 0, height: 0 } + let dimensions: ImageStyle | null = { width: 0, height: 0 } // 枚举值 if (typeof width === 'string' && ['cover', 'contain'].includes(width)) { @@ -317,13 +315,9 @@ function backgroundSize (imageProps: ImageProps, preImageInfo: PreImageInfo, ima dimensions = calculateSize(width as number, imageSizeHeight / imageSizeWidth, layoutInfo?.width, true) if (!dimensions) return } else { // 数值类型 ImageStyle - if (type === 'linear' && (!layoutWidth || !layoutHeight) && (isPercent(width) || isPercent(height))) { - // ios 上 linear 组件只要重新触发渲染,在渲染过程中外层容器 width 或者 height 被设置为 0,通过设置 % 的方式会渲染不出来,即使后面再更新为正常宽高也渲染不出来 - // 所以 hack 手动先将 linear 宽高也设置为 0,后面再更新为正确的数值或 %。 - dimensions = { - width: 0, - height: 0 - } as { width: NumberVal, height: NumberVal } + if (__mpx_mode__ === 'ios' && type === 'linear' && (!layoutWidth || !layoutHeight) && (isPercent(width) || isPercent(height))) { + // iOS 百分比渐变在零尺寸布局下先归零,布局恢复后再使用百分比。 + dimensions = { width: 0, height: 0 } } else { dimensions = { width: isPercent(width) ? width : +width, diff --git a/packages/webpack-plugin/test/runtime/react-native/mpx-view-gradient.spec.ts b/packages/webpack-plugin/test/runtime/react-native/mpx-view-gradient.spec.ts index 8c7bf1ebd8..fda1ffd588 100644 --- a/packages/webpack-plugin/test/runtime/react-native/mpx-view-gradient.spec.ts +++ b/packages/webpack-plugin/test/runtime/react-native/mpx-view-gradient.spec.ts @@ -102,3 +102,92 @@ describe('mpx-view linear-gradient parser', () => { act(() => renderer!.unmount()) }) }) + +describe.each(['android', 'harmony'])('%s gradient layout', (platform) => { + beforeEach(() => { global.__mpx_mode__ = platform }) + afterEach(() => { global.__mpx_mode__ = 'ios' }) + const render = (style: Record = {}) => React.createElement(MpxView, { + 'enable-background': true, + style: Object.assign({ width: 200, height: 100, backgroundImage: 'linear-gradient(90deg, red, blue)' }, style) + }) + const fillStyle = { position: 'absolute', width: '100%', height: '100%', left: 0, top: 0 } + + test.each([ + [undefined], [['auto']], [['cover']], [['contain']], [['100%', '100%']] + ])('renders full-size gradient immediately for %j', (backgroundSize) => { + let renderer: ReactTestRenderer + act(() => { renderer = create(render(backgroundSize ? { backgroundSize } : {})) }) + expect(renderer!.root.findByType('LinearGradient').props.style).toEqual(fillStyle) + act(() => renderer!.root.findAllByType('View')[1].props.onLayout(layoutEvent(200, 100))) + act(() => renderer!.update(render({ backgroundSize, backgroundImage: 'linear-gradient(90deg, green, blue)' }))) + expect(renderer!.root.findByType('LinearGradient').props.style).toEqual(fillStyle) + act(() => renderer!.unmount()) + }) + + test.each([ + ['left', 20, 'top', 0], ['right', 20, 'bottom', 10], ['left', -20, 'top', 0] + ])('keeps full dimensions for offset %s %s %s %s', (x, dx, y, dy) => { + let renderer: ReactTestRenderer + act(() => { renderer = create(render({ backgroundPosition: [x, dx, y, dy] })) }) + expect(renderer!.root.findByType('LinearGradient').props.style).toEqual({ position: 'absolute', width: '100%', height: '100%', [x]: dx, [y]: dy }) + act(() => renderer!.root.findAllByType('View')[1].props.onLayout(layoutEvent(200, 100))) + expect(renderer!.root.findByType('LinearGradient').props.style).toEqual({ position: 'absolute', width: '100%', height: '100%', [x]: dx, [y]: dy }) + act(() => renderer!.root.findAllByType('View')[1].props.onLayout(layoutEvent(0, 100))) + expect(renderer!.root.findByType('LinearGradient').props.style).toMatchObject({ width: '100%', height: '100%' }) + act(() => renderer!.root.findAllByType('View')[1].props.onLayout(layoutEvent(200, 100))) + expect(renderer!.root.findByType('LinearGradient').props.style).toMatchObject({ width: '100%', height: '100%' }) + act(() => renderer!.unmount()) + }) + + test('keeps layout for diagonal angles but renders partial percentages immediately', () => { + let renderer: ReactTestRenderer + act(() => { renderer = create(renderGradient(['100%', '100%'])) }) + expect(renderer!.root.findAllByType('LinearGradient')).toHaveLength(0) + act(() => renderer!.root.findAllByType('View')[1].props.onLayout(layoutEvent(200, 100))) + expect(renderer!.root.findByType('LinearGradient').props.angle).toBeCloseTo(26.565) + act(() => renderer!.unmount()) + act(() => { renderer = create(render({ backgroundSize: ['50%', '75%'] })) }) + expect(renderer!.root.findByType('LinearGradient').props.style).toMatchObject({ width: '50%', height: '75%' }) + act(() => renderer!.unmount()) + }) + + test('switches between fill and layout-dependent styles using the cached layout', () => { + let renderer: ReactTestRenderer + act(() => { renderer = create(render()) }) + expect(renderer!.root.findByType('LinearGradient').props.style).toEqual(fillStyle) + act(() => renderer!.root.findAllByType('View')[1].props.onLayout(layoutEvent(200, 100))) + act(() => renderer!.update(render({ backgroundSize: ['50%', '100%'], backgroundPosition: ['center', 'center'] }))) + expect(renderer!.root.findByType('LinearGradient').props.style).toEqual({ position: 'absolute', width: '50%', height: '100%', left: 50, top: 0 }) + act(() => renderer!.update(render())) + expect(renderer!.root.findByType('LinearGradient').props.style).toEqual(fillStyle) + act(() => renderer!.root.findAllByType('View')[1].props.onLayout(layoutEvent(0, 100))) + act(() => renderer!.update(render({ backgroundImage: 'linear-gradient(90deg, green, blue)' }))) + expect(renderer!.root.findByType('LinearGradient').props.style).toEqual(fillStyle) + act(() => renderer!.root.findAllByType('View')[1].props.onLayout(layoutEvent(200, 100))) + act(() => renderer!.update(render({ backgroundPosition: ['center', 'center'] }))) + expect(renderer!.root.findByType('LinearGradient').props.style).toEqual({ position: 'absolute', width: '100%', height: '100%', left: 0, top: 0 }) + act(() => renderer!.unmount()) + }) +}) + +describe('iOS gradient visibility restoration', () => { + test('waits for layout then keeps the gradient mounted with zero or percentage dimensions', () => { + let renderer: ReactTestRenderer + act(() => { + renderer = create(React.createElement(MpxView, { + 'enable-background': true, + style: { width: 100, height: 100, backgroundImage: 'linear-gradient(179deg, #fff 0%, #000 76%)', backgroundSize: ['100%', '100%'] } + })) + }) + expect(renderer!.root.findAllByType('LinearGradient')).toHaveLength(0) + ;[0, 98, 0, 98].forEach(height => { + act(() => renderer!.root.findAllByType('View')[1].props.onLayout(layoutEvent(98, height))) + if (height) { + expect(renderer!.root.findByType('LinearGradient').props.style).toMatchObject({ width: '100%', height: '100%' }) + } else { + expect(renderer!.root.findByType('LinearGradient').props.style).toMatchObject({ width: 0, height: 0 }) + } + }) + act(() => renderer!.unmount()) + }) +})