React Native for OpenHarmony 实战:DisplayInfo 显示信息详解

React Native for OpenHarmony 实战:DisplayInfo 显示信息详解

摘要

本文深入探讨 React Native for OpenHarmony 中 DisplayInfo 显示信息的使用方法与适配技巧。作为跨平台开发的关键环节,准确获取设备显示信息对实现响应式布局至关重要。文章详细解析了 DisplayInfo 的核心概念、基础与进阶用法,并通过实战案例展示了在 OpenHarmony 设备上的具体应用。通过本文,开发者将掌握在 OpenHarmony 平台上高效使用 DisplayInfo 的最佳实践,解决跨平台显示适配的常见痛点,包括折叠屏适配、多窗口模式处理和安全区域计算等关键问题,为构建高质量的跨平台应用奠定坚实基础。

引言

在移动应用开发中,屏幕适配始终是一个重要课题。随着 OpenHarmony 生态的快速发展,越来越多的开发者开始关注如何在这一新兴平台上进行高效开发。React Native 作为广受欢迎的跨平台框架,其与 OpenHarmony 的结合为开发者提供了新的可能性。

作为一位拥有 5 年 React Native 开发经验的工程师,我在最近的 OpenHarmony 项目中深入研究了 DisplayInfo 的使用方法。记得在一次实际项目中,我们遇到了一个棘手的问题:应用在折叠屏设备上展开时 UI 布局错乱,横竖屏切换时内容被状态栏遮挡。经过深入研究,我发现问题的根源在于对 OpenHarmony 显示信息获取机制的理解不足。

DisplayInfo 作为 OpenHarmony 提供的显示信息获取 API,在实现响应式布局、适配不同屏幕尺寸和分辨率方面发挥着关键作用。然而,由于 React Native 原生的 Dimensions API 与 OpenHarmony 的 DisplayInfo 存在差异,开发者在实际使用中常常遇到各种适配问题。

本文将分享我在 OpenHarmony 项目中的实战经验,包括如何正确获取屏幕尺寸、处理安全区域、适配折叠屏设备等。通过详细的技术解析和可运行的代码示例,帮助你避免我曾经踩过的坑,高效开发出在 OpenHarmony 设备上表现优异的 React Native 应用。

DisplayInfo 核心概念介绍

什么是 DisplayInfo

DisplayInfo 是 OpenHarmony 系统中用于获取设备显示信息的核心 API。它提供了丰富的设备显示参数,包括但不限于:

  • 屏幕尺寸(宽度和高度)
  • 分辨率
  • 像素密度(DPI)
  • 屏幕方向
  • 安全区域
  • 多窗口模式信息
  • 折叠屏状态

在 OpenHarmony 中,DisplayInfo 是通过系统服务获取的,它能够准确反映当前设备的显示状态,特别是在处理折叠屏、多窗口等复杂场景时表现出色。

与 React Native 原生的 Dimensions API 不同,DisplayInfo 是 OpenHarmony 平台特有的 API,提供了更精确、更全面的显示信息。在 React Native for OpenHarmony 开发中,我们需要将两者结合起来使用,以实现最佳的跨平台体验。

DisplayInfo 的主要功能和属性

DisplayInfo 提供了多个关键属性,这些属性对于实现响应式布局至关重要:

属性 类型 描述 OpenHarmony 特有
width number 屏幕宽度(像素)
height number 屏幕高度(像素)
density number 像素密度(DPI) ⚠️(更精确)
scaledDensity number 缩放密度
xdpi number 水平 DPI
ydpi number 垂直 DPI
orientation string 屏幕方向('portrait' | 'landscape') ⚠️(更精细)
rotation number 屏幕旋转角度(0-360)
safeArea object 安全区域信息
foldState string 折叠屏状态
hingeAngle number 铰链角度(0-180°)
isTabletopMode boolean 是否桌面模式
windowWidth number 当前窗口宽度
windowHeight number 当前窗口高度

这些属性使得开发者能够精确控制 UI 布局,适配各种不同的设备和场景。特别是对于 OpenHarmony 特有的功能,如折叠屏支持和多窗口模式,DisplayInfo 提供了 React Native Dimensions API 无法获取的关键信息。

为什么在 React Native 中需要使用 DisplayInfo

在 React Native 应用开发中,我们通常使用 Dimensions API 来获取设备的显示信息。然而,在 OpenHarmony 平台上,由于系统架构和 API 设计的差异,原生的 Dimensions API 可能无法准确获取所有必要的显示信息,特别是在处理折叠屏、多窗口等特殊场景时。

DisplayInfo 作为 OpenHarmony 平台的原生 API,能够提供更全面、更准确的显示信息,帮助开发者解决以下问题:

  1. 精确的屏幕尺寸获取:在某些设备上,Dimensions API 可能无法准确区分状态栏、导航栏等系统 UI 所占用的空间,而 DisplayInfo 提供了安全区域信息,可以更精确地进行布局。

  2. 折叠屏设备适配:OpenHarmony 的 DisplayInfo 能够检测到折叠屏的状态变化,提供当前展开的屏幕区域信息,这对于开发折叠屏应用至关重要。例如,当设备从折叠状态变为展开状态时,应用需要能够动态调整布局以充分利用更大的屏幕空间。

  3. 多窗口模式支持:在多窗口模式下,Dimensions API 通常只能获取整个屏幕的尺寸,而无法获取当前应用窗口的实际尺寸。DisplayInfo 则能够提供当前窗口的精确尺寸,使应用能够在有限的空间内提供最佳用户体验。

  4. 高 DPI 设备适配:DisplayInfo 提供了详细的 DPI 信息,包括水平和垂直方向的 DPI,帮助开发者更好地处理高分辨率设备的显示问题,确保图像和文本的清晰度。

  5. 桌面模式检测:对于支持桌面模式的折叠屏设备,DisplayInfo 可以检测设备是否处于桌面模式(半折叠状态),使应用能够提供适合该模式的 UI 布局。

与 React Native Dimensions API 的关系

React Native 的 Dimensions API 是一个跨平台的 API,用于获取设备的屏幕尺寸和分辨率。它提供了 getaddEventListener 等方法,可以方便地获取和监听屏幕尺寸的变化。

然而,Dimensions API 在不同平台上的实现有所不同:

  • 在 iOS 和 Android 上,Dimensions API 基于原生平台的 API 实现
  • 在 OpenHarmony 上,由于缺乏直接对应的支持,Dimensions API 可能无法提供完整或准确的信息

DisplayInfo 可以看作是 OpenHarmony 平台上 Dimensions API 的补充或替代方案。在 React Native for OpenHarmony 中,我们通常需要将两者结合起来使用:
OpenHarmony
iOS/Android
React Native App
Platform Check
Use DisplayInfo API
Use Dimensions API
Get Detailed Display Info
Get Basic Display Info
Responsive Layout

在实际开发中,我们可以通过创建一个统一的 API 层,根据平台自动选择使用 Dimensions API 还是 DisplayInfo,从而简化应用代码。这种封装方式不仅提高了代码的可维护性,还确保了在不同平台上的行为一致性。

React Native与OpenHarmony平台适配要点

React Native 原生支持的显示信息 API

React Native 提供了 Dimensions API 用于获取设备的显示信息:

javascript 复制代码
import { Dimensions } from 'react-native';

// 获取屏幕尺寸
const { width, height } = Dimensions.get('window');

// 监听尺寸变化
const subscription = Dimensions.addEventListener('change', ({ window }) => {
  console.log('New dimensions:', window);
});

Dimensions API 在 iOS 和 Android 上工作良好,但在 OpenHarmony 上存在以下限制:

  1. 无法准确获取安全区域信息:Dimensions API 在 OpenHarmony 上无法区分状态栏、导航栏等系统 UI 所占用的空间,导致内容可能被遮挡。

  2. 不支持折叠屏状态检测:OpenHarmony 的折叠屏设备需要特殊处理,但 Dimensions API 无法提供折叠状态信息。

  3. 多窗口模式支持不足:在多窗口模式下,Dimensions API 返回的是整个屏幕的尺寸,而非当前应用窗口的实际尺寸。

  4. DPI 信息不准确:Dimensions API 在 OpenHarmony 上可能无法提供准确的 DPI 信息,影响高分辨率设备的显示效果。

  5. 屏幕方向检测不精细:Dimensions API 只能区分横屏和竖屏,无法获取精确的旋转角度,这在桌面模式(半折叠)下尤为重要。

OpenHarmony 平台的特殊性

OpenHarmony 作为新兴的操作系统,在显示管理方面有其独特之处:

  1. 分布式架构:OpenHarmony 支持设备间的分布式能力,这意味着显示信息可能来自不同的物理设备。例如,应用可能在手机上运行,但显示在电视上,这种场景下需要特殊的显示信息处理。

  2. 折叠屏支持:OpenHarmony 对折叠屏设备有原生支持,DisplayInfo 能够反映当前的折叠状态,包括完全展开、半折叠(桌面模式)和完全折叠等状态。

  3. 多窗口模式:OpenHarmony 的多窗口实现与 Android 有所不同,应用可能在部分屏幕区域运行,需要根据窗口尺寸调整 UI。

  4. 自适应 UI 框架:OpenHarmony 提供了自适应 UI 框架,与 React Native 的响应式布局理念有相似之处,但也存在差异。例如,OpenHarmony 的自适应布局更注重设备能力的动态调整。

  5. 安全区域计算:OpenHarmony 设备可能有各种屏幕形状(如刘海屏、打孔屏、曲面屏等),安全区域计算更为复杂,需要精确获取各边缘的偏移量。

需要适配的关键方面

在将 React Native 应用迁移到 OpenHarmony 平台时,显示信息相关的适配主要包括:

  1. 安全区域处理:OpenHarmony 设备可能有不同的屏幕形状和系统 UI 布局,需要正确计算安全区域。例如,华为 Mate X 系列折叠屏设备在展开状态下有特殊的屏幕形状,需要特别处理。

  2. 折叠屏适配:当设备折叠状态变化时,应用需要能够动态调整布局。这包括检测折叠状态、处理展开/折叠过渡动画,以及为不同状态提供合适的 UI 布局。

  3. 多窗口模式支持:在多窗口模式下,应用应该能够根据可用空间调整 UI。例如,当窗口宽度小于 600px 时,应切换到紧凑布局;当宽度大于 992px 时,可以显示更丰富的 UI 元素。

  4. DPI 适配:不同 OpenHarmony 设备可能有不同的 DPI 设置,需要正确处理像素密度。例如,平板设备通常有更高的 DPI,需要提供更高分辨率的图像资源。

  5. 屏幕方向变化:OpenHarmony 对屏幕方向的处理可能与 Android 有所不同,特别是在桌面模式下,设备可能处于中间旋转角度。

OpenHarmony DisplayInfo 与 React Native 的集成方案

为了在 React Native for OpenHarmony 中有效使用 DisplayInfo,我们需要创建一个适配层。这个适配层应该:

  1. 封装原生模块:创建 React Native 原生模块,将 OpenHarmony 的 DisplayInfo API 暴露给 JavaScript。

  2. 统一 API 接口 :提供与 Dimensions API 类似的接口,降低迁移成本。例如,实现 getaddEventListener 方法。

  3. 处理平台差异:自动处理不同平台的差异,提供一致的 API。例如,在 OpenHarmony 上使用 DisplayInfo,在其他平台上使用 Dimensions。

  4. 事件监听机制:实现类似 Dimensions 的事件监听机制,支持动态尺寸变化。

下面是一个详细的架构图,展示了 React Native 与 OpenHarmony DisplayInfo 的集成方式:
调用
iOS/Android
OpenHarmony
React Native App
DisplayInfo API
平台判断
Dimensions API
DisplayInfo Native Module
OpenHarmony DisplayInfo Service
系统显示信息
基本显示信息
详细显示信息
响应式布局
适配的UI

在这个架构中,DisplayInfo API 会根据当前运行平台自动选择使用原生的 Dimensions API 还是 OpenHarmony 的 DisplayInfo 服务,为上层应用提供一致的接口。这种设计确保了代码的可移植性,同时充分利用了 OpenHarmony 平台的特有能力。

DisplayInfo基础用法实战

获取屏幕尺寸

获取屏幕尺寸是应用开发中最基本的需求之一。在 OpenHarmony 上,我们可以使用 DisplayInfo 来获取更精确的屏幕尺寸信息。

首先,我们需要创建一个原生模块来访问 OpenHarmony 的 DisplayInfo API:

javascript 复制代码
// displayInfo.js
import { NativeModules, Platform } from 'react-native';

const { DisplayInfoModule } = NativeModules;

/**
 * 获取当前显示信息
 * @returns {Promise<DisplayInfo>}
 */
export const getDisplayInfo = () => {
  if (Platform.OS === 'openharmony') {
    return DisplayInfoModule.getDisplayInfo();
  }
  // 对于其他平台,可以返回 Dimensions 的信息作为 fallback
  const { width, height } = Dimensions.get('window');
  return Promise.resolve({
    width,
    height,
    density: 1,
    orientation: width > height ? 'landscape' : 'portrait',
    safeArea: { top: 0, right: 0, bottom: 0, left: 0 }
  });
};

/**
 * DisplayInfo 接口定义
 * @typedef {Object} DisplayInfo
 * @property {number} width - 屏幕宽度(像素)
 * @property {number} height - 屏幕高度(像素)
 * @property {number} density - 像素密度
 * @property {string} orientation - 屏幕方向('portrait' | 'landscape')
 * @property {Object} safeArea - 安全区域
 * @property {number} safeArea.top - 顶部安全区域
 * @property {number} safeArea.right - 右侧安全区域
 * @property {number} safeArea.bottom - 底部安全区域
 * @property {number} safeArea.left - 左侧安全区域
 */

然后,我们可以在应用中使用这个 API:

javascript 复制代码
// ScreenInfo.js
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { getDisplayInfo } from './displayInfo';

const ScreenInfo = () => {
  const [displayInfo, setDisplayInfo] = useState(null);

  useEffect(() => {
    const loadDisplayInfo = async () => {
      try {
        const info = await getDisplayInfo();
        setDisplayInfo(info);
      } catch (error) {
        console.error('Failed to get display info:', error);
      }
    };

    loadDisplayInfo();
  }, []);

  if (!displayInfo) {
    return <Text>Loading...</Text>;
  }

  return (
    <View style={styles.container}>
      <Text style={styles.text}>Width: {displayInfo.width}px</Text>
      <Text style={styles.text}>Height: {displayInfo.height}px</Text>
      <Text style={styles.text}>Density: {displayInfo.density}</Text>
      <Text style={styles.text}>Orientation: {displayInfo.orientation}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  text: {
    fontSize: 18,
    marginVertical: 5,
  },
});

export default ScreenInfo;

代码解释:

  1. 我们创建了一个 getDisplayInfo 函数,它会根据当前平台返回相应的显示信息。

  2. 在 OpenHarmony 平台上,它会调用原生模块获取 DisplayInfo;在其他平台上,它会使用 Dimensions API 作为 fallback。

  3. 在组件中,我们使用 useEffect 钩子异步获取显示信息并更新状态。

  4. 显示信息包括屏幕宽度、高度、像素密度和方向。

OpenHarmony平台适配要点:

  • OpenHarmony 的 DisplayInfo 会返回物理像素值,而 React Native 的 Dimensions 返回的是逻辑像素值,需要注意单位转换。例如,如果设备的 density 是 2.625,那么 1080px 的物理宽度对应 411.43 逻辑像素。

  • 在 OpenHarmony 上,屏幕尺寸可能会随着折叠状态变化而动态改变,需要监听变化事件。例如,当折叠屏设备从折叠状态变为展开状态时,宽度可能从 600px 变为 1920px。

  • 多窗口模式下,返回的尺寸是当前窗口的尺寸,而非整个屏幕的尺寸。例如,在分屏模式下,应用可能只占用屏幕的一半。

获取安全区域

安全区域是指可以安全显示内容的区域,避开状态栏、导航栏等系统 UI。在 OpenHarmony 设备上,由于屏幕形状多样(如刘海屏、打孔屏等),正确处理安全区域尤为重要。

javascript 复制代码
// safeArea.js
import { NativeModules, Platform } from 'react-native';

const { DisplayInfoModule } = NativeModules;

/**
 * 获取安全区域
 * @returns {Promise<Object>}
 */
export const getSafeArea = () => {
  if (Platform.OS === 'openharmony') {
    return DisplayInfoModule.getSafeArea();
  }
  
  // 对于 iOS,可以使用 SafeAreaView 或 constants
  if (Platform.OS === 'ios') {
    const constants = require('react-native/Libraries/Utilities/Platform').Constants;
    return Promise.resolve({
      top: constants.statusBarHeight,
      bottom: 0, // 需要根据实际情况调整
      left: 0,
      right: 0
    });
  }
  
  // Android 和其他平台的 fallback
  return Promise.resolve({
    top: 0,
    bottom: 0,
    left: 0,
    right: 0
  });
};

/**
 * 使用安全区域创建样式
 * @param {Object} baseStyle - 基础样式
 * @returns {Object} 包含安全区域的样式
 */
export const withSafeArea = (baseStyle = {}) => {
  return async () => {
    const safeArea = await getSafeArea();
    return {
      ...baseStyle,
      paddingTop: baseStyle.paddingTop !== undefined 
        ? baseStyle.paddingTop + safeArea.top 
        : safeArea.top,
      paddingBottom: baseStyle.paddingBottom !== undefined 
        ? baseStyle.paddingBottom + safeArea.bottom 
        : safeArea.bottom,
      paddingLeft: baseStyle.paddingLeft !== undefined 
        ? baseStyle.paddingLeft + safeArea.left 
        : safeArea.left,
      paddingRight: baseStyle.paddingRight !== undefined 
        ? baseStyle.paddingRight + safeArea.right 
        : safeArea.right,
    };
  };
};

使用示例:

javascript 复制代码
// SafeAreaExample.js
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { withSafeArea } from './safeArea';

const SafeAreaExample = () => {
  const [containerStyle, setContainerStyle] = useState({});

  useEffect(() => {
    const loadStyle = async () => {
      const style = await withSafeArea({
        flex: 1,
        backgroundColor: '#f0f0f0',
        alignItems: 'center',
        justifyContent: 'center'
      })();
      setContainerStyle(style);
    };

    loadStyle();
  }, []);

  return (
    <View style={containerStyle}>
      <Text style={styles.text}>This content is within the safe area</Text>
      <Text style={styles.subText}>Top: Status bar area</Text>
      <Text style={styles.subText}>Bottom: Navigation bar area</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  text: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  subText: {
    fontSize: 16,
    color: '#666',
  },
});

export default SafeAreaExample;

代码解释:

  1. getSafeArea 函数获取当前设备的安全区域信息,包括顶部(状态栏)、底部(导航栏)、左右侧(打孔区域)的偏移。

  2. withSafeArea 函数用于将安全区域应用到现有样式中,自动计算并添加必要的内边距。

  3. 在组件中,我们异步获取安全区域样式并应用到容器,确保内容不会被系统 UI 遮挡。

OpenHarmony平台适配要点:

  • OpenHarmony 的 DisplayInfo 会提供精确的安全区域信息,这些值可能因设备型号而异。例如,华为 Mate X3 的展开状态下,顶部安全区域可能为 44px,底部为 34px。

  • 在折叠屏设备上,安全区域可能会随着折叠状态变化而动态改变。例如,当设备从折叠状态变为展开状态时,顶部安全区域可能从 44px 变为 0px。

  • 多窗口模式下,安全区域需要根据窗口位置重新计算。例如,当应用位于屏幕左侧时,右侧安全区域可能为 0。

  • 安全区域的值是物理像素,需要根据 density 转换为逻辑像素。例如,如果 density 是 2.625,44px 物理像素对应约 16.76 逻辑像素。

获取屏幕方向

屏幕方向是响应式布局的重要参考因素。在 OpenHarmony 上,我们可以使用 DisplayInfo 获取更精确的屏幕方向信息。

javascript 复制代码
// orientation.js
import { NativeModules, Platform, Dimensions } from 'react-native';

const { DisplayInfoModule } = NativeModules;

/**
 * 屏幕方向类型
 * @typedef {'portrait' | 'landscape' | 'unknown'} Orientation
 */

/**
 * 获取当前屏幕方向
 * @returns {Promise<Orientation>}
 */
export const getOrientation = () => {
  if (Platform.OS === 'openharmony') {
    return DisplayInfoModule.getOrientation();
  }
  
  // 使用 Dimensions 作为 fallback
  const { width, height } = Dimensions.get('window');
  return Promise.resolve(width > height ? 'landscape' : 'portrait');
};

/**
 * 监听屏幕方向变化
 * @param {(orientation: Orientation) => void} callback
 * @returns {Object} 取消监听的方法
 */
export const addOrientationListener = (callback) => {
  if (Platform.OS === 'openharmony') {
    const subscription = DisplayInfoModule.addOrientationListener(callback);
    return {
      remove: () => DisplayInfoModule.removeOrientationListener(subscription)
    };
  }
  
  // 使用 Dimensions 监听作为 fallback
  const dimensionsSubscription = Dimensions.addEventListener('change', ({ window }) => {
    callback(window.width > window.height ? 'landscape' : 'portrait');
  });
  
  return {
    remove: () => dimensionsSubscription?.remove?.()
  };
};

使用示例:

javascript 复制代码
// OrientationExample.js
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, Platform } from 'react-native';
import { getOrientation, addOrientationListener } from './orientation';

const OrientationExample = () => {
  const [orientation, setOrientation] = useState('unknown');

  useEffect(() => {
    let isMounted = true;
    
    const loadOrientation = async () => {
      try {
        const currentOrientation = await getOrientation();
        if (isMounted) {
          setOrientation(currentOrientation);
        }
      } catch (error) {
        console.error('Failed to get orientation:', error);
      }
    };

    loadOrientation();
    
    const orientationListener = addOrientationListener((newOrientation) => {
      if (isMounted) {
        setOrientation(newOrientation);
      }
    });

    return () => {
      isMounted = false;
      orientationListener.remove();
    };
  }, []);

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Screen Orientation</Text>
      <View style={[styles.indicator, 
        orientation === 'landscape' ? styles.landscape : styles.portrait]}>
        <Text style={styles.orientationText}>{orientation.toUpperCase()}</Text>
      </View>
      <Text style={styles.instructions}>
        {orientation === 'portrait' 
          ? 'Rotate your device to landscape mode' 
          : 'Rotate your device to portrait mode'}
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 30,
  },
  indicator: {
    width: 200,
    height: 200,
    borderRadius: 10,
    justifyContent: 'center',
    alignItems: 'center',
    marginBottom: 20,
  },
  portrait: {
    backgroundColor: '#4CAF50',
  },
  landscape: {
    backgroundColor: '#2196F3',
    width: 300,
    height: 150,
  },
  orientationText: {
    color: 'white',
    fontSize: 24,
    fontWeight: 'bold',
  },
  instructions: {
    fontSize: 18,
    textAlign: 'center',
    color: '#666',
  },
});

export default OrientationExample;

代码解释:

  1. getOrientation 函数获取当前屏幕方向,返回 'portrait'、'landscape' 或 'unknown'。

  2. addOrientationListener 函数用于监听屏幕方向变化,当方向改变时触发回调。

  3. 在组件中,我们首先获取初始方向,然后设置监听器,当方向变化时更新 UI 显示。

  4. 我们使用不同的样式来表示横屏和竖屏状态,提供直观的视觉反馈。

OpenHarmony平台适配要点:

  • OpenHarmony 的 DisplayInfo 提供了更精确的方向信息,包括 90°、180°、270° 等旋转角度,而不仅仅是横屏/竖屏。这对于桌面模式(半折叠)尤为重要。

  • 在折叠屏设备上,方向变化可能与折叠状态相关联。例如,当设备从折叠状态变为展开状态时,方向可能从竖屏变为横屏。

  • 多窗口模式下,方向信息可能与整个屏幕的方向不同。例如,当应用在竖屏设备上以横屏窗口运行时,应用的方向是横屏,而整个设备的方向是竖屏。

  • 方向变化事件可能非常频繁,特别是在桌面模式下,需要适当节流处理以避免性能问题。

处理像素密度

像素密度(DPI)对于处理高分辨率设备的显示至关重要。在 OpenHarmony 上,我们可以使用 DisplayInfo 获取准确的像素密度信息。

javascript 复制代码
// density.js
import { NativeModules, Platform, PixelRatio } from 'react-native';

const { DisplayInfoModule } = NativeModules;

/**
 * 获取像素密度信息
 * @returns {Promise<Object>}
 */
export const getDensityInfo = () => {
  if (Platform.OS === 'openharmony') {
    return DisplayInfoModule.getDensityInfo();
  }
  
  // 使用 PixelRatio 作为 fallback
  return Promise.resolve({
    density: PixelRatio.get(),
    fontScale: PixelRatio.getFontScale(),
    dpi: PixelRatio.get()*160
  });
};

/**
 * 根据像素密度调整尺寸
 * @param {number} size - 基础尺寸
 * @returns {number} 调整后的尺寸
 */
export const scaleSize = (size) => {
  return getDensityInfo().then(info => {
    // 在 OpenHarmony 上,我们可能需要不同的缩放逻辑
    if (Platform.OS === 'openharmony') {
      // OpenHarmony 可能使用不同的缩放因子
      return size * (info.density / 1.5); // 假设 OpenHarmony 的基准密度是 1.5
    }
    // 其他平台使用标准的 PixelRatio
    return PixelRatio.roundToNearestPixel(size);
  });
};

使用示例:

javascript 复制代码
// DensityExample.js
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { getDensityInfo, scaleSize } from './density';

const DensityExample = () => {
  const [densityInfo, setDensityInfo] = useState(null);
  const [scaledSize, setScaledSize] = useState(0);

  useEffect(() => {
    const loadDensityInfo = async () => {
      try {
        const info = await getDensityInfo();
        setDensityInfo(info);
        
        // 测试缩放一个 100px 的尺寸
        const size = await scaleSize(100);
        setScaledSize(size);
      } catch (error) {
        console.error('Failed to get density info:', error);
      }
    };

    loadDensityInfo();
  }, []);

  if (!densityInfo) {
    return <Text>Loading density info...</Text>;
  }

  return (
    <View style={styles.container}>
      <Text style={styles.info}>Density: {densityInfo.density}</Text>
      <Text style={styles.info}>Font Scale: {densityInfo.fontScale}</Text>
      <Text style={styles.info}>DPI: {densityInfo.dpi}</Text>
      
      <View style={styles.sizeContainer}>
        <Text style={styles.label}>Original size: 100px</Text>
        <View style={[styles.sizeBox, { width: 100, height: 100 }]} />
        
        <Text style={styles.label}>Scaled size: {scaledSize}px</Text>
        <View style={[styles.sizeBox, { width: scaledSize, height: scaledSize }]} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    justifyContent: 'center',
  },
  info: {
    fontSize: 18,
    marginVertical: 5,
  },
  sizeContainer: {
    marginTop: 30,
    alignItems: 'center',
  },
  label: {
    fontSize: 16,
    marginTop: 10,
    fontWeight: 'bold',
  },
  sizeBox: {
    backgroundColor: '#3F51B5',
    marginVertical: 10,
  },
});

export default DensityExample;

代码解释:

  1. getDensityInfo 函数获取像素密度相关信息,包括 density、fontScale 和 dpi。

  2. scaleSize 函数根据像素密度调整尺寸,确保在不同 DPI 设备上显示一致。

  3. 在组件中,我们展示原始密度信息和缩放后的尺寸效果,直观地展示 DPI 适配的效果。

OpenHarmony平台适配要点:

  • OpenHarmony 的 DisplayInfo 提供了更详细的 DPI 信息,包括水平和垂直方向的 DPI,这在某些设备上可能不同(如长宽比特殊的屏幕)。

  • 在 OpenHarmony 上,可能需要使用不同的缩放基准(例如,基准密度可能不是 160 DPI)。例如,某些平板设备可能使用 1.5 作为基准密度。

  • 高 DPI 设备上,需要注意文本和图像的清晰度。对于图像资源,应提供 @2x、@3x 等不同分辨率的版本。

  • 字体缩放(fontScale)可能受系统设置影响,应尊重用户的可访问性偏好。

DisplayInfo进阶用法

动态监听显示信息变化

在移动应用中,显示信息可能会动态变化,例如屏幕旋转、窗口大小调整、折叠状态变化等。我们需要能够监听这些变化并相应地更新 UI。

javascript 复制代码
// displayInfo.js (扩展)
/**
 * 监听显示信息变化
 * @param {(displayInfo: DisplayInfo) => void} callback
 * @returns {Object} 取消监听的方法
 */
export const addDisplayInfoListener = (callback) => {
  if (Platform.OS === 'openharmony') {
    const subscription = DisplayInfoModule.addDisplayInfoListener((info) => {
      callback({
        width: info.width,
        height: info.height,
        density: info.density,
        orientation: info.orientation,
        safeArea: info.safeArea
      });
    });
    
    return {
      remove: () => DisplayInfoModule.removeDisplayInfoListener(subscription)
    };
  }
  
  // 使用 Dimensions 监听作为 fallback
  const dimensionsSubscription = Dimensions.addEventListener('change', ({ window }) => {
    callback({
      width: window.width,
      height: window.height,
      density: 1,
      orientation: window.width > window.height ? 'landscape' : 'portrait',
      safeArea: { top: 0, right: 0, bottom: 0, left: 0 }
    });
  });
  
  return {
    remove: () => dimensionsSubscription?.remove?.()
  };
};

/**
 * 使用显示信息的 Hook
 * @returns {DisplayInfo}
 */
export const useDisplayInfo = () => {
  const [displayInfo, setDisplayInfo] = useState(null);

  useEffect(() => {
    let isMounted = true;
    
    const loadDisplayInfo = async () => {
      try {
        const info = await getDisplayInfo();
        if (isMounted) {
          setDisplayInfo(info);
        }
      } catch (error) {
        console.error('Failed to get display info:', error);
      }
    };

    loadDisplayInfo();
    
    const listener = addDisplayInfoListener((info) => {
      if (isMounted) {
        setDisplayInfo(info);
      }
    });

    return () => {
      isMounted = false;
      listener.remove();
    };
  }, []);

  return displayInfo;
};

使用示例:

javascript 复制代码
// DynamicLayout.js
import React from 'react';
import { View, Text, StyleSheet, Platform } from 'react-native';
import { useDisplayInfo } from './displayInfo';

const DynamicLayout = () => {
  const displayInfo = useDisplayInfo();
  
  if (!displayInfo) {
    return <Text>Loading...</Text>;
  }

  const isLandscape = displayInfo.orientation === 'landscape';
  const containerStyle = {
    flexDirection: isLandscape ? 'row' : 'column',
    padding: 20,
    backgroundColor: isLandscape ? '#E3F2FD' : '#FBE9E7'
  };

  return (
    <View style={[styles.container, containerStyle]}>
      <View style={[styles.box, styles.primaryBox]}>
        <Text style={styles.boxText}>Main Content</Text>
        <Text style={styles.detail}>Width: {displayInfo.width}px</Text>
        <Text style={styles.detail}>Height: {displayInfo.height}px</Text>
        <Text style={styles.detail}>Orientation: {displayInfo.orientation}</Text>
      </View>
      
      {isLandscape && (
        <View style={[styles.box, styles.secondaryBox]}>
          <Text style={styles.boxText}>Sidebar</Text>
          <Text style={styles.detail}>Safe Area Top: {displayInfo.safeArea.top}px</Text>
          <Text style={styles.detail}>Safe Area Bottom: {displayInfo.safeArea.bottom}px</Text>
        </View>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  box: {
    flex: 1,
    margin: 10,
    padding: 20,
    borderRadius: 10,
    alignItems: 'center',
    justifyContent: 'center',
  },
  primaryBox: {
    backgroundColor: '#BBDEFB',
  },
  secondaryBox: {
    backgroundColor: '#FFECB3',
  },
  boxText: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  detail: {
    fontSize: 16,
    color: '#555',
  },
});

export default DynamicLayout;

代码解释:

  1. 我们扩展了 displayInfo.js,添加了 addDisplayInfoListeneruseDisplayInfo Hook。

  2. useDisplayInfo Hook 封装了获取和监听显示信息的逻辑,自动处理组件挂载和卸载时的资源清理。

  3. DynamicLayout 组件中,我们使用这个 Hook 来实现动态布局,当屏幕方向变化时,布局会自动从纵向切换到横向,添加侧边栏。

  4. 我们还根据方向变化调整了背景颜色,提供视觉反馈,帮助开发者理解布局变化。

OpenHarmony平台适配要点:

  • OpenHarmony 的 DisplayInfo 事件监听更加精确,能够捕获到折叠状态变化等特殊事件。例如,当折叠屏设备从折叠状态变为展开状态时,会触发显示信息变化事件。

  • 在多窗口模式下,窗口大小变化会触发显示信息变化事件。例如,当用户调整分屏比例时,应用窗口尺寸会变化。

  • 折叠屏设备上,展开/折叠操作会触发显示信息变化。需要注意的是,这些事件可能非常频繁,特别是在用户缓慢展开设备时。

  • 需要处理快速连续变化的情况,避免频繁重渲染。可以使用节流(throttle)或防抖(debounce)技术来优化性能。

处理多窗口模式

OpenHarmony 支持多窗口模式,应用可能在部分屏幕区域运行。我们需要能够检测并适应这种场景。

javascript 复制代码
// multiWindow.js
import { NativeModules, Platform } from 'react-native';

const { DisplayInfoModule } = NativeModules;

/**
 * 多窗口模式信息
 * @typedef {Object} MultiWindowInfo
 * @property {boolean} isInMultiWindow - 是否在多窗口模式
 * @property {number} windowWidth - 当前窗口宽度
 * @property {number} windowHeight - 当前窗口高度
 * @property {number} screenWidth - 整个屏幕宽度
 * @property {number} screenHeight - 整个屏幕高度
 */

/**
 * 获取多窗口模式信息
 * @returns {Promise<MultiWindowInfo>}
 */
export const getMultiWindowInfo = () => {
  if (Platform.OS === 'openharmony') {
    return DisplayInfoModule.getMultiWindowInfo();
  }
  
  // 其他平台的 fallback(通常不支持多窗口)
  return Promise.resolve({
    isInMultiWindow: false,
    windowWidth: Dimensions.get('window').width,
    windowHeight: Dimensions.get('window').height,
    screenWidth: Dimensions.get('screen').width,
    screenHeight: Dimensions.get('screen').height
  });
};

/**
 * 监听多窗口模式变化
 * @param {(info: MultiWindowInfo) => void} callback
 * @returns {Object} 取消监听的方法
 */
export const addMultiWindowListener = (callback) => {
  if (Platform.OS === 'openharmony') {
    const subscription = DisplayInfoModule.addMultiWindowListener(callback);
    return {
      remove: () => DisplayInfoModule.removeMultiWindowListener(subscription)
    };
  }
  
  // 其他平台通常不支持多窗口变化监听
  return { remove: () => {} };
};

使用示例:

javascript 复制代码
// MultiWindowExample.js
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, Platform } from 'react-native';
import { getMultiWindowInfo, addMultiWindowListener } from './multiWindow';

const MultiWindowExample = () => {
  const [multiWindowInfo, setMultiWindowInfo] = useState(null);
  const [isCompact, setIsCompact] = useState(false);

  useEffect(() => {
    let isMounted = true;
    
    const loadMultiWindowInfo = async () => {
      try {
        const info = await getMultiWindowInfo();
        if (isMounted) {
          setMultiWindowInfo(info);
          setIsCompact(info.windowWidth < 600);
        }
      } catch (error) {
        console.error('Failed to get multi-window info:', error);
      }
    };

    loadMultiWindowInfo();
    
    const listener = addMultiWindowListener((info) => {
      if (isMounted) {
        setMultiWindowInfo(info);
        setIsCompact(info.windowWidth < 600);
      }
    });

    return () => {
      isMounted = false;
      listener.remove();
    };
  }, []);

  if (!multiWindowInfo) {
    return <Text>Loading multi-window info...</Text>;
  }

  return (
    <View style={styles.container}>
      <View style={[styles.header, 
        multiWindowInfo.isInMultiWindow ? styles.multiWindowHeader : styles.fullScreenHeader]}>
        <Text style={styles.headerText}>
          {multiWindowInfo.isInMultiWindow ? 'Multi-Window Mode' : 'Full Screen Mode'}
        </Text>
      </View>
      
      <View style={styles.content}>
        {isCompact ? (
          <View style={styles.compactLayout}>
            <Text style={styles.compactText}>
              Compact layout for small window size ({multiWindowInfo.windowWidth}px)
            </Text>
            <View style={styles.compactItem}>
              <Text>Main Content</Text>
            </View>
          </View>
        ) : (
          <View style={styles.expandedLayout}>
            <Text style={styles.expandedText}>
              Expanded layout for larger window size ({multiWindowInfo.windowWidth}px)
            </Text>
            <View style={styles.grid}>
              <View style={[styles.gridItem, styles.primaryItem]}>
                <Text>Main Content</Text>
              </View>
              <View style={[styles.gridItem, styles.secondaryItem]}>
                <Text>Sidebar</Text>
              </View>
            </View>
          </View>
        )}
      </View>
      
      <View style={styles.footer}>
        <Text>Window: {multiWindowInfo.windowWidth}x{multiWindowInfo.windowHeight}</Text>
        <Text>Screen: {multiWindowInfo.screenWidth}x{multiWindowInfo.screenHeight}</Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  header: {
    padding: 15,
    alignItems: 'center',
  },
  multiWindowHeader: {
    backgroundColor: '#FFECB3',
  },
  fullScreenHeader: {
    backgroundColor: '#E8F5E9',
  },
  headerText: {
    fontSize: 20,
    fontWeight: 'bold',
  },
  content: {
    flex: 1,
    padding: 15,
  },
  compactLayout: {
    flex: 1,
  },
  compactText: {
    marginBottom: 10,
    textAlign: 'center',
  },
  compactItem: {
    flex: 1,
    backgroundColor: '#BBDEFB',
    justifyContent: 'center',
    alignItems: 'center',
    borderRadius: 8,
  },
  expandedLayout: {
    flex: 1,
  },
  expandedText: {
    marginBottom: 10,
    textAlign: 'center',
  },
  grid: {
    flex: 1,
    flexDirection: 'row',
  },
  gridItem: {
    flex: 1,
    margin: 5,
    borderRadius: 8,
    justifyContent: 'center',
    alignItems: 'center',
  },
  primaryItem: {
    backgroundColor: '#BBDEFB',
  },
  secondaryItem: {
    backgroundColor: '#FFCCBC',
  },
  footer: {
    padding: 10,
    borderTopWidth: 1,
    borderTopColor: '#ddd',
    alignItems: 'center',
  },
});

export default MultiWindowExample;

代码解释:

  1. getMultiWindowInfo 获取多窗口模式相关信息,包括是否在多窗口模式、当前窗口尺寸和整个屏幕尺寸。

  2. addMultiWindowListener 监听多窗口模式变化,当窗口尺寸变化时触发回调。

  3. 在组件中,我们根据窗口宽度判断是否使用紧凑布局(windowWidth < 600px)。

  4. 当窗口大小变化时,自动切换布局模式,提供最佳的用户体验。

OpenHarmony平台适配要点:

  • OpenHarmony 的多窗口实现与 Android 有差异,需要使用 DisplayInfo 获取准确的窗口尺寸。例如,在 OpenHarmony 上,窗口尺寸可能不是整数,而是带有小数的值。

  • 在多窗口模式下,应用应该能够优雅降级,提供适合小窗口的 UI。例如,当窗口宽度小于 600px 时,应隐藏侧边栏,使用单列布局。

  • 窗口大小可能频繁变化,需要优化布局计算性能。可以使用 useMemo 或 useCallback 来避免不必要的重新渲染。

  • 需要处理窗口从多窗口模式进入全屏模式的过渡,提供平滑的用户体验。可以使用 Animated API 实现过渡效果。

适配折叠屏设备

折叠屏设备是 OpenHarmony 重点支持的设备类型,DisplayInfo 提供了检测折叠状态的能力。

javascript 复制代码
// foldable.js
import { NativeModules, Platform } from 'react-native';

const { DisplayInfoModule } = NativeModules;

/**
 * 折叠屏状态
 * @typedef {'unfolded' | 'half_folded' | 'folded' | 'unknown'} FoldState
 */

/**
 * 折叠屏信息
 * @typedef {Object} FoldableInfo
 * @property {FoldState} foldState - 折叠状态
 * @property {boolean} isTabletopMode - 是否桌面模式
 * @property {number} hingeAngle - 铰链角度(0-180度)
 * @property {DisplayInfo} displayInfo - 当前显示信息
 */

/**
 * 获取折叠屏信息
 * @returns {Promise<FoldableInfo>}
 */
export const getFoldableInfo = () => {
  if (Platform.OS === 'openharmony') {
    return DisplayInfoModule.getFoldableInfo();
  }
  
  // 非折叠屏设备的 fallback
  return Promise.resolve({
    foldState: 'unknown',
    isTabletopMode: false,
    hingeAngle: 0,
    displayInfo: {
      width: Dimensions.get('window').width,
      height: Dimensions.get('window').height,
      density: 1,
      orientation: Dimensions.get('window').width > Dimensions.get('window').height ? 'landscape' : 'portrait',
      safeArea: { top: 0, right: 0, bottom: 0, left: 0 }
    }
  });
};

/**
 * 监听折叠状态变化
 * @param {(info: FoldableInfo) => void} callback
 * @returns {Object} 取消监听的方法
 */
export const addFoldableListener = (callback) => {
  if (Platform.OS === 'openharmony') {
    const subscription = DisplayInfoModule.addFoldableListener(callback);
    return {
      remove: () => DisplayInfoModule.removeFoldableListener(subscription)
    };
  }
  
  // 非折叠屏设备不支持监听
  return { remove: () => {} };
};

使用示例:

javascript 复制代码
// FoldableExample.js
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, Platform, Animated } from 'react-native';
import { getFoldableInfo, addFoldableListener } from './foldable';

const FoldableExample = () => {
  const [foldableInfo, setFoldableInfo] = useState(null);
  const [hingeAngleAnim] = useState(new Animated.Value(0));
  const [isUnfolded, setIsUnfolded] = useState(false);

  useEffect(() => {
    let isMounted = true;
    
    const loadFoldableInfo = async () => {
      try {
        const info = await getFoldableInfo();
        if (isMounted) {
          setFoldableInfo(info);
          setIsUnfolded(info.foldState === 'unfolded');
          Animated.timing(hingeAngleAnim, {
            toValue: info.hingeAngle,
            duration: 300,
            useNativeDriver: false
          }).start();
        }
      } catch (error) {
        console.error('Failed to get foldable info:', error);
      }
    };

    loadFoldableInfo();
    
    const listener = addFoldableListener((info) => {
      if (isMounted) {
        setFoldableInfo(info);
        setIsUnfolded(info.foldState === 'unfolded');
        Animated.timing(hingeAngleAnim, {
          toValue: info.hingeAngle,
          duration: 300,
          useNativeDriver: false
        }).start();
      }
    });

    return () => {
      isMounted = false;
      listener.remove();
    };
  }, []);

  if (!foldableInfo) {
    return <Text>Loading foldable info...</Text>;
  }

  const hingeAngle = hingeAngleAnim.interpolate({
    inputRange: [0, 180],
    outputRange: ['0deg', '180deg']
  });

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>Foldable Device Demo</Text>
        <Text style={styles.status}>
          Fold State: {foldableInfo.foldState.toUpperCase()}
        </Text>
        {foldableInfo.isTabletopMode && (
          <Text style={styles.tabletop}>Tabletop Mode Active</Text>
        )}
      </View>
      
      <View style={styles.deviceContainer}>
        <View style={styles.deviceFrame}>
          <View style={[styles.screen, isUnfolded ? styles.unfoldedScreen : styles.foldedScreen]}>
            <Text style={styles.screenText}>
              {isUnfolded ? 'Unfolded Mode' : 'Folded Mode'}
            </Text>
            <Text style={styles.screenDetail}>
              Size: {foldableInfo.displayInfo.width}x{foldableInfo.displayInfo.height}
            </Text>
          </View>
          
          {!isUnfolded && (
            <Animated.View style={[styles.hinge, { transform: [{ rotate: hingeAngle }] }]} />
          )}
        </View>
      </View>
      
      <View style={styles.content}>
        {isUnfolded ? (
          <View style={styles.unfoldedContent}>
            <Text style={styles.contentTitle}>Expanded View</Text>
            <Text>This layout is optimized for unfolded state with larger screen area.</Text>
            <View style={styles.grid}>
              <View style={[styles.card, styles.card1]}><Text>Content 1</Text></View>
              <View style={[styles.card, styles.card2]}><Text>Content 2</Text></View>
              <View style={[styles.card, styles.card3]}><Text>Content 3</Text></View>
              <View style={[styles.card, styles.card4]}><Text>Content 4</Text></View>
            </View>
          </View>
        ) : (
          <View style={styles.foldedContent}>
            <Text style={styles.contentTitle}>Compact View</Text>
            <Text>This layout is optimized for folded state with smaller screen.</Text>
            <View style={styles.list}>
              <View style={styles.listItem}><Text>List Item 1</Text></View>
              <View style={styles.listItem}><Text>List Item 2</Text></View>
              <View style={styles.listItem}><Text>List Item 3</Text></View>
            </View>
          </View>
        )}
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  header: {
    marginBottom: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 5,
  },
  status: {
    fontSize: 18,
    color: '#555',
  },
  tabletop: {
    fontSize: 16,
    color: '#D32F2F',
    fontWeight: 'bold',
    marginTop: 5,
  },
  deviceContainer: {
    alignItems: 'center',
    marginVertical: 20,
  },
  deviceFrame: {
    width: 300,
    height: 300,
    backgroundColor: '#616161',
    borderRadius: 20,
    padding: 10,
    justifyContent: 'center',
    alignItems: 'center',
  },
  screen: {
    flex: 1,
    width: '100%',
    backgroundColor: '#E0E0E0',
    borderRadius: 10,
    padding: 15,
    justifyContent: 'center',
    alignItems: 'center',
  },
  unfoldedScreen: {
    flexDirection: 'row',
  },
  foldedScreen: {
    // 单屏显示
  },
  screenText: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 5,
  },
  screenDetail: {
    fontSize: 14,
    color: '#666',
  },
  hinge: {
    position: 'absolute',
    width: 4,
    height: '100%',
    backgroundColor: '#424242',
  },
  content: {
    flex: 1,
  },
  unfoldedContent: {
    flex: 1,
  },
  foldedContent: {
    flex: 1,
  },
  contentTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  grid: {
    flexDirection: 'row',
    flexWrap: 'wrap',
  },
  card: {
    width: '50%',
    padding: 10,
  },
  card1: { backgroundColor: '#BBDEFB' },
  card2: { backgroundColor: '#FFECB3' }
  card3: { backgroundColor: '#C8E6C9' }
  card4: { backgroundColor: '#F8BBD0' }
  list: {
    flex: 1,
  }
  listItem: {
    padding: 15,
    backgroundColor: '#E0E0E0',
    marginBottom: 10,
    borderRadius: 5,
  }
});

export default FoldableExample;

代码解释:

  1. getFoldableInfo 获取折叠屏状态和相关信息,包括折叠状态、铰链角度和桌面模式。

  2. addFoldableListener 监听折叠状态变化,当折叠状态改变时触发回调。

  3. 在组件中,我们使用 Animated API 平滑过渡铰链角度变化,提供更好的视觉体验。

  4. 根据折叠状态,展示不同的 UI 布局:展开状态下显示网格布局,折叠状态下显示列表布局。

OpenHarmony平台适配要点:

  • OpenHarmony 的 DisplayInfo 提供了详细的折叠状态信息,包括铰链角度(0-180度)。这可以用于创建更自然的过渡效果。

  • 在展开状态下,可以利用更大的屏幕空间提供更丰富的 UI,例如网格布局、分栏显示等。

  • 在折叠状态下,应该提供适合小屏幕的紧凑布局,例如单列列表、简化导航等。

  • 桌面模式(半折叠)需要特殊处理,可能需要固定应用在屏幕的一部分。例如,当设备处于桌面模式时,应用可能只占用下半部分屏幕。

  • 折叠状态变化可能非常频繁,特别是在用户缓慢展开设备时,需要优化性能,避免卡顿。

实现响应式布局系统

基于 DisplayInfo,我们可以构建一个更强大的响应式布局系统,自动适应不同设备和场景。

javascript 复制代码
// responsive.js
import { useState, useEffect } from 'react';
import { Dimensions, Platform } from 'react-native';
import { getDisplayInfo, addDisplayInfoListener } from './displayInfo';

/**
 * 响应式断点配置
 * @typedef {Object} Breakpoints
 * @property {number} xs - 超小屏幕 (<576px)
 * @property {number} sm - 小屏幕 (≥576px)
 * @property {number} md - 中等屏幕 (≥768px)
 * @property {number} lg - 大屏幕 (≥992px)
 * @property {number} xl - 超大屏幕 (≥1200px)
 * @property {number} xxl - 特大屏幕 (≥1400px)
 */

/**
 * 默认断点配置
 * @type {Breakpoints}
 */
const DEFAULT_BREAKPOINTS = {
  xs: 0,
  sm: 576,
  md: 768,
  lg: 992,
  xl: 1200,
  xxl: 1400
};

/**
 * 响应式信息
 * @typedef {Object} ResponsiveInfo
 * @property {string} breakpoint - 当前断点 (xs, sm, md, lg, xl, xxl)
 * @property {boolean} isMobile - 是否移动设备
 * @property {boolean} isTablet - 是否平板设备
 * @property {boolean} isDesktop - 是否桌面设备
 * @property {boolean} isLandscape - 是否横屏
 * @property {boolean} isPortrait - 是否竖屏
 * @property {number} width - 屏幕宽度
 * @property {number} height - 屏幕高度
 * @property {Object} safeArea - 安全区域
 */

/**
 * 获取响应式信息
 * @param {Breakpoints} [breakpoints=DEFAULT_BREAKPOINTS] - 自定义断点
 * @returns {ResponsiveInfo}
 */
export const getResponsiveInfo = (breakpoints = DEFAULT_BREAKPOINTS) => {
  const { width, height, orientation, safeArea } = Dimensions.get('window');
  
  // 确定断点
  let breakpoint = 'xs';
  if (width >= breakpoints.xxl) breakpoint = 'xxl';
  else if (width >= breakpoints.xl) breakpoint = 'xl';
  else if (width >= breakpoints.lg) breakpoint = 'lg';
  else if (width >= breakpoints.md) breakpoint = 'md';
  else if (width >= breakpoints.sm) breakpoint = 'sm';
  
  // 确定设备类型
  const isMobile = width < breakpoints.md;
  const isTablet = width >= breakpoints.md && width < breakpoints.lg;
  const isDesktop = width >= breakpoints.lg;
  
  return {
    breakpoint,
    isMobile,
    isTablet,
    isDesktop,
    isLandscape: orientation === 'landscape',
    isPortrait: orientation === 'portrait',
    width,
    height,
    safeArea
  };
};

/**
 * 响应式 Hook
 * @param {Breakpoints} [breakpoints=DEFAULT_BREAKPOINTS] - 自定义断点
 * @returns {ResponsiveInfo}
 */
export const useResponsive = (breakpoints = DEFAULT_BREAKPOINTS) => {
  const [responsiveInfo, setResponsiveInfo] = useState(() => 
    getResponsiveInfo(breakpoints)
  );

  useEffect(() => {
    let isMounted = true;
    
    const updateResponsiveInfo = () => {
      if (isMounted) {
        setResponsiveInfo(getResponsiveInfo(breakpoints));
      }
    };
    
    // OpenHarmony 专用监听
    if (Platform.OS === 'openharmony') {
      const listener = addDisplayInfoListener(() => {
        updateResponsiveInfo();
      });
      
      return () => {
        isMounted = false;
        listener.remove();
      };
    }
    
    // 其他平台使用 Dimensions 监听
    const dimensionsListener = Dimensions.addEventListener('change', updateResponsiveInfo);
    
    return () => {
      isMounted = false;
      dimensionsListener?.remove?.();
    };
  }, [breakpoints]);

  return responsiveInfo;
};

/**
 * 响应式样式助手
 */
export const responsiveStyle = (breakpoints = DEFAULT_BREAKPOINTS) => ({
  /**
   * 根据断点应用样式
   * @param {Object} styles - 断点样式映射
   * @returns {Object} 合并后的样式
   */
  breakpoint: (styles) => {
    const { breakpoint } = getResponsiveInfo(breakpoints);
    return styles[breakpoint] || {};
  },
  
  /**
   * 根据设备类型应用样式
   * @param {Object} styles - 设备类型样式映射
   * @returns {Object} 合并后的样式
   */
  device: (styles) => {
    const { isMobile, isTablet, isDesktop } = getResponsiveInfo(breakpoints);
    if (isDesktop && styles.desktop) return styles.desktop;
    if (isTablet && styles.tablet) return styles.tablet;
    if (isMobile && styles.mobile) return styles.mobile;
    return {};
  },
  
  /**
   * 根据屏幕方向应用样式
   * @param {Object} styles - 方向样式映射
   * @returns {Object} 合并后的样式
   */
  orientation: (styles) => {
    const { isLandscape, isPortrait } = getResponsiveInfo(breakpoints);
    if (isLandscape && styles.landscape) return styles.landscape;
    if (isPortrait && styles.portrait) return styles.portrait;
    return {};
  }
});

使用示例:

javascript 复制代码
// ResponsiveExample.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useResponsive, responsiveStyle } from './responsive';

const ResponsiveExample = () => {
  const responsive = useResponsive();
  const rs = responsiveStyle();
  
  const containerStyle = [
    styles.container,
    rs.breakpoint({
      xs: styles.containerXs,
      sm: styles.containerSm,
      md: styles.containerMd,
      lg: styles.containerLg
    }),
    rs.orientation({
      landscape: styles.landscape,
      portrait: styles.portrait
    })
  ];
  
  const contentStyle = rs.device({
    mobile: styles.contentMobile,
    tablet: styles.contentTablet,
    desktop: styles.contentDesktop
  });

  return (
    <View style={containerStyle}>
      <View style={styles.header}>
        <Text style={styles.title}>Responsive Layout</Text>
        <Text style={styles.subtitle}>
          Breakpoint: {responsive.breakpoint} | 
          Device: {responsive.isMobile ? 'Mobile' : responsive.isTablet ? 'Tablet' : 'Desktop'} |
          Orientation: {responsive.isLandscape ? 'Landscape' : 'Portrait'}
        </Text>
      </View>
      
      <View style={[styles.content, contentStyle]}>
        <Text style={styles.contentText}>
          This layout adapts to different screen sizes and orientations.
        </Text>
        
        <View style={styles.grid}>
          <View style={[styles.card, styles.card1]}>
            <Text>XS/SM Layout</Text>
            <Text>Width: {responsive.width}px</Text>
          </View>
          {responsive.width >= 768 && (
            <View style={[styles.card, styles.card2]}>
              <Text>MD+ Layout</Text>
              <Text>Only shown on medium+ screens</Text>
            </View>
          )}
          {responsive.width >= 992 && (
            <View style={[styles.card, styles.card3]}>
              <Text>LG+ Layout</Text>
              <Text>Only shown on large+ screens</Text>
            </View>
          )}
        </View>
      </View>
      
      <View style={styles.footer}>
        <Text>Safe Area: Top {responsive.safeArea.top}, Bottom {responsive.safeArea.bottom}</Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  containerXs: {
    backgroundColor: '#FFEBEE',
  },
  containerSm: {
    backgroundColor: '#FBE9E7',
  },
  containerMd: {
    backgroundColor: '#E8F5E9',
  },
  containerLg: {
    backgroundColor: '#E3F2FD',
  },
  landscape: {
    backgroundColor: '#E0F7FA',
  },
  portrait: {
    backgroundColor: '#F3E5F5',
  },
  header: {
    marginBottom: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
  },
  subtitle: {
    fontSize: 16,
    color: '#555',
    marginTop: 5,
  },
  content: {
    flex: 1,
  },
  contentMobile: {
    backgroundColor: '#FFCCBC',
  },
  contentTablet: {
    backgroundColor: '#C5E1A5',
  },
  contentDesktop: {
    backgroundColor: '#80DEEA',
  },
  contentText: {
    fontSize: 18,
    marginBottom: 20,
    textAlign: 'center',
  },
  grid: {
    flexDirection: 'row',
    flexWrap: 'wrap',
  },
  card: {
    flex: 1,
    minWidth: 150,
    padding: 15,
    margin: 5,
    borderRadius: 8,
  },
  card1: { backgroundColor: '#BBDEFB' },
  card2: { backgroundColor: '#FFECB3' },
  card3: { backgroundColor: '#C8E6C9' },
  footer: {
    marginTop: 20,
    padding: 10,
    borderTopWidth: 1,
    borderTopColor: '#ddd',
  },
});

export default ResponsiveExample;

代码解释:

  1. 我们创建了一个响应式系统,基于 DisplayInfo 和 Dimensions API,提供断点、设备类型和屏幕方向的检测。

  2. useResponsive Hook 提供了响应式信息,自动处理显示信息变化。

  3. responsiveStyle 提供了样式助手,可以根据断点、设备类型和方向应用不同样式。

  4. 在组件中,我们根据响应式信息动态调整 UI,实现真正的响应式布局。

OpenHarmony平台适配要点:

  • OpenHarmony 的 DisplayInfo 提供了更精确的显示信息,使响应式系统更加准确。例如,可以检测到折叠屏设备的展开状态,并将其视为"桌面"设备。

  • 在折叠屏设备上,断点判断应该考虑当前展开的屏幕区域。例如,当设备展开时,可能从"平板"断点变为"桌面"断点。

  • 多窗口模式下,响应式系统应该基于窗口尺寸而非整个屏幕尺寸。例如,当应用在大屏幕上以小窗口运行时,应该使用移动设备的布局。

  • 桌面模式(半折叠)可能需要特殊的断点处理。例如,可以定义一个"half-folded"断点,提供适合该状态的 UI。

  • 响应式系统应该考虑性能,避免频繁重新计算和渲染。可以使用 useMemo 和 useCallback 来优化性能。

OpenHarmony平台特定注意事项

OpenHarmony 与 Android 的显示系统差异

OpenHarmony 的显示系统与 Android 有显著差异,这影响了 DisplayInfo 的使用方式:

  1. 显示服务架构不同

    • Android 使用 WindowManagerService 管理显示
    • OpenHarmony 使用自己的 DisplayManager 服务
    • 这导致 DisplayInfo 的获取方式和数据结构有所不同
  2. 折叠屏支持

    • OpenHarmony 对折叠屏有原生支持,提供详细的折叠状态信息
    • Android 需要通过额外的 API(如 Samsung 的 Folding API)获取类似信息
  3. 多窗口模式

    • OpenHarmony 的多窗口实现与 Android 有差异
    • 在 OpenHarmony 中,应用可能在部分物理屏幕区域运行
  4. 安全区域计算

    • OpenHarmony 的安全区域计算考虑了更多设备特性
    • 不同品牌设备的安全区域可能有显著差异

下面是 OpenHarmony 与 Android 显示系统的主要差异对比表:

特性 OpenHarmony Android 注意事项
显示服务 DisplayManager WindowManagerService OpenHarmony 的 API 更简洁,但功能略有不同
折叠屏支持 原生支持,提供铰链角度等详细信息 需要厂商特定 API(如 Samsung) OpenHarmony 提供统一的折叠状态 API
多窗口模式 窗口尺寸可能小于物理屏幕 通常全屏或分屏 OpenHarmony 需要特别处理窗口尺寸
安全区域 包含更详细的设备特定信息 标准安全区域 OpenHarmony 的安全区域可能更复杂
DPI 计算 基于物理像素的精确计算 逻辑像素转换 OpenHarmony 的 DPI 信息更准确
屏幕方向 提供更精细的旋转角度 基本方向(横/竖) OpenHarmony 支持更多中间状态
桌面模式 原生支持 无直接支持 OpenHarmony 可检测半折叠状态

OpenHarmony 显示信息获取的最佳实践

在 React Native for OpenHarmony 应用中,获取显示信息的最佳实践包括:

1. 使用统一的 API 层

创建一个统一的 API 层来处理不同平台的差异:

javascript 复制代码
// display.js
import { Platform } from 'react-native';
import { getDisplayInfo as getOpenHarmonyDisplayInfo } from './openharmony/displayInfo';
import { getDisplayInfo as getAndroidDisplayInfo } from './android/displayInfo';
import { getDisplayInfo as getIosDisplayInfo } from './ios/displayInfo';

/**
 * 统一的显示信息获取函数
 * @returns {Promise<DisplayInfo>}
 */
export const getDisplayInfo = () => {
  switch (Platform.OS) {
    case 'openharmony':
      return getOpenHarmonyDisplayInfo();
    case 'android':
      return getAndroidDisplayInfo();
    case 'ios':
      return getIosDisplayInfo();
    default:
      throw new Error(`Unsupported platform: ${Platform.OS}`);
  }
};

/**
 * 统一的显示信息监听
 * @param {(displayInfo: DisplayInfo) => void} callback
 * @returns {Object} 取消监听的方法
 */
export const addDisplayInfoListener = (callback) => {
  switch (Platform.OS) {
    case 'openharmony':
      return addOpenHarmonyDisplayInfoListener(callback);
    case 'android':
      return addAndroidDisplayInfoListener(callback);
    case 'ios':
      return addIosDisplayInfoListener(callback);
    default:
      throw new Error(`Unsupported platform: ${Platform.OS}`);
  }
};
2. 处理异步初始化

由于 DisplayInfo 可能需要异步获取,在应用启动时应该妥善处理:

javascript 复制代码
// App.js
import React, { useState, useEffect } from 'react';
import { View, Text, ActivityIndicator } from 'react-native';
import { getDisplayInfo } from './display';

const App = () => {
  const [displayInfo, setDisplayInfo] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;
    
    const initDisplayInfo = async () => {
      try {
        const info = await getDisplayInfo();
        if (isMounted) {
          setDisplayInfo(info);
        }
      } catch (err) {
        if (isMounted) {
          setError(err);
        }
      }
    };

    initDisplayInfo();
    
    return () => {
      isMounted = false;
    };
  }, []);

  if (error) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <Text style={{ color: 'red' }}>Error: {error.message}</Text>
      </View>
    );
  }

  if (!displayInfo) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" />
        <Text>Loading display information...</Text>
      </View>
    );
  }

  return (
    // 应用主内容
    <MainApp displayInfo={displayInfo} />
  );
};
3. 优雅降级与回退机制

为确保应用在不支持的平台上也能正常运行,应该实现优雅降级:

javascript 复制代码
// display.js (简化版)
import { Platform, Dimensions, PixelRatio } from 'react-native';

/**
 * 获取显示信息(带优雅降级)
 * @returns {Promise<DisplayInfo>}
 */
export const getDisplayInfo = async () => {
  try {
    if (Platform.OS === 'openharmony') {
      // 尝试使用 OpenHarmony 原生 API
      return await NativeModules.DisplayInfoModule.getDisplayInfo();
    }
    
    // 其他平台使用 Dimensions 和 PixelRatio
    const { width, height } = Dimensions.get('window');
    return {
      width,
      height,
      density: PixelRatio.get(),
      orientation: width > height ? 'landscape' : 'portrait',
      safeArea: {
        top: 0,
        right: 0,
        bottom: 0,
        left: 0
      }
    };
  } catch (error) {
    console.warn('Fallback to basic display info:', error);
    // 最终回退到基础信息
    const { width, height } = Dimensions.get('window');
    return {
      width,
      height,
      density: 1,
      orientation: width > height ? 'landscape' : 'portrait',
      safeArea: {
        top: 0,
        right: 0,
        bottom: 0,
        left: 0
      }
    };
  }
};
4. 性能优化

频繁获取显示信息可能影响性能,需要注意:

  1. 避免频繁调用:在渲染循环中避免直接调用获取显示信息的 API
  2. 使用 Memoization:对计算结果进行缓存
  3. 节流处理:对频繁触发的事件进行节流
javascript 复制代码
// performance.js
import { useMemo, useState, useEffect } from 'react';
import _ from 'lodash';
import { getDisplayInfo } from './display';

/**
 * 获取优化后的显示信息
 * @returns {DisplayInfo}
 */
export const useOptimizedDisplayInfo = () => {
  const [displayInfo, setDisplayInfo] = useState(null);
  
  useEffect(() => {
    let isMounted = true;
    
    const loadDisplayInfo = async () => {
      try {
        const info = await getDisplayInfo();
        if (isMounted) {
          setDisplayInfo(info);
        }
      } catch (error) {
        console.error('Failed to get display info:', error);
      }
    };

    // 初始加载
    loadDisplayInfo();
    
    // 节流处理显示信息变化
    const throttledUpdate = _.throttle(async () => {
      const info = await getDisplayInfo();
      if (isMounted) {
        setDisplayInfo(info);
      }
    }, 100); // 100ms 节流
    
    const listener = addDisplayInfoListener(throttledUpdate);
    
    return () => {
      isMounted = false;
      listener.remove();
      throttledUpdate.cancel();
    };
  }, []);

  // 使用 useMemo 避免不必要的重新计算
  const optimizedInfo = useMemo(() => {
    if (!displayInfo) return null;
    
    return {
      ...displayInfo,
      // 添加一些预计算的值
      isLargeScreen: displayInfo.width >= 1024,
      isTablet: displayInfo.width >= 768 && displayInfo.width < 1024,
      isPhone: displayInfo.width < 768,
      aspectRatio: displayInfo.width / displayInfo.height
    };
  }, [displayInfo]);

  return optimizedInfo;
};

OpenHarmony 显示适配的常见陷阱

在使用 DisplayInfo 时,开发者常遇到以下问题:

1. 混淆物理像素和逻辑像素

OpenHarmony 的 DisplayInfo 返回的是物理像素值,而 React Native 通常使用逻辑像素。这可能导致尺寸计算错误。

解决方案:创建一个转换函数:

javascript 复制代码
/**
 * 将物理像素转换为逻辑像素
 * @param {number} physicalPixels - 物理像素值
 * @param {number} density - 像素密度
 * @returns {number} 逻辑像素值
 */
export const toLogicalPixels = (physicalPixels, density) => {
  // OpenHarmony 的基准密度可能与 Android 不同
  const baseDensity = Platform.OS === 'openharmony' ? 160 : 160;
  return physicalPixels / (density * (baseDensity / 160));
};

/**
 * 将逻辑像素转换为物理像素
 * @param {number} logicalPixels - 逻辑像素值
 * @param {number} density - 像素密度
 * @returns {number} 物理像素值
 */
export const toPhysicalPixels = (logicalPixels, density) => {
  const baseDensity = Platform.OS === 'openharmony' ? 160 : 160;
  return logicalPixels * (density * (baseDensity / 160));
};
2. 忽略安全区域

在刘海屏、打孔屏设备上,忽略安全区域会导致内容被系统 UI 遮挡。

解决方案:始终考虑安全区域:

javascript 复制代码
// safeArea.js
import { View, StyleSheet } from 'react-native';
import { getSafeArea } from './safeArea';

/**
 * 安全区域容器组件
 */
export const SafeAreaContainer = ({ children, style }) => {
  const [containerStyle, setContainerStyle] = useState({});
  
  useEffect(() => {
    const loadStyle = async () => {
      const safeArea = await getSafeArea();
      setContainerStyle({
        paddingTop: safeArea.top,
        paddingBottom: safeArea.bottom,
        paddingLeft: safeArea.left,
        paddingRight: safeArea.right
      });
    };
    
    loadStyle();
  }, []);
  
  return (
    <View style={[styles.container, containerStyle, style]}>
      {children}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1
  }
});
3. 折叠状态变化处理不当

在折叠屏设备上,折叠状态变化可能导致布局突变。

解决方案:平滑过渡和状态管理:

javascript 复制代码
// foldableTransition.js
import { useState, useEffect, useRef } from 'react';
import { Animated, Easing } from 'react-native';
import { getFoldableInfo, addFoldableListener } from './foldable';

/**
 * 折叠状态过渡 Hook
 */
export const useFoldableTransition = () => {
  const [foldState, setFoldState] = useState('unknown');
  const transitionAnim = useRef(new Animated.Value(0)).current;
  
  useEffect(() => {
    let isMounted = true;
    
    const loadFoldableInfo = async () => {
      try {
        const info = await getFoldableInfo();
        if (isMounted) {
          setFoldState(info.foldState);
          transitionAnim.setValue(info.foldState === 'unfolded' ? 1 : 0);
        }
      } catch (error) {
        console.error('Failed to get foldable info:', error);
      }
    };

    loadFoldableInfo();
    
    const listener = addFoldableListener((info) => {
      if (isMounted) {
        setFoldState(info.foldState);
        Animated.timing(transitionAnim, {
          toValue: info.foldState === 'unfolded' ? 1 : 0,
          duration: 300,
          easing: Easing.out(Easing.ease),
          useNativeDriver: false
        }).start();
      }
    });

    return () => {
      isMounted = false;
      listener.remove();
    };
  }, []);
  
  return {
    foldState,
    transitionAnim
  };
};
4. 多窗口模式下的布局问题

在多窗口模式下,应用可能只占用部分屏幕空间,但 Dimensions API 可能返回整个屏幕的尺寸。

解决方案:使用 DisplayInfo 获取窗口尺寸:

javascript 复制代码
// multiWindowLayout.js
import { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { getMultiWindowInfo, addMultiWindowListener } from './multiWindow';

/**
 * 多窗口自适应布局组件
 */
export const MultiWindowLayout = ({ children }) => {
  const [isCompact, setIsCompact] = useState(false);
  
  useEffect(() => {
    let isMounted = true;
    
    const loadMultiWindowInfo = async () => {
      try {
        const info = await getMultiWindowInfo();
        if (isMounted) {
          setIsCompact(info.windowWidth < 600);
        }
      } catch (error) {
        console.error('Failed to get multi-window info:', error);
      }
    };

    loadMultiWindowInfo();
    
    const listener = addMultiWindowListener((info) => {
      if (isMounted) {
        setIsCompact(info.windowWidth < 600);
      }
    });

    return () => {
      isMounted = false;
      listener.remove();
    };
  }, []);
  
  return (
    <View style={isCompact ? styles.compact : styles.expanded}>
      {children}
    </View>
  );
};

const styles = StyleSheet.create({
  compact: {
    // 紧凑布局样式
  },
  expanded: {
    // 扩展布局样式
  }
});

OpenHarmony 显示适配的性能考量

在 OpenHarmony 上使用 DisplayInfo 时,性能是一个重要考量因素:

1. 事件监听频率

频繁的显示信息变化事件可能导致性能问题。

优化策略

  • 使用节流(throttle)处理频繁事件
  • 只在需要时订阅事件
  • 避免在事件处理中进行复杂计算
javascript 复制代码
// throttle.js
import _ from 'lodash';

/**
 * 节流显示信息变化监听
 * @param {(displayInfo: DisplayInfo) => void} callback
 * @param {number} [wait=100] - 节流等待时间(毫秒)
 * @returns {Object} 取消监听的方法
 */
export const throttleDisplayInfoListener = (callback, wait = 100) => {
  const throttledCallback = _.throttle(callback, wait);
  const listener = addDisplayInfoListener(throttledCallback);
  
  return {
    remove: () => {
      listener.remove();
      throttledCallback.cancel();
    }
  };
};
2. 布局计算优化

复杂的响应式布局计算可能影响渲染性能。

优化策略

  • 预计算常用值
  • 使用 useMemo 优化计算
  • 避免在渲染函数中进行复杂计算
javascript 复制代码
// layout.js
import { useMemo } from 'react';
import { useDisplayInfo } from './displayInfo';

/**
 * 计算响应式布局参数
 */
export const useLayoutParams = () => {
  const displayInfo = useDisplayInfo();
  
  return useMemo(() => {
    if (!displayInfo) return null;
    
    const { width, height } = displayInfo;
    const isLargeScreen = width >= 1024;
    const isTablet = width >= 768 && width < 1024;
    const isPhone = width < 768;
    const aspectRatio = width / height;
    
    // 预计算常用布局值
    const gridColumns = isLargeScreen ? 4 : isTablet ? 3 : 2;
    const itemSize = isLargeScreen ? 200 : isTablet ? 150 : 100;
    
    return {
      isLargeScreen,
      isTablet,
      isPhone,
      aspectRatio,
      gridColumns,
      itemSize
    };
  }, [displayInfo]);
};
3. 图像资源管理

在不同 DPI 设备上,需要提供合适的图像资源。

优化策略

  • 使用矢量图形(SVG)
  • 按需加载不同分辨率的图像
  • 使用 React Native 的 Image 组件的 resizeMode 属性
javascript 复制代码
// image.js
import { Image, Dimensions } from 'react-native';
import { getDensityInfo } from './density';

/**
 * 响应式图像组件
 */
export const ResponsiveImage = ({ source, style, ...props }) => {
  const [displayInfo] = useState(() => Dimensions.get('window'));
  const [densityInfo, setDensityInfo] = useState(null);
  
  useEffect(() => {
    getDensityInfo().then(setDensityInfo);
  }, []);
  
  // 选择适当的图像资源
  const getSource = () => {
    if (!densityInfo || !source) return source;
    
    const { density } = densityInfo;
    const { width, height } = displayInfo;
    
    // 根据密度选择图像
    if (density >= 3 && source['@3x']) {
      return source['@3x'];
    } else if (density >= 2 && source['@2x']) {
      return source['@2x'];
    }
    
    return source;
  };
  
  return (
    <Image
      source={getSource()}
      style={[
        style,
        // 根据屏幕尺寸调整图像大小
        width && { width },
        height && { height }
      ]}
      {...props}
    />
  );
};

OpenHarmony 显示适配的调试技巧

调试 OpenHarmony 上的显示适配问题可能比较困难,以下是一些有用的技巧:

1. 显示信息可视化

创建一个调试组件,实时显示当前的显示信息:

javascript 复制代码
// DisplayDebug.js
import React from 'react';
import { View, Text, StyleSheet, Platform } from 'react-native';
import { useDisplayInfo } from './displayInfo';

const DisplayDebug = () => {
  const displayInfo = useDisplayInfo();
  
  if (!displayInfo) return null;
  
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Display Info</Text>
      <View style={styles.infoRow}>
        <Text style={styles.label}>Width:</Text>
        <Text style={styles.value}>{displayInfo.width}px</Text>
      </View>
      <View style={styles.infoRow}>
        <Text style={styles.label}>Height:</Text>
        <Text style={styles.value}>{displayInfo.height}px</Text>
      </View>
      <View style={styles.infoRow}>
        <Text style={styles.label}>Density:</Text>
        <Text style={styles.value}>{displayInfo.density}</Text>
      </View>
      <View style={styles.infoRow}>
        <Text style={styles.label}>Orientation:</Text>
        <Text style={styles.value}>{displayInfo.orientation}</Text>
      </View>
      <View style={styles.infoRow}>
        <Text style={styles.label}>Safe Area:</Text>
        <Text style={styles.value}>
          T:{displayInfo.safeArea.top} R:{displayInfo.safeArea.right} 
          B:{displayInfo.safeArea.bottom} L:{displayInfo.safeArea.left}
        </Text>
      </View>
      <Text style={styles.platform}>
        Platform: {Platform.OS} {Platform.Version}
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    position: 'absolute',
    top: 20,
    right: 20,
    backgroundColor: 'rgba(0,0,0,0.7)',
    padding: 10,
    borderRadius: 5,
    zIndex: 9999,
  },
  title: {
    color: 'white',
    fontWeight: 'bold',
    marginBottom: 5,
  },
  infoRow: {
    flexDirection: 'row',
  },
  label: {
    color: '#aaa',
    width: 90,
  },
  value: {
    color: 'white',
  },
  platform: {
    color: '#66F',
    fontSize: 12,
    marginTop: 5,
  },
});

export default DisplayDebug;
2. 模拟不同设备场景

在开发过程中,可能需要模拟不同的设备场景:

javascript 复制代码
// deviceSimulator.js
import { NativeModules } from 'react-native';

const { DisplayInfoModule } = NativeModules;

/**
 * 模拟设备场景(仅用于开发调试)
 * @param {Object} options
 * @param {number} [options.width] - 模拟宽度
 * @param {number} [options.height] - 模拟高度
 * @param {string} [options.orientation] - 模拟方向
 * @param {number} [options.density] - 模拟密度
 */
export const simulateDevice = (options) => {
  if (__DEV__ && Platform.OS === 'openharmony') {
    DisplayInfoModule.simulateDisplayInfo(options);
  }
};

/**
 * 重置设备模拟
 */
export const resetDeviceSimulation = () => {
  if (__DEV__ && Platform.OS === 'openharmony') {
    DisplayInfoModule.resetDisplayInfoSimulation();
  }
};

然后在应用中添加调试按钮:

javascript 复制代码
// DebugControls.js
import React from 'react';
import { View, Button, Platform } from 'react-native';
import { simulateDevice, resetDeviceSimulation } from './deviceSimulator';

const DebugControls = () => {
  if (!__DEV__ || Platform.OS !== 'openharmony') {
    return null;
  }

  return (
    <View style={{ padding: 10, backgroundColor: '#f0f0f0' }}>
      <View style={{ flexDirection: 'row', marginBottom: 5 }}>
        <Button title="Phone (Portrait)" onPress={() => 
          simulateDevice({ width: 360, height: 640, orientation: 'portrait' })
        } />
        <Button title="Phone (Landscape)" onPress={() => 
          simulateDevice({ width: 640, height: 360, orientation: 'landscape' })
        } />
      </View>
      <View style={{ flexDirection: 'row' }}>
        <Button title="Tablet (Portrait)" onPress={() => 
          simulateDevice({ width: 800, height: 1280, orientation: 'portrait' })
        } />
        <Button title="Tablet (Landscape)" onPress={() => 
          simulateDevice({ width: 1280, height: 800, orientation: 'landscape' })
        } />
      </View>
      <Button 
        title="Reset Simulation" 
        onPress={resetDeviceSimulation} 
        color="#D32F2F"
      />
    </View>
  );
};

export default DebugControls;
3. 日志记录

详细的日志记录有助于诊断显示适配问题:

javascript 复制代码
// logger.js
import { Platform } from 'react-native';
import { getDisplayInfo } from './displayInfo';

/**
 * 记录显示信息变化
 */
export const logDisplayInfoChanges = () => {
  let lastInfo = null;
  
  const logInfo = async () => {
    try {
      const info = await getDisplayInfo();
      
      // 检查是否有变化
      if (!lastInfo || 
          info.width !== lastInfo.width || 
          info.height !== lastInfo.height ||
          info.orientation !== lastInfo.orientation) {
        
        console.log('[DisplayInfo]', {
          platform: Platform.OS,
          width: info.width,
          height: info.height,
          density: info.density,
          orientation: info.orientation,
          safeArea: info.safeArea,
          timestamp: new Date().toISOString()
        });
        
        lastInfo = info;
      }
    } catch (error) {
      console.error('[DisplayInfo] Error:', error);
    }
  };
  
  // 初始记录
  logInfo();
  
  // 监听变化
  const listener = addDisplayInfoListener(logInfo);
  
  return {
    stop: () => listener.remove()
  };
};

在应用启动时启用:

javascript 复制代码
// App.js
import { useEffect } from 'react';
import { logDisplayInfoChanges } from './logger';

const App = () => {
  useEffect(() => {
    if (__DEV__) {
      const logger = logDisplayInfoChanges();
      return () => logger.stop();
    }
  }, []);
  
  // ... 其他代码
};

常见问题与解决方案

在 React Native for OpenHarmony 开发中,DisplayInfo 相关的常见问题及解决方案如下表所示:

DisplayInfo 常见问题与解决方案

问题描述 可能原因 解决方案 适用平台
获取的屏幕尺寸不准确 1. 混淆了物理像素和逻辑像素 2. 未考虑状态栏/导航栏高度 3. 多窗口模式下获取了整个屏幕尺寸 1. 使用 toLogicalPixels 转换物理像素 2. 使用 getSafeArea 获取安全区域 3. 在 OpenHarmony 上使用 getMultiWindowInfo OpenHarmony ✅ Android ⚠️
横竖屏切换时布局错乱 1. 未监听方向变化 2. 样式未根据方向动态调整 3. 折叠屏设备上未处理半折叠状态 1. 使用 addOrientationListener 监听方向变化 2. 使用响应式布局系统 3. 对于折叠屏,使用 addFoldableListener OpenHarmony ✅ iOS ⚠️
折叠屏设备上 UI 不适配 1. 未检测折叠状态 2. 未针对展开/折叠状态提供不同布局 3. 未处理桌面模式 1. 使用 getFoldableInfo 获取折叠状态 2. 为不同状态设计专用布局 3. 检测 isTabletopMode 并调整 UI OpenHarmony ✅
高 DPI 设备上图像模糊 1. 未提供高分辨率图像资源 2. 未正确处理像素密度 3. 使用固定像素值而非比例布局 1. 提供 @2x、@3x 图像资源 2. 使用 scaleSize 处理尺寸 3. 优先使用 flex 布局和百分比 所有平台 ✅
多窗口模式下布局异常 1. 假设应用总是全屏运行 2. 未根据窗口尺寸调整布局 3. 未处理窗口大小动态变化 1. 使用 getMultiWindowInfo 获取窗口尺寸 2. 实现紧凑/扩展两种布局模式 3. 使用节流处理窗口大小变化 OpenHarmony ✅ Android ⚠️
安全区域计算不准确 1. 未考虑设备特定的屏幕形状 2. 使用硬编码值而非动态获取 3. 未处理横竖屏切换时的安全区域变化 1. 使用 getSafeArea 获取动态安全区域 2. 避免硬编码安全区域值 3. 监听显示信息变化并更新 OpenHarmony ✅ iOS ⚠️
DisplayInfo 获取超时 1. 在 UI 线程同步调用 2. 网络或系统服务延迟 3. 未处理异步初始化 1. 始终使用异步方式获取 2. 添加超时处理 3. 实现加载状态和错误处理 所有平台 ✅
响应式布局性能问题 1. 频繁重新计算布局 2. 未使用 memoization 3. 在渲染函数中进行复杂计算 1. 使用节流处理频繁事件 2. 使用 useMemo 优化计算 3. 预计算常用布局值 所有平台 ✅

OpenHarmony 与 React Native Dimensions API 对比

特性 OpenHarmony DisplayInfo React Native Dimensions API 建议
获取方式 异步获取,需调用原生模块 同步获取,Dimensions.get() OpenHarmony 上优先使用 DisplayInfo
屏幕尺寸 返回物理像素 返回逻辑像素 注意单位转换
安全区域 提供精确的安全区域信息 iOS 有部分支持,Android 有限 OpenHarmony 上必须使用 DisplayInfo
折叠屏支持 原生支持,提供详细状态 不支持 需要 DisplayInfo 实现
多窗口模式 支持获取窗口尺寸 通常返回整个屏幕尺寸 OpenHarmony 上必须使用 DisplayInfo
事件监听 提供详细的显示变化事件 基本的尺寸变化事件 OpenHarmony 上使用 DisplayInfo 监听
DPI 信息 提供精确的 DPI 信息 有限的密度信息 OpenHarmony 上优先使用 DisplayInfo
跨平台兼容 仅 OpenHarmony 所有 React Native 支持的平台 创建统一 API 层处理差异

总结与展望

本文要点回顾

本文深入探讨了 React Native for OpenHarmony 中 DisplayInfo 显示信息的使用方法与适配技巧:

  1. DisplayInfo 核心概念:我们了解了 DisplayInfo 的主要功能和属性,以及它与 React Native Dimensions API 的关系。DisplayInfo 作为 OpenHarmony 平台特有的 API,提供了更精确、更全面的显示信息,特别是在处理折叠屏、多窗口等复杂场景时表现出色。

  2. 基础用法:通过代码示例,我们学习了如何获取屏幕尺寸、安全区域、屏幕方向和像素密度等基本信息。这些基础用法是构建响应式 UI 的基石,对于确保应用在各种设备上正确显示至关重要。

  3. 进阶用法:我们探讨了动态监听显示信息变化、处理多窗口模式、适配折叠屏设备以及实现响应式布局系统的高级技术。这些进阶用法使我们能够创建更加智能、适应性更强的 UI,提供卓越的用户体验。

  4. OpenHarmony 特定注意事项:我们分析了 OpenHarmony 与 Android 的显示系统差异,讨论了最佳实践和常见陷阱。理解这些差异对于成功开发 OpenHarmony 应用至关重要,能够帮助我们避免常见的适配问题。

  5. 性能与调试:我们分享了性能优化策略和实用的调试技巧,帮助开发者更高效地解决显示适配问题。在实际开发中,性能和调试往往是决定应用质量的关键因素。

技术展望

随着 OpenHarmony 生态的不断发展,DisplayInfo 相关技术也将持续演进:

  1. 更精细的设备适配:未来 OpenHarmony 可能会提供更详细的设备特性信息,如屏幕曲率、刷新率等,帮助开发者实现更精确的适配。例如,可以根据屏幕刷新率调整动画帧率,提供更流畅的用户体验。

  2. 分布式显示支持:随着 OpenHarmony 分布式能力的增强,DisplayInfo 可能会支持跨设备的显示信息获取,实现真正的分布式 UI。例如,应用可以在手机上运行,但显示在电视上,DisplayInfo 将提供电视的显示信息。

  3. AI 驱动的自适应布局:结合 AI 技术,未来的 DisplayInfo 可能会提供智能布局建议,自动优化 UI 以适应不同设备。例如,AI 可以分析用户习惯,自动调整布局以提高可用性。

  4. 标准化跨平台 API:React Native 社区可能会推动标准化的跨平台显示信息 API,减少平台差异带来的适配成本。这将使开发者能够更轻松地构建跨平台应用,而无需处理复杂的平台特定代码。

后续优化方向

对于正在开发 React Native for OpenHarmony 应用的开发者,以下方向值得关注:

  1. 构建更完善的响应式系统:基于本文介绍的技术,可以进一步开发更强大的响应式布局库,支持更复杂的场景。例如,可以集成 CSS 媒体查询的概念,提供更灵活的布局控制。

  2. 优化折叠屏体验:深入研究折叠屏设备的使用模式,设计更自然的展开/折叠过渡动画和交互。例如,可以创建自定义的折叠动画,使 UI 变化更加流畅。

  3. 性能监控与优化:实现 DisplayInfo 相关操作的性能监控,持续优化布局计算和渲染性能。可以使用 React Native 的性能工具,如 Performance Monitor,来识别和解决性能瓶颈。

  4. 跨平台兼容层:开发更健壮的跨平台兼容层,简化 React Native 应用向 OpenHarmony 的迁移过程。这可以包括统一的 API 层、样式系统和组件库,使代码更加可移植。

通过持续学习和实践,我们可以在 React Native for OpenHarmony 开发中取得更大的成功,为用户提供卓越的跨平台体验。

完整项目Demo地址

本文所有代码示例均已整合到完整项目中,您可以在以下地址获取:

完整项目Demo地址:https://atomgit.com/pickstar/AtomGitDemos

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

在这里,您可以与其他开发者交流 React Native for OpenHarmony 的开发经验,共同推动跨平台技术的发展。我们定期分享技术文章、举办线上研讨会,并提供项目支持,帮助您更好地掌握 OpenHarmony 跨平台开发技能。

无论您是刚刚开始接触 OpenHarmony,还是已经有一定经验的开发者,这个社区都将为您提供有价值的学习资源和实践机会。期待与您一起探索 React Native for OpenHarmony 的无限可能!

相关推荐
_李小白2 小时前
【Android 美颜相机】第六天:GPUImageView解析
android·数码相机
哈哈你是真的厉害2 小时前
React Native 鸿蒙跨平台开发:FlatList 基础列表代码指南
react native·react.js·harmonyos
Mr_sun.2 小时前
Day04——权限认证-基础
android·服务器·数据库
北辰当尹5 小时前
第27天 安全开发-PHP应用&TP框架&路由访问&对象操作&内置过滤绕过&核心漏洞
android·安全·php
yueqc15 小时前
Android 线程梳理
android·线程
顾林海5 小时前
Android登录模块设计:别让“大门”变成“破篱笆”
android·经验分享·面试·架构·移动端
摘星编程5 小时前
React Native for OpenHarmony 实战:SnapCarousel 轮播组件详解
javascript·react native·react.js
摘星编程6 小时前
React Native for OpenHarmony 实战:PagingScroll 分页滚动详解
javascript·react native·react.js
嵌入式-老费6 小时前
Android开发(总结)
android