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();
}
}