Expo 54 iOS 26液态玻璃风格的导航栏按钮偶发性延展

移动开发 2026-07-10

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

解决方案

尝试将水平内边距设置为3,看看是否能解决问题。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章