【HarmonyOS】React Native实战+Popover内容自适应

HarmonyOS实战:React Native实现Popover内容自适应


🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

概述

Popover组件的内容大小通常是不固定的,根据展示内容的不同,Popover的尺寸需要动态调整。实现内容自适应能够让Popover更好地适应各种使用场景,提供更好的用户体验。

在OpenHarmony 6.0.0平台上,由于布局计算的差异,Popover内容自适应需要特别处理尺寸测量和边界检测。本文将深入讲解如何实现一个内容自适应的Popover组件。

自适应原理

尺寸计算流程

复制代码
内容尺寸自适应流程:

初始状态
    │
    ▼
设置固定宽度或最大宽度
    │
    ▼
渲染内容获取实际高度 (onLayout)
    │
    ▼
根据内容高度调整Popover位置
    │
    ▼
边界检测确保不超出屏幕
    │
    ▼
应用最终尺寸和位置

尺寸策略

策略 宽度 高度 适用场景
fixed 固定值 固定值 内容固定
width-fixed 固定值 自适应 宽度固定内容变化
max-constrained 最大值 自适应 防止过宽
full-auto 自适应 自适应 完全自定义

核心实现

动态尺寸测量

typescript 复制代码
const [popoverSize, setPopoverSize] = useState({ width: 0, height: 0 });

const handleLayout = (event: LayoutChangeEvent) => {
  const { width, height } = event.nativeEvent.layout;
  setPopoverSize({ width, height });
  // 根据实际尺寸重新计算位置
};

位置重新计算

typescript 复制代码
useEffect(() => {
  if (popoverSize.width > 0 && popoverSize.height > 0) {
    // 根据实际内容尺寸调整位置
    recalculatePosition(popoverSize);
  }
}, [popoverSize]);

完整实现代码

typescript 复制代码
/**
 * HarmonyOS实战:Popover内容自适应
 *
 * @platform OpenHarmony 6.0.0 (API 20)
 * @react-native 0.72.5
 * @typescript 4.8.4
 */

import React, { useState, useRef, useEffect } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  Modal,
  Dimensions,
  TextInput,
} from 'react-native';

interface AdaptivePopoverProps {
  visible: boolean;
  anchor: JSX.Element;
  children: React.ReactNode;
  onClose: () => void;
  maxWidth?: number;
}

interface AdaptivePopoverDemoProps {
  onBack: () => void;
}

// 自适应内容的Popover组件
const AdaptivePopover: React.FC<AdaptivePopoverProps> = ({
  visible,
  anchor,
  children,
  onClose,
  maxWidth = 280,
}) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [contentSize, setContentSize] = useState({ width: 0, height: 0 });
  const anchorRef = useRef<View>(null);
  const contentRef = useRef<View>(null);

  // 测量内容尺寸
  const handleContentLayout = (event: any) => {
    const { width, height } = event.nativeEvent.layout;
    setContentSize({ width, height });
  };

  // 计算位置
  useEffect(() => {
    if (visible && anchorRef.current) {
      anchorRef.current.measureInWindow((x, y, anchorWidth, anchorHeight) => {
        const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
        const offset = 8;

        // 初始位置(下方居中)
        let posX = x + anchorWidth / 2 - maxWidth / 2;
        let posY = y + anchorHeight + offset;

        // 根据实际内容尺寸调整位置
        if (contentSize.width > 0 && contentSize.height > 0) {
          posX = x + anchorWidth / 2 - contentSize.width / 2;
        }

        // 边界检测
        const actualWidth = contentSize.width || maxWidth;
        const actualHeight = contentSize.height || 150;
        const safeAreaTop = 50;
        const safeAreaBottom = 34;

        posX = Math.max(16, Math.min(posX, screenWidth - actualWidth - 16));
        posY = Math.max(safeAreaTop + 16, Math.min(posY, screenHeight - actualHeight - safeAreaBottom - 16));

        setPosition({ x: posX, y: posY });
      });
    }
  }, [visible, contentSize, maxWidth]);

  return (
    <View style={styles.anchorWrapper}>
      <View ref={anchorRef}>{anchor}</View>
      <Modal
        visible={visible}
        transparent
        animationType="fade"
        onRequestClose={onClose}
      >
        <TouchableOpacity style={styles.modalOverlay} onPress={onClose} activeOpacity={1}>
          <TouchableOpacity
            style={[
              styles.popoverBox,
              {
                left: position.x,
                top: position.y,
                maxWidth: maxWidth,
              },
            ]}
            activeOpacity={1}
          >
            <View ref={contentRef} onLayout={handleContentLayout}>
              {children}
            </View>
          </TouchableOpacity>
        </TouchableOpacity>
      </Modal>
    </View>
  );
};

// 演示页面
const AdaptivePopoverDemoScreen: React.FC<AdaptivePopoverDemoProps> = ({ onBack }) => {
  const [visible1, setVisible1] = useState(false);
  const [visible2, setVisible2] = useState(false);
  const [visible3, setVisible3] = useState(false);
  const [inputText, setInputText] = useState('');

  return (
    <View style={styles.container}>
      {/* 顶部导航栏 */}
      <View style={styles.navigationBar}>
        <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}>动态调整内容尺寸</Text>
        </View>
      </View>

      {/* 平台信息 */}
      <View style={styles.versionBanner}>
        <Text style={styles.versionText}>OpenHarmony 6.0.0 | API 20</Text>
      </View>

      {/* 功能介绍 */}
      <View style={styles.introCard}>
        <Text style={styles.introTitle}>内容自适应Popover</Text>
        <Text style={styles.introDesc}>
          Popover根据内容动态调整尺寸,确保完整显示所有信息
        </Text>
      </View>

      {/* 演示区域 */}
      <View style={styles.demoSection}>
        <Text style={styles.demoTitle}>不同内容长度演示</Text>

        <View style={styles.demoGrid}>
          {/* 短内容 */}
          <AdaptivePopover
            visible={visible1}
            anchor={
              <TouchableOpacity
                style={[styles.demoButton, { backgroundColor: '#3b82f6' }]}
                onPress={() => setVisible1(true)}
              >
                <Text style={styles.demoButtonText}>短内容</Text>
              </TouchableOpacity>
            }
            onClose={() => setVisible1(false)}
          >
            <View style={styles.popoverContent}>
              <Text style={styles.popoverText}>这是一段简短的提示信息</Text>
            </View>
          </AdaptivePopover>

          {/* 中等长度 */}
          <AdaptivePopover
            visible={visible2}
            anchor={
              <TouchableOpacity
                style={[styles.demoButton, { backgroundColor: '#10b981' }]}
                onPress={() => setVisible2(true)}
              >
                <Text style={styles.demoButtonText}>中等内容</Text>
              </TouchableOpacity>
            }
            onClose={() => setVisible2(false)}
          >
            <View style={styles.popoverContent}>
              <Text style={styles.popoverTitle}>操作选项</Text>
              <TouchableOpacity style={styles.popoverItem}>
                <Text style={styles.popoverItemText}>📄 新建文档</Text>
              </TouchableOpacity>
              <TouchableOpacity style={styles.popoverItem}>
                <Text style={styles.popoverItemText}>📁 打开文件夹</Text>
              </TouchableOpacity>
              <TouchableOpacity style={styles.popoverItem}>
                <Text style={styles.popoverItemText}>💾 保存文件</Text>
              </TouchableOpacity>
            </View>
          </AdaptivePopover>
        </View>

        {/* 动态输入内容 */}
        <View style={styles.inputDemo}>
          <Text style={styles.inputLabel}>动态内容演示:</Text>
          <TextInput
            style={styles.textInput}
            placeholder="输入内容..."
            value={inputText}
            onChangeText={setInputText}
            multiline
          />
          <AdaptivePopover
            visible={visible3}
            maxWidth={320}
            anchor={
              <TouchableOpacity
                style={styles.triggerButton}
                onPress={() => setVisible3(true)}
              >
                <Text style={styles.triggerButtonText}>显示Popover</Text>
              </TouchableOpacity>
            }
            onClose={() => setVisible3(false)}
          >
            <View style={styles.popoverContent}>
              <Text style={styles.popoverTitle}>动态内容</Text>
              <Text style={styles.popoverText}>
                {inputText || '请在上方的输入框中输入内容,然后点击按钮查看效果'}
              </Text>
            </View>
          </AdaptivePopover>
        </View>
      </View>

      {/* 技术要点 */}
      <View style={styles.techCard}>
        <Text style={styles.cardTitle}>核心技术</Text>
        <View style={styles.techList}>
          <View style={styles.techItem}>
            <Text style={styles.techIcon}>📏</Text>
            <View style={styles.techContent}>
              <Text style={styles.techTitle}>onLayout监听</Text>
              <Text style={styles.techDesc}>获取内容实际渲染尺寸</Text>
            </View>
          </View>
          <View style={styles.techItem}>
            <Text style={styles.techIcon}>🔄</Text>
            <View style={styles.techContent}>
              <Text style={styles.techTitle}>动态位置计算</Text>
              <Text style={styles.techDesc}>根据尺寸调整弹出位置</Text>
            </View>
          </View>
          <View style={styles.techItem}>
            <Text style={styles.techIcon}>✓</Text>
            <View style={styles.techContent}>
              <Text style={styles.techTitle}>边界约束</Text>
              <Text style={styles.techDesc}>确保不超出可视区域</Text>
            </View>
          </View>
        </View>
      </View>

      {/* 适配要点 */}
      <View style={styles.adaptCard}>
        <Text style={styles.adaptTitle}>OpenHarmony适配要点</Text>
        <View style={styles.adaptList}>
          <Text style={styles.adaptItem}>• 使用onLayout获取实际尺寸</Text>
          <Text style={styles.adaptItem}>• 设置maxWidth防止过宽</Text>
          <Text style={styles.adaptItem}>• 内容变化时重新计算位置</Text>
          <Text style={styles.adaptItem}>• 测试长文本和特殊字符</Text>
        </View>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  navigationBar: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    backgroundColor: '#8b5cf6',
    paddingTop: 50,
  },
  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: '#ede9fe',
    paddingHorizontal: 16,
    paddingVertical: 8,
  },
  versionText: {
    fontSize: 12,
    color: '#6d28d9',
    textAlign: 'center',
  },
  introCard: {
    margin: 16,
    padding: 16,
    backgroundColor: '#fff',
    borderRadius: 12,
  },
  introTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#334155',
    marginBottom: 8,
  },
  introDesc: {
    fontSize: 14,
    color: '#64748b',
  },
  demoSection: {
    padding: 16,
  },
  demoTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#334155',
    marginBottom: 16,
  },
  demoGrid: {
    flexDirection: 'row',
    gap: 12,
    marginBottom: 24,
  },
  demoButton: {
    paddingHorizontal: 20,
    paddingVertical: 14,
    borderRadius: 10,
    minWidth: 110,
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.15,
    shadowRadius: 4,
    elevation: 3,
  },
  demoButtonText: {
    color: '#fff',
    fontSize: 14,
    fontWeight: '600',
  },
  inputDemo: {
    backgroundColor: '#fff',
    padding: 16,
    borderRadius: 12,
  },
  inputLabel: {
    fontSize: 14,
    color: '#64748b',
    marginBottom: 10,
  },
  textInput: {
    backgroundColor: '#f1f5f9',
    borderRadius: 8,
    padding: 12,
    fontSize: 14,
    color: '#334155',
    minHeight: 80,
    marginBottom: 12,
    textAlignVertical: 'top',
  },
  triggerButton: {
    backgroundColor: '#8b5cf6',
    paddingVertical: 12,
    borderRadius: 8,
    alignItems: 'center',
  },
  triggerButtonText: {
    color: '#fff',
    fontSize: 15,
    fontWeight: '600',
  },
  anchorWrapper: {
    position: 'relative',
  },
  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,
    elevation: 8,
  },
  popoverContent: {
    padding: 16,
  },
  popoverTitle: {
    fontSize: 15,
    fontWeight: 'bold',
    color: '#334155',
    marginBottom: 12,
  },
  popoverText: {
    fontSize: 14,
    color: '#64748b',
    lineHeight: 20,
  },
  popoverItem: {
    paddingVertical: 10,
    paddingHorizontal: 12,
    borderRadius: 6,
  },
  popoverItemText: {
    fontSize: 15,
    color: '#334155',
  },
  techCard: {
    backgroundColor: '#fff',
    margin: 16,
    padding: 16,
    borderRadius: 12,
  },
  cardTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#334155',
    marginBottom: 12,
  },
  techList: {
    gap: 12,
  },
  techItem: {
    flexDirection: 'row',
    alignItems: 'flex-start',
  },
  techIcon: {
    fontSize: 24,
    marginRight: 12,
  },
  techContent: {
    flex: 1,
  },
  techTitle: {
    fontSize: 14,
    fontWeight: '600',
    color: '#334155',
    marginBottom: 4,
  },
  techDesc: {
    fontSize: 12,
    color: '#64748b',
    lineHeight: 18,
  },
  adaptCard: {
    backgroundColor: '#f3e8ff',
    margin: 16,
    marginBottom: 32,
    padding: 16,
    borderRadius: 12,
  },
  adaptTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#7c3aed',
    marginBottom: 12,
  },
  adaptList: {
    gap: 6,
  },
  adaptItem: {
    fontSize: 13,
    color: '#4b5563',
    lineHeight: 20,
  },
});

export default AdaptivePopoverDemoScreen;

核心实现要点

1. 内容尺寸测量

typescript 复制代码
const handleContentLayout = (event: LayoutChangeEvent) => {
  const { width, height } = event.nativeEvent.layout;
  setContentSize({ width, height });
};

2. 动态位置调整

typescript 复制代码
useEffect(() => {
  if (contentSize.width > 0) {
    posX = x + anchorWidth / 2 - contentSize.width / 2;
  }
}, [contentSize]);

3. 宽度约束

typescript 复制代码
// 设置最大宽度防止过宽
<Popover maxWidth={280}>
  {children}
</Popover>

OpenHarmony适配要点

问题 解决方案
尺寸测量延迟 使用onLayout而非预估
位置偏移 根据实际尺寸重新计算
边界溢出 动态调整位置
性能优化 缓存测量结果

项目源码

完整项目代码:https://atomgit.com/lbbxmx111/AtomGitNewsDemo

开源鸿蒙社区:https://openharmonycrossplatform.csdn.net

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

📕个人领域 :Linux/C++/java/AI

🚀 个人主页有点流鼻涕 · CSDN

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
maaath1 小时前
【maaath】Flutter for OpenHarmony 乐器学习应用开发实战
flutter·华为·harmonyos
沐言人生2 小时前
React Native 源码分析1——HybridData 机制深度分析
android·react native
空中海3 小时前
01 React Native 基础、核心组件与布局体系
javascript·react native·react.js
数智顾问3 小时前
(123页PPT)华为流程管理体系精髓提炼(附下载方式)
运维·华为
李游Leo4 小时前
HarmonyOS AbilityStage 实战:别把启动参数散落在每个页面里
harmonyos
Yue1685 小时前
一文教你五分钟学会Zustand,React状态管理更加方便!
react native
空中海5 小时前
03 性能、动画与 React Native 新架构
react native·react.js·架构
李李李勃谦6 小时前
鸿蒙PCBI 报表工具:连接数据库与可视化报表生成
数据库·华为·交互·harmonyos
maaath7 小时前
【maaath】 Flutter for OpenHarmony 实战:电池优化应用开发指南
flutter·华为·harmonyos
空中海7 小时前
02 React Native状态、导航、数据流与设备能力
javascript·react native·react.js