屏幕尺寸发生变化更新监听

import 'dart:math';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:get/get.dart';

class UI {
  static String? packageName;
  /// UI 设计稿尺寸,所有 pxW/pxH 缩放都以这个尺寸作为基准。
  static const Size _designSize = Size(390, 844);

  /// UI 最大适配尺寸,折叠屏展开或大屏设备不会超过这个尺寸继续放大。
  static const Size _maxSize = Size(430, 932);

  /// 当前缓存的屏幕短边宽度,单位是逻辑像素。
  static double _initScreenWidth = 0;

  /// 当前缓存的屏幕长边高度,单位是逻辑像素。
  static double _initScreenHeight = 0;

  /// 当前缓存的顶部安全区高度,单位是逻辑像素。
  static double _initTopSafe = 0;

  /// 当前缓存的底部安全区高度,单位是逻辑像素。
  static double _initBottomSafe = 0;

  /// 当前宽度缩放比例,pxW/sp 会使用这个比例换算设计稿尺寸。
  static double _scaleWidth = 1;

  /// 当前高度缩放比例,pxH 会使用这个比例换算设计稿尺寸。
  static double _scaleHeight = 1;

  /// 标记 UI 尺寸缓存是否已经完成首次初始化,避免 metrics 回调在初始化前触发刷新。
  static bool _metricsInitialized = false;

  /// 标记是否已经安排了一次 Get.forceAppUpdate,用于防止同一帧重复触发全局刷新。
  static bool _forceAppUpdateScheduled = false;

  static double pxW(double px) {
    return _scaleWidth * px;
  }

  static double pxH(double px) {
    return _scaleHeight * px;
  }

  static double sp(double sp) {
    return _scaleWidth * sp;
  }

  static double get screenWidth => min(_initScreenWidth, _initScreenHeight);

  static double get screenHeight => max(_initScreenWidth, _initScreenHeight);

  static double leftSafeH(BuildContext context) {
    return MediaQuery.of(context).padding.left;
  }

  static double bottomSafeH() {
    return _initBottomSafe;
  }

  static double get appBarHeight => AppBar().preferredSize.height;
  static double topSafeH() {
    return _initTopSafe;
  }

  static EdgeInsets edgeLTRB(
      double left, double top, double right, double bottom) {
    // return EdgeInsets.fromLTRB(pxW(left), pxH(top), pxW(right), pxH(bottom));
    return EdgeInsets.fromLTRB(pxW(left), pxW(top), pxW(right), pxW(bottom));
  }

  static EdgeInsets edgeAll(double px) {
    return edgeLTRB(px, px, px, px);
  }

  static EdgeInsets edgeOnly({
    double left = 0.0,
    double top = 0.0,
    double right = 0.0,
    double bottom = 0.0,
  }) {
    return edgeLTRB(left, top, right, bottom);
  }

  static EdgeInsets edgeSymmetric({
    double vertical = 0.0,
    double horizontal = 0.0,
  }) {
    return EdgeInsets.symmetric(
        vertical: pxW(vertical), horizontal: pxW(horizontal));
  }

  /// 初始化 UI 适配数据,并注册折叠屏/分屏尺寸变化监听。
  ///
  /// [context] 用于读取当前页面的 [MediaQuery],完成首次尺寸、安全区和缩放比例计算。
  /// 不改 GetMaterialApp 入口,依赖 AppMainPage 现有调用完成全局尺寸监听注册。
  /// 这里不缓存 BuildContext,也不在 metrics 回调里取 Get.context,避免路由切换时拿到失效或非当前窗口的 context。
  static void initAppUi(BuildContext context) {
    _UIResponsiveObserver.ensureRegistered();
    final changed = updateMetrics(context);
    if (_metricsInitialized && changed) {
      _scheduleForceAppUpdate();
    }
    _metricsInitialized = true;
  }

  /// 使用当前 widget tree 的 [context] 更新 UI 尺寸缓存。
  ///
  /// [context] 用于读取当前页面的 [MediaQuery]。常规 build 阶段优先使用它,
  /// 可以保证首次启动和普通重建时取到与当前页面一致的尺寸。
  /// 返回 `true` 表示屏幕尺寸、安全区或缩放比例发生变化;返回 `false` 表示缓存无需更新。
  static bool updateMetrics(BuildContext context) {
    return _updateMetrics(MediaQuery.of(context));
  }

  /// 根据 [mediaQuery] 更新 UI 尺寸缓存。
  ///
  /// [mediaQuery] 提供屏幕尺寸、安全区和像素密度等窗口信息。
  /// 返回 `true` 表示屏幕尺寸、安全区或缩放比例发生变化;返回 `false` 表示缓存无需更新。
  static bool _updateMetrics(MediaQueryData mediaQuery) {
    final deviceWidth = min(mediaQuery.size.width, mediaQuery.size.height);
    final deviceHeight = max(mediaQuery.size.width, mediaQuery.size.height);
    final topSafe = mediaQuery.padding.top;
    final bottomSafe = mediaQuery.padding.bottom;
    final scaleWidth = min(_maxSize.width, deviceWidth) / _designSize.width;
    final scaleHeight = min(_maxSize.height, deviceHeight) / _designSize.height;

    final changed = _isChanged(_initScreenWidth, deviceWidth) ||
        _isChanged(_initScreenHeight, deviceHeight) ||
        _isChanged(_initTopSafe, topSafe) ||
        _isChanged(_initBottomSafe, bottomSafe) ||
        _isChanged(_scaleWidth, scaleWidth) ||
        _isChanged(_scaleHeight, scaleHeight);

    if (!changed) return false;

    _initScreenWidth = deviceWidth;
    _initScreenHeight = deviceHeight;
    _initTopSafe = topSafe;
    _initBottomSafe = bottomSafe;
    _scaleWidth = scaleWidth;
    _scaleHeight = scaleHeight;
    return true;
  }

  /// 判断两个尺寸值是否有有效变化。
  ///
  /// [oldValue] 是缓存中的旧值,[newValue] 是本次读取的新值。
  /// 返回 `true` 表示差值超过 0.01,需要更新缓存;这个阈值用于过滤浮点误差。
  static bool _isChanged(double oldValue, double newValue) {
    return (oldValue - newValue).abs() > 0.01;
  }

  /// 使用 Flutter View 更新 UI 尺寸缓存。
  ///
  /// [view] 是当前 Flutter 窗口,提供物理尺寸、像素密度和安全区信息。
  /// 折叠屏/分屏尺寸变化时可能没有新的 BuildContext,直接从 Flutter View 读取最新窗口信息。
  /// 相比 Get.context,这里不依赖当前路由 navigator context,更适合 didChangeMetrics 这种全局窗口回调。
  /// 返回 `true` 表示屏幕尺寸、安全区或缩放比例发生变化;返回 `false` 表示缓存无需更新。
  static bool _updateMetricsFromView(ui.FlutterView view) {
    return _updateMetrics(MediaQueryData.fromView(view));
  }

  /// 处理 Flutter 窗口尺寸变化后的 UI 缓存刷新。
  ///
  /// 折叠屏展开/折叠、分屏和旋转都会触发 metrics 变化。
  /// 方法会在下一帧读取最新 [ui.FlutterView],如果尺寸确实变化,再触发 GetX 全局刷新。
  static void _refreshMetricsAfterWindowChanged() {
    if (!_metricsInitialized) return;

    WidgetsBinding.instance.addPostFrameCallback((_) {
      final views = WidgetsBinding.instance.platformDispatcher.views;
      if (views.isEmpty) return;

      final changed = _updateMetricsFromView(views.first);
      if (changed) {
        _scheduleForceAppUpdate();
      }
    });
  }

  /// 防抖触发 GetX 全局刷新。
  ///
  /// UI.pxW/pxH/sp 是静态缓存,尺寸变化不会自动重建页面。
  /// 这里通过 [Get.forceAppUpdate] 让依赖这些静态尺寸的页面重新 build。
  static void _scheduleForceAppUpdate() {
    if (_forceAppUpdateScheduled) return;

    _forceAppUpdateScheduled = true;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _forceAppUpdateScheduled = false;
      Get.forceAppUpdate();
    });
  }
}

class _UIResponsiveObserver extends WidgetsBindingObserver {
  _UIResponsiveObserver._();

  static final _UIResponsiveObserver _instance = _UIResponsiveObserver._();
  static bool _registered = false;

  /// 注册全局窗口变化监听。
  ///
  /// 多次调用只会注册一次,避免重复监听造成重复刷新。
  static void ensureRegistered() {
    if (_registered) return;
    WidgetsBinding.instance.addObserver(_instance);
    _registered = true;
  }

  @override
  void didChangeMetrics() {
    UI._refreshMetricsAfterWindowChanged();
  }
}

 

posted @ 2026-07-03 13:54  呢哇哦比较  阅读(5)  评论(0)    收藏  举报