HarmonyOS实战:React Native实现Popover弹出位置精准控制

HarmonyOS实战:React Native实现Popover弹出位置精准控制

在HarmonyOS(OpenHarmony)平台开发React Native应用时,Popover弹出框的位置精准控制是提升用户体验的核心要点。合理的弹出位置能保证弹出内容与触发元素视觉关联紧密,同时避免被屏幕边缘截断、遮挡,适配不同屏幕尺寸的设备。本文基于OpenHarmony 6.0.0(API 20)React Native 0.72.5 技术栈,从坐标原理、定位策略、OpenHarmony适配、完整代码实现等方面,详细讲解如何实现Popover弹出位置的精细化控制,同时优化代码可读性与工程化规范

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

  • [HarmonyOS实战:React Native实现Popover弹出位置精准控制](#HarmonyOS实战:React Native实现Popover弹出位置精准控制)

一、Popover位置计算核心原理

1.1 屏幕坐标系统

Popover的位置计算基于屏幕绝对坐标系统 ,屏幕左上角为坐标原点(0,0),右下角为(screenWidth, screenHeight)。触发元素(Anchor)为位置参考基准,Popover的坐标需根据触发元素的位置、自身尺寸进行计算,基础坐标示意如下:

复制代码
┌────────────────────────────────────┐
│ (0,0) 屏幕原点                     │
│   ┌────────┐                       │
│   │ Anchor │ 触发元素(参考基准)  │
│   └────────┘                       │
│          ↓ 偏移量                  │
│      ┌──────┐                      │
│      │Popover│ 弹出框(目标元素)  │
│      └──────┘                      │
│                                    │
└────────────────────────────────────┘
                (screenWidth, screenHeight) 屏幕右下角

基础计算公式(底部居中弹出)

typescript 复制代码
// Popover水平居中对齐触发元素
popoverX = anchorX + anchorWidth/2 - popoverWidth/2
// Popover在触发元素底部偏移指定距离
popoverY = anchorY + anchorHeight + offset

1.2 定位策略

根据业务场景的空间差异,设计4种核心定位策略,适配不同的屏幕空间需求:

策略 计算方式 适用场景
center 居中对齐触发元素 通用主流场景
start 左/上对齐触发元素 右侧/下侧空间充足
end 右/下对齐触发元素 左侧/上侧空间充足
auto 自动选择最佳弹出位置 空间不确定场景

本文实现center/start/end三种基础策略,auto策略可基于基础策略扩展空间检测逻辑实现。

二、OpenHarmony平台适配要点

OpenHarmony的布局计算、API支持与传统RN端存在差异,直接使用web端或其他端的定位逻辑会出现坐标偏移、元素溢出等问题,核心适配要点集中在测量API安全区域边界检测三方面。

2.1 测量API选择

OpenHarmony对RN的测量API做了部分兼容限制,需选择精准的屏幕坐标获取方式,避免父容器嵌套导致的坐标偏差:

API 用途 OpenHarmony注意事项
measure 相对于父容器获取坐标 需遍历父组件链,易出现嵌套偏移
measureInWindow 相对于屏幕获取坐标 推荐使用,结果精准无嵌套影响
getBoundingClientRect web端通用API 完全不支持,使用measureInWindow替代

核心原则 :优先使用measureInWindow获取触发元素的屏幕绝对坐标,是实现精准定位的基础。

2.2 安全区域处理

OpenHarmony设备存在状态栏底部导航栏等系统安全区域,Popover需避开该区域,否则会出现被系统控件遮挡的问题:

  • 状态栏安全区域:顶部默认偏移50px(适配大部分OpenHarmony设备)
  • 底部安全区域:底部默认偏移34px
  • 屏幕边距:左右两侧保留16px边距,保证视觉留白

2.3 边界检测逻辑

通过Math.max/Math.min约束Popover的坐标范围,确保其完全在屏幕可视区域内,避免元素溢出屏幕:

typescript 复制代码
// 通用边界检测函数
const ensureInBounds = (
  x: number,
  y: number,
  width: number,
  height: number,
  screenWidth: number,
  screenHeight: number
) => {
  const safeAreaTop = 50;  // 状态栏安全区域
  const safeAreaBottom = 34; // 底部安全区域
  // 水平约束:左右保留16px边距
  const finalX = Math.max(16, Math.min(x, screenWidth - width - 16));
  // 垂直约束:避开状态栏、底部安全区域,保留16px边距
  const finalY = Math.max(safeAreaTop + 16, Math.min(y, screenHeight - height - safeAreaBottom - 16));
  return { x: finalX, y: finalY };
};

三、完整代码实现(工程化规范版)

本次实现的Popover组件具备方向可配 (上/下/左/右)、对齐方式可配 (start/center/end)、边界自适配OpenHarmony专属兼容特性,同时遵循React Native组件开发规范,做了类型定义、代码解耦、样式归一化优化。

3.1 组件整体设计

  1. 封装通用PositionControlledPopover组件,通过props暴露配置项,支持业务灵活调用
  2. 实现演示页面PositionControlDemoScreen,支持实时切换弹出方向、对齐方式,直观验证效果
  3. 所有样式通过StyleSheet.create定义,统一管理,适配OpenHarmony样式渲染规则
  4. 完善TypeScript类型定义,避免类型隐式转换问题

3.2 完整可运行代码

typescript 复制代码
/**
 * HarmonyOS实战:React Native Popover弹出位置精准控制
 * 技术栈:OpenHarmony 6.0.0 (API 20) + React Native 0.72.5 + TypeScript 4.8.4
 * 核心特性:方向可配、对齐可配、边界自适配、OpenHarmony专属兼容
 */
import React, { useState, useRef, useEffect } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  Modal,
  Dimensions,
  ViewProps,
} from 'react-native';

// 定义Popover组件Props类型
export interface PositionControlledPopoverProps {
  visible: boolean; // 是否显示
  anchor: React.ReactNode; // 触发元素
  children: React.ReactNode; // Popover内容
  placement?: 'top' | 'bottom' | 'left' | 'right'; // 弹出方向
  align?: 'start' | 'center' | 'end'; // 对齐方式
  onClose: () => void; // 关闭回调
  offset?: number; // 与触发元素的偏移量,默认8px
  popoverSize?: { width: number; height: number }; // Popover尺寸,默认200*150
}

// 定义演示页面Props类型
export interface PositionControlDemoProps {
  onBack?: () => void; // 返回上一页回调
}

// 边界检测工具函数
const ensureInBounds = (
  x: number,
  y: number,
  width: number,
  height: number,
  screenWidth: number,
  screenHeight: number
) => {
  const safeAreaTop = 50;  // OpenHarmony状态栏安全区域
  const safeAreaBottom = 34; // OpenHarmony底部安全区域
  return {
    x: Math.max(16, Math.min(x, screenWidth - width - 16)),
    y: Math.max(safeAreaTop + 16, Math.min(y, screenHeight - height - safeAreaBottom - 16)),
  };
};

// 通用位置可控Popover组件
const PositionControlledPopover: React.FC<PositionControlledPopoverProps> = ({
  visible,
  anchor,
  children,
  placement = 'bottom',
  align = 'center',
  onClose,
  offset = 8,
  popoverSize = { width: 200, height: 150 },
}) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const anchorRef = useRef<View>(null);
  const { width: popoverWidth, height: popoverHeight } = popoverSize;
  const { width: screenWidth, height: screenHeight } = Dimensions.get('window');

  // 监听显隐、配置变化,重新计算位置
  useEffect(() => {
    // 仅在显示且触发元素挂载完成时计算位置
    if (!visible || !anchorRef.current) return;

    // 获取触发元素的屏幕绝对坐标
    anchorRef.current.measureInWindow((anchorX, anchorY, anchorWidth, anchorHeight) => {
      let posX = anchorX;
      let posY = anchorY;

      // 根据弹出方向+对齐方式计算基础坐标
      switch (placement) {
        case 'bottom':
          posY = anchorY + anchorHeight + offset;
          posX = getHorizontalPos(anchorX, anchorWidth, popoverWidth, align);
          break;
        case 'top':
          posY = anchorY - popoverHeight - offset;
          posX = getHorizontalPos(anchorX, anchorWidth, popoverWidth, align);
          break;
        case 'left':
          posX = anchorX - popoverWidth - offset;
          posY = getVerticalPos(anchorY, anchorHeight, popoverHeight, align);
          break;
        case 'right':
          posX = anchorX + anchorWidth + offset;
          posY = getVerticalPos(anchorY, anchorHeight, popoverHeight, align);
          break;
      }

      // 边界检测,确保Popover在可视区域内
      const finalPos = ensureInBounds(posX, posY, popoverWidth, popoverHeight, screenWidth, screenHeight);
      setPosition(finalPos);
    });
  }, [visible, placement, align, offset, popoverSize, screenWidth, screenHeight]);

  // 水平对齐坐标计算工具函数
  const getHorizontalPos = (anchorX: number, anchorW: number, popoverW: number, align: string) => {
    switch (align) {
      case 'start': return anchorX;
      case 'center': return anchorX + anchorW / 2 - popoverW / 2;
      case 'end': return anchorX + anchorW - popoverW;
      default: return anchorX;
    }
  };

  // 垂直对齐坐标计算工具函数
  const getVerticalPos = (anchorY: number, anchorH: number, popoverH: number, align: string) => {
    switch (align) {
      case 'start': return anchorY;
      case 'center': return anchorY + anchorH / 2 - popoverH / 2;
      case 'end': return anchorY + anchorH - popoverH;
      default: return anchorY;
    }
  };

  // 点击遮罩层关闭Popover
  const handleOverlayPress = () => {
    onClose && onClose();
  };

  return (
    <View style={styles.anchorWrapper}>
      {/* 触发元素,绑定ref用于获取坐标 */}
      <View ref={anchorRef}>{anchor}</View>
      {/* Popover弹窗,使用Modal实现透明遮罩 */}
      <Modal
        visible={visible}
        transparent
        animationType="fade"
        onRequestClose={handleOverlayPress}
        hardwareAccelerated
      >
        <TouchableOpacity style={styles.modalOverlay} onPress={handleOverlayPress} activeOpacity={1}>
          <View
            style={[
              styles.popoverBox,
              { left: position.x, top: position.y, width: popoverWidth, height: popoverHeight }
            ]}
          >
            {children}
          </View>
        </TouchableOpacity>
      </Modal>
    </View>
  );
};

// 演示页面:支持实时切换配置,验证Popover效果
const PositionControlDemoScreen: React.FC<PositionControlDemoProps> = ({ onBack }) => {
  const [visible, setVisible] = useState(false);
  const [placement, setPlacement] = useState<'top' | 'bottom' | 'left' | 'right'>('bottom');
  const [align, setAlign] = useState<'start' | 'center' | 'end'>('center');

  // 关闭Popover
  const handleClose = () => setVisible(false);
  // 打开Popover
  const handleOpen = () => setVisible(true);

  return (
    <View style={styles.container}>
      {/* 顶部导航栏 */}
      <View style={styles.navigationBar}>
        {onBack && (
          <TouchableOpacity onPress={onBack} style={styles.backBtn}>
            <Text style={styles.backText}>← 返回</Text>
          </TouchableOpacity>
        )}
        <View style={styles.titleWrapper}>
          <Text style={styles.mainTitle}>Popover位置精准控制</Text>
          <Text style={styles.subTitle}>OpenHarmony 6.0.0 + RN 0.72.5</Text>
        </View>
      </View>

      {/* 平台信息栏 */}
      <View style={styles.versionBanner}>
        <Text style={styles.versionText}>适配平台:OpenHarmony 6.0.0 (API 20) | 技术栈:React Native + TypeScript</Text>
      </View>

      {/* 功能介绍卡片 */}
      <View style={styles.card}>
        <Text style={styles.cardTitle}>功能说明</Text>
        <Text style={styles.cardDesc}>
          支持上/下/左/右四个弹出方向,搭配start/center/end三种对齐方式,
          自动做边界检测,确保Popover不溢出屏幕、不被系统安全区域遮挡。
        </Text>
      </View>

      {/* 配置区域:切换弹出方向和对齐方式 */}
      <View style={styles.card}>
        <Text style={styles.cardTitle}>位置配置</Text>
        {/* 弹出方向选择 */}
        <View style={styles.configSection}>
          <Text style={styles.configLabel}>弹出方向:</Text>
          <View style={styles.optionRow}>
            {(['top', 'bottom', 'left', 'right'] as const).map((dir) => (
              <TouchableOpacity
                key={dir}
                style={[styles.optionBtn, placement === dir && styles.optionBtnActive]}
                onPress={() => setPlacement(dir)}
              >
                <Text style={[styles.optionText, placement === dir && styles.optionTextActive]}>
                  {dir.toUpperCase()}
                </Text>
              </TouchableOpacity>
            ))}
          </View>
        </View>
        {/* 对齐方式选择 */}
        <View style={styles.configSection}>
          <Text style={styles.configLabel}>对齐方式:</Text>
          <View style={styles.optionRow}>
            {(['start', 'center', 'end'] as const).map((aln) => (
              <TouchableOpacity
                key={aln}
                style={[styles.optionBtn, align === aln && styles.optionBtnActive]}
                onPress={() => setAlign(aln)}
              >
                <Text style={[styles.optionText, align === aln && styles.optionTextActive]}>
                  {aln.toUpperCase()}
                </Text>
              </TouchableOpacity>
            ))}
          </View>
        </View>
        {/* 当前配置展示 */}
        <Text style={styles.currentConfig}>
          当前配置:{placement.toUpperCase()} - {align.toUpperCase()}
        </Text>
      </View>

      {/* 演示区域:触发Popover */}
      <View style={styles.demoArea}>
        <PositionControlledPopover
          visible={visible}
          placement={placement}
          align={align}
          anchor={
            <TouchableOpacity style={styles.anchorButton} onPress={handleOpen}>
              <Text style={styles.anchorButtonText}>点击弹出Popover</Text>
            </TouchableOpacity>
          }
          onClose={handleClose}
        >
          {/* Popover自定义内容 */}
          <View style={styles.popoverContent}>
            <Text style={styles.popoverTitle}>Popover内容区</Text>
            <Text style={styles.popoverDesc}>
              弹出方向:{placement}
              {'\n'}对齐方式:{align}
            </Text>
            <TouchableOpacity style={styles.popoverActionBtn} onPress={handleClose}>
              <Text style={styles.popoverActionText}>确认关闭</Text>
            </TouchableOpacity>
          </View>
        </PositionControlledPopover>
      </View>

      {/* 核心技术要点 */}
      <View style={[styles.card, styles.techCard]}>
        <Text style={styles.cardTitle}>核心技术要点</Text>
        <View style={styles.techList}>
          <View style={styles.techItem}>
            <Text style={styles.techIcon}>📍</Text>
            <View>
              <Text style={styles.techTitle}>measureInWindow</Text>
              <Text style={styles.techDesc}>获取触发元素屏幕绝对坐标,无嵌套偏移</Text>
            </View>
          </View>
          <View style={styles.techItem}>
            <Text style={styles.techIcon}>📐</Text>
            <View>
              <Text style={styles.techTitle}>坐标计算逻辑</Text>
              <Text style={styles.techDesc}>方向+对齐解耦,封装通用计算工具函数</Text>
            </View>
          </View>
          <View style={styles.techItem}>
            <Text style={styles.techIcon}>🛡️</Text>
            <View>
              <Text style={styles.techTitle}>边界自适配</Text>
              <Text style={styles.techDesc}>避开系统安全区域,防止元素溢出屏幕</Text>
            </View>
          </View>
          <View style={styles.techItem}>
            <Text style={styles.techIcon}>📱</Text>
            <View>
              <Text style={styles.techTitle}>OpenHarmony专属兼容</Text>
              <Text style={styles.techDesc}>适配状态栏/底部安全区域,兼容API差异</Text>
            </View>
          </View>
        </View>
      </View>
    </View>
  );
};

// 全局样式定义,统一适配OpenHarmony
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  anchorWrapper: {
    position: 'relative',
  },
  // 导航栏样式
  navigationBar: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    backgroundColor: '#6366f1',
    paddingTop: 50, // 适配OpenHarmony状态栏
  },
  backBtn: {
    padding: 8,
  },
  backText: {
    fontSize: 16,
    color: '#fff',
    fontWeight: '600',
  },
  titleWrapper: {
    flex: 1,
    marginLeft: 8,
  },
  mainTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#fff',
  },
  subTitle: {
    fontSize: 12,
    color: 'rgba(255, 255, 255, 0.85)',
    marginTop: 2,
  },
  // 版本信息栏
  versionBanner: {
    backgroundColor: '#e0e7ff',
    padding: 12,
  },
  versionText: {
    fontSize: 12,
    color: '#4338ca',
    textAlign: 'center',
  },
  // 通用卡片样式
  card: {
    margin: 16,
    padding: 16,
    backgroundColor: '#fff',
    borderRadius: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.05,
    shadowRadius: 4,
  },
  cardTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#334155',
    marginBottom: 12,
  },
  cardDesc: {
    fontSize: 14,
    color: '#64748b',
    lineHeight: 20,
  },
  // 配置区域样式
  configSection: {
    marginBottom: 20,
  },
  configLabel: {
    fontSize: 14,
    color: '#64748b',
    marginBottom: 10,
  },
  optionRow: {
    flexDirection: 'row',
    gap: 8,
  },
  optionBtn: {
    flex: 1,
    paddingVertical: 12,
    borderRadius: 8,
    backgroundColor: '#f1f5f9',
    alignItems: 'center',
  },
  optionBtnActive: {
    backgroundColor: '#6366f1',
  },
  optionText: {
    fontSize: 13,
    color: '#64748b',
    fontWeight: '500',
  },
  optionTextActive: {
    color: '#fff',
  },
  currentConfig: {
    fontSize: 13,
    color: '#6366f1',
    textAlign: 'center',
    padding: 12,
    backgroundColor: '#eef2ff',
    borderRadius: 8,
    marginTop: 8,
  },
  // 演示区域样式
  demoArea: {
    alignItems: 'center',
    paddingVertical: 40,
  },
  anchorButton: {
    backgroundColor: '#6366f1',
    paddingHorizontal: 36,
    paddingVertical: 16,
    borderRadius: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.15,
    shadowRadius: 4,
  },
  anchorButtonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
  // Popover弹窗样式
  modalOverlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.3)',
  },
  popoverBox: {
    position: 'absolute',
    backgroundColor: '#fff',
    borderRadius: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.2,
    shadowRadius: 8,
    justifyContent: 'center',
  },
  popoverContent: {
    padding: 16,
    height: '100%',
    display: 'flex',
    flexDirection: 'column',
    gap: 12,
  },
  popoverTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#334155',
  },
  popoverDesc: {
    fontSize: 14,
    color: '#64748b',
    lineHeight: 20,
  },
  popoverActionBtn: {
    marginTop: 8,
    paddingVertical: 10,
    backgroundColor: '#6366f1',
    borderRadius: 8,
    alignItems: 'center',
  },
  popoverActionText: {
    color: '#fff',
    fontSize: 14,
    fontWeight: '500',
  },
  // 技术要点卡片
  techCard: {
    marginBottom: 32,
  },
  techList: {
    gap: 16,
  },
  techItem: {
    flexDirection: 'row',
    alignItems: 'flex-start',
    gap: 12,
  },
  techIcon: {
    fontSize: 24,
    marginTop: 2,
  },
  techTitle: {
    fontSize: 14,
    fontWeight: '600',
    color: '#334155',
  },
  techDesc: {
    fontSize: 12,
    color: '#64748b',
    lineHeight: 18,
    marginTop: 4,
  },
});

export default PositionControlDemoScreen;

四、核心实现要点解析

4.1 坐标计算解耦

水平对齐垂直对齐 的计算逻辑封装为独立工具函数getHorizontalPosgetVerticalPos,避免在switch中写重复代码,提升代码可维护性,后续扩展auto对齐策略时可直接复用。

4.2 灵活的配置项设计

通过props暴露偏移量Popover尺寸等配置项,避免硬编码,适配不同业务的视觉需求:

  • offset:自定义Popover与触发元素的间距,默认8px
  • popoverSize:自定义Popover的宽高,默认200*150px
  • placement/align:分别控制弹出方向和对齐方式,解耦位置配置

4.3 生命周期优化

通过useEffect监听显隐状态配置项屏幕尺寸的变化,仅在必要时重新计算位置,避免不必要的性能损耗:

typescript 复制代码
// 仅监听相关依赖,依赖变化才重新计算
useEffect(() => { ... }, [visible, placement, align, offset, popoverSize, screenWidth, screenHeight]);

4.4 样式归一化

  1. 所有样式通过StyleSheet.create定义,遵循RN性能优化原则,避免内联样式的性能问题
  2. 适配OpenHarmony的视觉规范,统一圆角、边距、阴影样式,保证跨设备视觉一致性
  3. 抽离通用样式(如cardoptionBtn),减少代码冗余

五、扩展与优化建议

5.1 扩展auto自动定位策略

基于现有边界检测逻辑,扩展auto策略:检测不同方向的屏幕剩余空间,自动选择空间最充足的方向弹出,适配动态布局场景。

5.2 支持Popover箭头指向

添加箭头组件,根据placementalign自动计算箭头的位置,让Popover与触发元素的视觉关联更紧密。

5.3 屏幕旋转适配

监听屏幕旋转事件(Dimensions.addEventListener('change', ...)),屏幕尺寸变化时重新计算Popover位置,适配横屏/竖屏切换场景。

5.4 触摸穿透处理

在OpenHarmony中,Modal遮罩层可能存在触摸穿透问题,可添加pointerEvents: 'box-none'样式或使用手势拦截解决。

5.5 动画优化

当前使用fade淡入淡出动画,可扩展位移动画 (如从触发元素滑出),通过Animated库实现,提升交互体验。

六、项目源码与资源

总结

在OpenHarmony平台实现React Native Popover的位置精准控制,核心是选对测量API处理系统安全区域做好边界检测,同时通过解耦坐标计算逻辑、设计灵活的配置项,提升组件的通用性和可维护性。本文实现的组件已适配OpenHarmony 6.0.0核心特性,可直接在项目中复用,也可基于此扩展更多个性化功能,满足不同的业务交互需求。


✨ 坚持用 清晰的图解 +易懂的硬件架构 + 硬件解析, 让每个知识点都 简单明了 !

🚀 个人主页一只大侠的侠 · CSDN

💬 座右铭 : "所谓成功就是以自己的方式度过一生。"

相关推荐
松叶似针2 小时前
Flutter三方库适配OpenHarmony【secure_application】— Window 管理与 getLastWindow API
flutter·harmonyos
●VON2 小时前
HarmonyOS应用开发实战(基础篇)Day05-《常见布局Row和Column》
学习·华为·harmonyos·鸿蒙·von
一只大侠的侠2 小时前
React Native for OpenHarmony:Calendar 日历组件实现指南
javascript·react native·react.js
前端不太难3 小时前
鸿蒙 App 架构重建后,为何再次失控
架构·状态模式·harmonyos
一只大侠的侠3 小时前
React Native for OpenHarmony:日期选择功能完整实现指南
javascript·react native·react.js
一只大侠的侠3 小时前
React Native实战:高性能StickyHeader粘性标题组件实现
javascript·react native·react.js
键盘鼓手苏苏3 小时前
Flutter for OpenHarmony:random_string 简单灵活的随机字符串生成器(验证码、密钥、UUID) 深度解析与鸿蒙适配指南
开发语言·flutter·华为·rust·harmonyos
无巧不成书02183 小时前
【RN鸿蒙教学|第9课时】数据更新+列表刷新实战:缓存与列表联动+多终端兼容闭环
react native·缓存·华为·交互·harmonyos
无巧不成书021811 小时前
【开源鸿蒙+Flutter实战】Step Two复盘(DAY8-14)|复杂页面落地·多终端适配·状态保持实战指南
flutter·开源·harmonyos