HarmonyOS 鸿蒙Next中Flutter框架多设备开发指导-悬停态适配场景

HarmonyOS 鸿蒙Next中Flutter框架多设备开发指导-悬停态适配场景

1.1 场景概述

在移动应用开发中,不同设备的屏幕形态各异,例如刘海屏、全面屏、折叠屏等,系统状态栏、导航栏、软键盘等元素亦会占据屏幕空间。折叠设备在半折悬停等形态下,折痕附近区域不宜放置关键操作,主内容区与底部交互带的可用高度也与全屏展开态不同。本文将详细介绍如何结合折叠状态、窗口尺寸与高度档位判定悬停布局,以及折痕避让、主从区域高度分配的实现原理与具体场景案例。本文对应工程内悬停布局示例页。

1.1.1 使用场景

用户在折叠屏半开悬停看视频或浏览内容时,需要重要按钮避开折痕、主画面与底部操作区比例合理,且从全屏到悬停切换时返回入口仍易点、不遮挡内容。悬停布局适配验证(折叠半开悬停态界面自适应),指的是页面主从区域能够根据折叠状态、逻辑高度档与横竖屏,自动切换是否进入悬停布局:在悬停态下增加折痕避让与动态填充占位、调整返回条位置,并按断点调节底部交互区高度,以提供折痕处不挡操作、悬停与全屏形态都好用的布局体验的布局方法。

横向断点 sm md lg xl
展示逻辑 底部交互区域高度约 80(逻辑像素,下同) 交互区高度约 90 交互区高度约 100 交互区高度约 120
展示布局

1.1.2 常见问题

悬停与折叠协同类界面若未统筹状态与尺寸,容易出现以下问题:

  • 未监听 Dimensions 变化,旋转或分屏后高度档与悬停判定仍用旧值
  • 悬停与非悬停共用同一顶栏坐标,与新增避让区重叠或遮挡
  • 主区与底部交互区高度分配固定,小屏主区过小或交互区难点击
  • 折叠 API 在部分环境不可用导致异常,未 try/catch 与降级

1.1.3 多设备适配

  • 适配点 1:在支持折叠的设备上切换半折叠,结合竖屏 + 大高度档中高度档 + 横屏(及窄竖屏) 等条件,验证是否出现折痕避让区动态填充区返回条位置变化。

图 1-1手机竖屏下半折叠悬停:主区、避让区、动态区与底部交互区关系。

图 1-2手机横屏下同一页,验证折痕避让与主区排布随方向变化。

  • 适配点 2横竖屏切换窗口尺寸变化后,高度断点与方向重算,悬停判定与主区 / 交互区高度是否同步更新;窄分窗下逻辑宽度 小于 500 的竖屏特例是否与预期一致。

图 2-1md 断点(交互区约 90)下界面效果。

图 2-2lg 断点(交互区约 100)下分屏或拉窗后主区剩余高度与交互区比例。

图 2-3PC / 特大宽(交互区约 120)下整体布局。

1.2 开发指导

1.2.1 Flutter开发

1.2.1.1 关键能力

1、折叠状态感知

通过鸿蒙原生 display API 实时感知设备的折叠状态,支持以下状态识别:

折叠状态 枚举值 编码 说明
FOLD_STATUS_EXPANDED expanded 1 设备完全展开,屏幕平整
FOLD_STATUS_FOLDED folded 2 设备完全折叠,仅外屏可用
FOLD_STATUS_HALF_FOLDED halfFolded 3 设备半折叠,处于悬停态
未知 unknown 0 非折叠屏设备或状态获取失败

支持两种获取方式:

  • 主动查询:通过 getFoldStatus() 一次性获取当前折叠状态
  • 被动监听:通过 foldStatusStream 实时监听折叠状态变化事件

2、折痕区域检测

当设备处于悬停态时,可获取屏幕折痕的精确位置信息:

  • creaseTop:折痕区域距屏幕顶部的距离(单位 vp)
  • creaseHeight:折痕区域的高度(单位 vp)

原生侧通过 display.getCurrentFoldCreaseRegion() 获取像素值,再经 px2vp() 转换为虚拟像素,确保与 Flutter 布局坐标系一致。

3、Flutter-鸿蒙原生通信

通过 MethodChannel 和 EventChannel 实现 Flutter 与鸿蒙原生层的双向通信:

通道类型 通道名称 用途
MethodChannel fold_status_detector 主动查询折叠状态、获取折痕区域
EventChannel fold_status_detector/events 实时推送折叠状态变化事件

1.2.1.2 指导案例

1、原生插件实现

原生插件实现 FlutterPlugin 和 MethodCallHandler 接口,负责与鸿蒙 display API 交互。

import {
  FlutterPlugin, FlutterPluginBinding,
  MethodChannel, MethodCall, MethodResult, MethodCallHandler,
  EventChannel, EventSink, StreamHandler
} from '@ohos/flutter_ohos';
import { display } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

export default class FoldStatusPlugin implements FlutterPlugin, MethodCallHandler {
  private methodChannel: MethodChannel | null = null;
  private eventChannel: EventChannel | null = null;
  private streamHandler: FoldStatusStreamHandler | null = null;

  getUniqueClassName(): string {
    return "FoldStatusPlugin";
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    // 创建 MethodChannel,处理一次性查询请求
    this.methodChannel = new MethodChannel(binding.getBinaryMessenger(), 'fold_status_detector');
    this.methodChannel.setMethodCallHandler(this);

    // 创建 EventChannel,处理持续监听请求
    this.streamHandler = new FoldStatusStreamHandler();
    this.eventChannel = new EventChannel(binding.getBinaryMessenger(), 'fold_status_detector/events');
    this.eventChannel.setStreamHandler(this.streamHandler);
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    if (this.methodChannel != null) {
      this.methodChannel.setMethodCallHandler(null);
      this.methodChannel = null;
    }
    if (this.eventChannel != null) {
      this.eventChannel.setStreamHandler(null);
      this.eventChannel = null;
    }
    this.streamHandler = null;
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    if (call.method === 'getFoldStatus') {
      this.getFoldStatus(result);
    } else if (call.method === 'getCreaseRegion') {
      this.getCreaseRegion(result);
    } else {
      result.notImplemented();
    }
  }
}

1.1、MethodChannel 方法实现

插件通过 onMethodCall 分发两个方法调用:

getFoldStatus — 获取当前折叠状态,调用 display.getFoldStatus() 并将枚举转换为整数编码返回:

private getFoldStatus(result: MethodResult): void {
  try {
    const foldStatus = display.getFoldStatus();
    let statusCode = 0;
    switch (foldStatus) {
      case display.FoldStatus.FOLD_STATUS_EXPANDED:
      statusCode = 1; break;
        case display.FoldStatus.FOLD_STATUS_FOLDED:
        statusCode = 2; break;
        case display.FoldStatus.FOLD_STATUS_HALF_FOLDED:
        statusCode = 3; break;
        default:
        statusCode = 0; break;
    }
    result.success(statusCode);
  } catch (error) {
    const err = error as BusinessError;
    result.error('FOLD_STATUS_ERROR', err.message, null);
  }
}

getCreaseRegion — 获取折痕区域,先通过 display.isFoldable() 判断设备是否支持折叠,再调用 display.getCurrentFoldCreaseRegion() 获取折痕矩形,通过 px2vp() 转换为虚拟像素:

private getCreaseRegion(result: MethodResult): void {
  try {
    if (display.isFoldable()) {
      const foldRegion: display.FoldCreaseRegion = display.getCurrentFoldCreaseRegion();
      const rect: display.Rect = foldRegion.creaseRects[0];
      const top: number = px2vp(rect.top);
      const height: number = px2vp(rect.height);
      result.success([top, height]);
    } else {
      result.success([0, 0]);
    }
  } catch (error) {
    const err = error as BusinessError;
    result.error('CREASE_REGION_ERROR', err.message, null);
  }
}

1.2 EventChannel 流式监听实现

通过 FoldStatusStreamHandler 实现 StreamHandler 接口,在 onListen 中注册 display.on(‘foldStatusChange’) 监听,在 onCancel 中通过 display.off() 取消监听:

class FoldStatusStreamHandler implements StreamHandler {
  private eventSink: EventSink | null = null;
  private foldStatusCallback: ((foldStatus: display.FoldStatus) => void) | null = null;

  onListen(args: ESObject, sink: EventSink): void {
    this.eventSink = sink;
    try {
      this.foldStatusCallback = (foldStatus: display.FoldStatus) => {
        if (this.eventSink != null) {
          this.eventSink.success(this.convertStatus(foldStatus));
        }
      };
      display.on('foldStatusChange', this.foldStatusCallback);
    } catch (error) {
      const err = error as BusinessError;
      if (this.eventSink != null) {
        this.eventSink.error('FOLD_STATUS_LISTENER_ERROR', err.message, null);
      }
    }
  }

  onCancel(args: ESObject): void {
    try {
      if (this.foldStatusCallback != null) {
        display.off('foldStatusChange', this.foldStatusCallback);
        this.foldStatusCallback = null;
      }
    } catch (error) {}
    this.eventSink = null;
  }

  private convertStatus(foldStatus: display.FoldStatus): number {
    switch (foldStatus) {
      case display.FoldStatus.FOLD_STATUS_EXPANDED: return 1;
      case display.FoldStatus.FOLD_STATUS_FOLDED: return 2;
      case display.FoldStatus.FOLD_STATUS_HALF_FOLDED: return 3;
      default: return 0;
    }
  }
}

2、Dart侧检测器实现

FoldStatusDetector 封装了 MethodChannel 和 EventChannel 的调用,提供三个静态方法:

import 'package:flutter/services.dart';

class FoldStatusDetector {
  static const MethodChannel _channel = MethodChannel('fold_status_detector');

  /// 一次性获取当前折叠状态
  static Future<FoldStatus> getFoldStatus() async {
    try {
      final int? status = await _channel.invokeMethod('getFoldStatus');
      return _parseFoldStatus(status);
    } catch (e) {
      return FoldStatus.unknown;
    }
  }
  
  /// 通过 EventChannel 持续监听折叠状态变化
  static Stream<FoldStatus> get foldStatusStream {
    return const EventChannel('fold_status_detector/events')
    .receiveBroadcastStream()
    .map((dynamic status) => _parseFoldStatus(status as int?));
  }
  
  /// 获取折痕区域 [top, height]
  static Future<List<double>> getCreaseRegion() async {
    try {
      final List<dynamic>? region =
      await _channel.invokeMethod('getCreaseRegion');
      if (region != null && region.length >= 2) {
        return [
          (region[0] as num).toDouble(),
          (region[1] as num).toDouble(),
        ];
      }
    } catch (e) {}
    return [0, 0];
  }
  
  static FoldStatus _parseFoldStatus(int? status) {
    switch (status) {
      case 1: return FoldStatus.expanded;
      case 2: return FoldStatus.folded;
      case 3: return FoldStatus.halfFolded;
      default: return FoldStatus.unknown;
    }
  }
}

3、获取折叠状态

import 'fold_status_detector.dart';

// 一次性查询当前状态
final status = await FoldStatusDetector.getFoldStatus();
if (status == FoldStatus.halfFolded) {
  // 当前处于悬停态
}

// 持续监听状态变化
final subscription = FoldStatusDetector.foldStatusStream.listen((status) {
  switch (status) {
    case FoldStatus.expanded:
    // 展开状态处理
      break;
    case FoldStatus.halfFolded:
    // 悬停态处理
      break;
    case FoldStatus.folded:
    // 折叠状态处理
      break;
    default:
      break;
  }
});

// 页面销毁时取消监听
subscription.cancel();

4、获取折痕区域

在悬停态下获取折痕位置,用于布局避让:

final creaseRegion = await FoldStatusDetector.getCreaseRegion();
final creaseTop = creaseRegion[0]; // 折痕距顶部距离(vp)
final creaseHeight = creaseRegion[1]; // 折痕高度(vp)

5、实现自定义悬停态布局

class _MyHoverPageState extends State<MyHoverPage> {
  bool _isHoverMode = false;
  double _creaseTop = 0;
  double _creaseHeight = 0;
  StreamSubscription<FoldStatus>? _subscription;

  //'''

  @override
  Widget build(BuildContext context) {
    if (!_isHoverMode) {
      // 非悬停态:常规布局
      return MyNormalLayout();
    }
    // 悬停态:根据 _creaseTop 和 _creaseHeight 分区布局
    return Column(
        children: [
        SizedBox(
          height: _creaseTop,
          child: MyUpperContent(), // 折痕上方内容
        ),
        SizedBox(height: _creaseHeight), // 折痕避让
        Expanded(
          child: MyLowerContent(), // 折痕下方内容
        ),
      ],
    );
  }
}

1.2.1.3 示例代码

悬停态布局的Sample示例代码地址:example ,开发者可以通过该地址查看完整的视频通话示例代码,并根据自己的需求进行修改和扩展。


更多关于HarmonyOS 鸿蒙Next中Flutter框架多设备开发指导-悬停态适配场景的实战教程也可以访问 https://www.itying.com/category-92-b0.html

2 回复

在HarmonyOS NEXT中,Flutter框架通过MouseRegionGestureDetector.onHover监听悬停事件。多设备适配时,利用MediaQuery判断设备类型,结合LayoutBuilder动态调整UI。悬停态可通过InkWellAnimatedContainer等实现视觉反馈,无需依赖系统层事件。

更多关于HarmonyOS 鸿蒙Next中Flutter框架多设备开发指导-悬停态适配场景的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在HarmonyOS Next中,Flutter悬停态适配的关键是结合折叠状态、折痕检测和断点高度分配。通过原生display API获取半折叠状态,通过MethodChannel和EventChannel实现Flutter与原生双向通信,主动查询或监听折叠变化。在悬停态下,根据窗口高度断点动态调整底部交互区高度,并利用折痕区域(creaseTop/creaseHeight)在布局中插入避让区,确保主内容不被折痕遮挡,返回条等元素也随之重定位,从而实现全屏与悬停切换时界面自适应。

回到顶部