You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,
  },
});

异常效果截图


问题分析与修复方案

核心原因

  1. iOS 26的Liquid Glass模式下,MenuView的布局约束和内部HeaderButton的动态样式存在冲突,导致布局重绘时按钮被意外拉伸;
  2. InnerContent组件没有明确的尺寸限制,在父容器尺寸变化时会被强制拉伸;
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 12:34:53