Expo 54 iOS 26自定义头部按钮间歇性异常拉伸问题
Expo 54 + iOS 26页面右侧自定义头部按钮间歇性拉伸异常
使用Expo 54开发时,在iOS 26系统环境下,页面右侧的自定义头部按钮会出现间歇性拉伸的异常情况。该问题难以稳定复现,但发生频率较高。
相关组件代码
HeaderButton.tsx
import { tokens } from '@/design'; import { color } from '@/design/colors'; import React from 'react'; import { AccessibilityRole, Platform, StyleProp, View, ViewStyle } from 'react-native'; import { BorderlessButton, RectButton } from 'react-native-gesture-handler'; import { Pressable } from './Pressable'; import { isLiquidGlassAvailable } from 'expo-glass-effect'; interface HeaderButtonProps { onPress?: () => void; children: React.ReactNode; testID?: string; accessibilityRole?: AccessibilityRole; accessibilityLabel?: string; disabled?: boolean; rippleShape?: 'circle' | 'rect'; style?: StyleProp<ViewStyle>; } const InnerContent = ({ children }: { children: React.ReactNode }) => ( <View style={{ flexDirection: 'row', alignItems: 'center', gap: tokens.spacing.xs }}> {children} </View> ); export function HeaderButton({ onPress, children, testID, accessibilityRole = 'button', accessibilityLabel, disabled = false, rippleShape = 'circle', style, }: HeaderButtonProps) { if (Platform.OS === 'android') { if (rippleShape === 'rect') { return ( <RectButton onPress={onPress} testID={testID} accessibilityRole={accessibilityRole} accessibilityLabel={accessibilityLabel} enabled={!disabled} rippleColor={color.androidHighlight} style={[ { paddingHorizontal: 6, opacity: disabled ? 0.3 : 1, minWidth: 48, minHeight: 48, borderRadius: tokens.radius.full, justifyContent: 'center', alignItems: 'center', }, style, ]} > <InnerContent>{children}</InnerContent> </RectButton> ); } return ( <BorderlessButton onPress={onPress} testID={testID} accessibilityRole={accessibilityRole} accessibilityLabel={accessibilityLabel} enabled={!disabled} rippleColor={color.androidHighlight} hitSlop={16} rippleRadius={20} style={[ { paddingHorizontal: 0, opacity: disabled ? 0.3 : 1, justifyContent: 'center', alignItems: 'center', }, style, ]} > <InnerContent>{children}</InnerContent> </BorderlessButton> ); } return ( <Pressable onPress={onPress} testID={testID} accessibilityRole={accessibilityRole} accessibilityLabel={accessibilityLabel} disabled={disabled} haptic hitSlop={8} style={[ { paddingHorizontal: isLiquidGlassAvailable() ? 6 : undefined, opacity: disabled ? 0.3 : 1, justifyContent: 'center', alignItems: 'center', }, style, ]} > <InnerContent>{children}</InnerContent> </Pressable> ); }
HeaderMenuButton.tsx
import { MenuAction, MenuView, NativeActionEvent } from '@react-native-menu/menu'; import { isLiquidGlassAvailable } from 'expo-glass-effect'; import React from 'react'; import { type AccessibilityRole, Platform, StyleSheet, View } from 'react-native'; import { HeaderButton } from './HeaderButton'; // Liquid Glass headers render icon-only actions with a noticeably smaller circular visual than the // pre-iOS-26 header buttons. Matching that 36pt visual keeps the menu trigger aligned with the // existing checkmark/save buttons while still constraining the native MenuView anchor. const LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE = 36; type AccessibleMenuViewProps = React.ComponentProps<typeof MenuView> & { accessibilityLabel?: string; accessibilityRole?: AccessibilityRole; }; // TODO: Remove this once @react-native-menu/menu exposes standard accessibility props in its TS types. const AccessibleMenuView = MenuView as React.ComponentType<AccessibleMenuViewProps>; interface HeaderMenuButtonProps { actions: MenuAction[]; onPressAction: ({ nativeEvent }: NativeActionEvent) => void; children: React.ReactNode; testID?: string; accessibilityLabel: string; } export function HeaderMenuButton({ actions, onPressAction, children, testID, accessibilityLabel, }: HeaderMenuButtonProps) { const shouldUseConstrainedAnchor = Platform.OS === 'ios' && isLiquidGlassAvailable(); return ( <AccessibleMenuView accessibilityLabel={accessibilityLabel} accessibilityRole="button" testID={testID} actions={actions} onPressAction={onPressAction} shouldOpenOnLongPress={false} style={shouldUseConstrainedAnchor ? styles.menuAnchor : undefined} > <View pointerEvents="none" accessible={false} accessibilityElementsHidden importantForAccessibility="no-hide-descendants" style={shouldUseConstrainedAnchor ? styles.content : undefined} > <HeaderButton accessibilityLabel={accessibilityLabel} style={shouldUseConstrainedAnchor ? styles.visualButton : undefined} > {children} </HeaderButton> </View> </AccessibleMenuView> ); } const styles = StyleSheet.create({ menuAnchor: { width: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE, height: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE, minWidth: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE, minHeight: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE, flexGrow: 0, flexShrink: 0, justifyContent: 'center', alignItems: 'center', }, content: { width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', }, visualButton: { width: '100%', height: '100%', paddingHorizontal: 0, }, });

问题分析与修复方案
核心原因
- iOS 26的Liquid Glass模式下,
MenuView的布局约束和内部HeaderButton的动态样式存在冲突,导致布局重绘时按钮被意外拉伸; InnerContent组件没有明确的尺寸限制,在父容器尺寸变化时会被强制拉伸;Pressable组件的默认布局逻辑可能在某些场景下自动扩展尺寸。
修复方案1:锁定按钮核心尺寸
修改HeaderButton.tsx中的InnerContent和iOS端Pressable样式,强制固定尺寸:
// 修改InnerContent组件 const InnerContent = ({ children }: { children: React.ReactNode }) => ( <View style={{ flexDirection: 'row', alignItems: 'center', gap: tokens.spacing.xs, width: '100%', height: '100%', justifyContent: 'center' }}> {children} </View> ); // 修改iOS端Pressable的样式 return ( <Pressable onPress={onPress} testID={testID} accessibilityRole={accessibilityRole} accessibilityLabel={accessibilityLabel} disabled={disabled} haptic hitSlop={8} style={[ { paddingHorizontal: isLiquidGlassAvailable() ? 6 : undefined, opacity: disabled ? 0.3 : 1, justifyContent: 'center', alignItems: 'center', minWidth: isLiquidGlassAvailable() ? 36 : 48, minHeight: isLiquidGlassAvailable() ? 36 : 48, flexGrow: 0, flexShrink: 0 }, style, ]} > <InnerContent>{children}</InnerContent> </Pressable> );
修复方案2:优化MenuButton布局层级
去掉HeaderMenuButton.tsx中多余的嵌套View,直接给HeaderButton传递明确尺寸:
// 修改HeaderMenuButton的渲染逻辑 return ( <AccessibleMenuView accessibilityLabel={accessibilityLabel} accessibilityRole="button" testID={testID} actions={actions} onPressAction={onPressAction} shouldOpenOnLongPress={false} style={[ shouldUseConstrainedAnchor ? styles.menuAnchor : undefined, { flexGrow: 0, flexShrink: 0 } ]} > <HeaderButton accessibilityLabel={accessibilityLabel} style={[ shouldUseConstrainedAnchor ? styles.visualButton : undefined, shouldUseConstrainedAnchor && { width: 36, height: 36 } ]} > {children} </HeaderButton> </AccessibleMenuView> ); // 修改styles中的visualButton const styles = StyleSheet.create({ // ...其他样式不变 visualButton: { paddingHorizontal: 0, }, });
额外建议
如果自定义Pressable组件存在自动调整尺寸的逻辑,添加属性禁用该行为;同时可以给按钮图标组件添加明确的width和height,进一步避免拉伸。
内容的提问来源于stack exchange,提问作者Victor
相关产品推荐
相关产品推荐

