diff --git a/lib/common/widgets/scale_app.dart b/lib/common/widgets/scale_app.dart new file mode 100644 index 000000000..308567ffb --- /dev/null +++ b/lib/common/widgets/scale_app.dart @@ -0,0 +1,121 @@ +import 'dart:async' show scheduleMicrotask; +import 'dart:collection' show Queue; +import 'dart:ui' show PointerDataPacket; + +import 'package:flutter/gestures.dart' show PointerEventConverter; +import 'package:flutter/rendering.dart' show RenderView, ViewConfiguration; +import 'package:flutter/widgets.dart'; + +/// ref https://github.com/LastMonopoly/scaled_app + +/// Adapted from [WidgetsFlutterBinding] +/// +class ScaledWidgetsFlutterBinding extends WidgetsFlutterBinding { + ScaledWidgetsFlutterBinding._({double scaleFactor = 1.0}) + : _scaleFactor = scaleFactor; + + /// Calculate scale factor from device size. + double _scaleFactor; + + /// Update scaleFactor callback, then rebuild layout + void setScaleFactor(double scaleFactor) { + if (_scaleFactor == scaleFactor) return; + _scaleFactor = scaleFactor; + handleMetricsChanged(); + } + + double devicePixelRatioScaled = 0; + + static ScaledWidgetsFlutterBinding? _binding; + + static ScaledWidgetsFlutterBinding get instance => _binding!; + + /// Scaling will be applied based on [scaleFactor] callback. + /// + static WidgetsBinding ensureInitialized({double scaleFactor = 1.0}) => + _binding ??= ScaledWidgetsFlutterBinding._(scaleFactor: scaleFactor); + + /// Override the method from [RendererBinding.createViewConfiguration] to + /// change what size or device pixel ratio the [RenderView] will use. + /// + /// See more: + /// * [RendererBinding.createViewConfiguration] + /// * [TestWidgetsFlutterBinding.createViewConfiguration] + @override + ViewConfiguration createViewConfigurationFor(RenderView renderView) { + final view = renderView.flutterView; + final devicePixelRatio = view.devicePixelRatio; + devicePixelRatioScaled = devicePixelRatio * _scaleFactor; + final BoxConstraints physicalConstraints = + BoxConstraints.fromViewConstraints(view.physicalConstraints); + return ViewConfiguration( + physicalConstraints: physicalConstraints, + logicalConstraints: physicalConstraints / devicePixelRatioScaled, + devicePixelRatio: devicePixelRatioScaled, + ); + } + + /// Adapted from [GestureBinding.initInstances] + @override + void initInstances() { + super.initInstances(); + platformDispatcher.onPointerDataPacket = _handlePointerDataPacket; + } + + @override + void unlocked() { + super.unlocked(); + _flushPointerEventQueue(); + } + + final Queue _pendingPointerEvents = Queue(); + + /// When we scale UI using [ViewConfiguration], [ui.window] stays the same. + /// + /// [GestureBinding] uses [platformDispatcher.implicitView.devicePixelRatio] for calculations, + /// so we override corresponding methods. + /// + void _handlePointerDataPacket(PointerDataPacket packet) { + // We convert pointer data to logical pixels so that e.g. the touch slop can be + // defined in a device-independent manner. + try { + _pendingPointerEvents.addAll( + PointerEventConverter.expand(packet.data, _devicePixelRatioForView), + ); + if (!locked) { + _flushPointerEventQueue(); + } + } catch (error, stack) { + FlutterError.reportError( + FlutterErrorDetails( + exception: error, + stack: stack, + library: 'gestures library', + context: ErrorDescription('while handling a pointer data packet'), + ), + ); + } + } + + double _devicePixelRatioForView(int viewId) => devicePixelRatioScaled; + + /// Dispatch a [PointerCancelEvent] for the given pointer soon. + /// + /// The pointer event will be dispatched before the next pointer event and + /// before the end of the microtask but not within this function call. + @override + void cancelPointer(int pointer) { + if (_pendingPointerEvents.isEmpty && !locked) { + scheduleMicrotask(_flushPointerEventQueue); + } + _pendingPointerEvents.addFirst(PointerCancelEvent(pointer: pointer)); + } + + void _flushPointerEventQueue() { + assert(!locked); + + while (_pendingPointerEvents.isNotEmpty) { + handlePointerEvent(_pendingPointerEvents.removeFirst()); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 5a01aa232..5cf1e0b5f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/build_config.dart'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/custom_toast.dart'; import 'package:PiliPlus/common/widgets/mouse_back.dart'; +import 'package:PiliPlus/common/widgets/scale_app.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/models/common/theme/theme_color_type.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; @@ -46,7 +47,7 @@ import 'package:window_manager/window_manager.dart' hide calcWindowPosition; WebViewEnvironment? webViewEnvironment; void main() async { - WidgetsFlutterBinding.ensureInitialized(); + ScaledWidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); tmpDirPath = (await getTemporaryDirectory()).path; appSupportDirPath = (await getApplicationSupportDirectory()).path; @@ -57,6 +58,7 @@ void main() async { if (kDebugMode) debugPrint('GStorage init error: $e'); exit(0); } + ScaledWidgetsFlutterBinding.instance.setScaleFactor(Pref.uiScale); if (PlatformUtils.isDesktop) { final customDownPath = Pref.downloadPath; @@ -298,29 +300,16 @@ class MyApp extends StatelessWidget { final mediaQuery = MediaQuery.of(context); final textScaler = TextScaler.linear(Pref.defaultTextScale); if (uiScale != 1.0) { - // Apply full UI scaling for desktop - final actualSize = mediaQuery.size; - final scaledSize = actualSize / uiScale; child = MediaQuery( data: mediaQuery.copyWith( - // Tell child the logical size it should layout to - size: scaledSize, textScaler: textScaler, + size: mediaQuery.size / uiScale, padding: mediaQuery.padding / uiScale, - viewPadding: mediaQuery.viewPadding / uiScale, viewInsets: mediaQuery.viewInsets / uiScale, + viewPadding: mediaQuery.viewPadding / uiScale, + devicePixelRatio: mediaQuery.devicePixelRatio * uiScale, ), - // Use OverflowBox to let child layout to scaledSize, - // then FittedBox scales it to fit actualSize - child: FittedBox( - fit: BoxFit.fill, - alignment: Alignment.topLeft, - child: SizedBox( - width: scaledSize.width, - height: scaledSize.height, - child: child, - ), - ), + child: child!, ); } else { child = MediaQuery( diff --git a/lib/pages/live_room/widgets/chat_panel.dart b/lib/pages/live_room/widgets/chat_panel.dart index 67e2ce43f..179270e70 100644 --- a/lib/pages/live_room/widgets/chat_panel.dart +++ b/lib/pages/live_room/widgets/chat_panel.dart @@ -9,7 +9,6 @@ import 'package:PiliPlus/pages/live_room/superchat/superchat_card.dart'; import 'package:PiliPlus/pages/video/widgets/header_control.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; -import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/gestures.dart'; @@ -295,11 +294,9 @@ class LiveRoomChatPanel extends StatelessWidget { TapUpDetails details, DanmakuMsg item, ) { - final uiScale = Pref.uiScale; - final dx = details.globalPosition.dx / uiScale; + final dx = details.globalPosition.dx; final renderBox = itemContext.findRenderObject() as RenderBox; - final dy = - renderBox.localToGlobal(renderBox.size.bottomLeft(.zero)).dy / uiScale; + final dy = renderBox.localToGlobal(renderBox.size.bottomLeft(.zero)).dy; final autoScroll = liveRoomController.autoScroll && !liveRoomController.disableAutoScroll.value; diff --git a/lib/pages/setting/models/style_settings.dart b/lib/pages/setting/models/style_settings.dart index 8141b9070..b38c3aa12 100644 --- a/lib/pages/setting/models/style_settings.dart +++ b/lib/pages/setting/models/style_settings.dart @@ -5,6 +5,7 @@ import 'package:PiliPlus/common/widgets/color_palette.dart'; import 'package:PiliPlus/common/widgets/custom_toast.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/scale_app.dart'; import 'package:PiliPlus/common/widgets/stateful_builder.dart'; import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/common/dynamic/dynamic_badge_mode.dart'; @@ -56,12 +57,6 @@ List get styleSettings => [ needReboot: true, ), ], - NormalModel( - title: '界面缩放', - getSubtitle: () => '当前缩放比例:${Pref.uiScale.toStringAsFixed(2)}', - leading: const Icon(Icons.zoom_in_outlined), - onTap: _showUiScaleDialog, - ), SwitchModel( title: '横屏适配', subtitle: '启用横屏布局与逻辑,平板、折叠屏等可开启;建议全屏方向设为【不改变当前方向】', @@ -116,6 +111,12 @@ List get styleSettings => [ Get.forceAppUpdate(); }, ), + NormalModel( + title: '界面缩放', + getSubtitle: () => '当前缩放比例:${Pref.uiScale.toStringAsFixed(2)}', + leading: const Icon(Icons.zoom_in_outlined), + onTap: _showUiScaleDialog, + ), NormalModel( title: '页面过渡动画', leading: const Icon(Icons.animation), @@ -846,10 +847,10 @@ void _showUiScaleDialog( TextButton( onPressed: () { Navigator.pop(context); - Pref.uiScale = 1.0; GStorage.setting.delete(SettingBoxKey.uiScale).whenComplete(() { setState(); Get.appUpdate(); + ScaledWidgetsFlutterBinding.instance.setScaleFactor(1.0); }); }, child: const Text('重置'), @@ -866,11 +867,11 @@ void _showUiScaleDialog( TextButton( onPressed: () { Navigator.pop(context); - Pref.uiScale = uiScale; GStorage.setting.put(SettingBoxKey.uiScale, uiScale).whenComplete( () { setState(); Get.appUpdate(); + ScaledWidgetsFlutterBinding.instance.setScaleFactor(uiScale); }, ); }, diff --git a/lib/utils/page_utils.dart b/lib/utils/page_utils.dart index dfa90247b..36b9aba87 100644 --- a/lib/utils/page_utils.dart +++ b/lib/utils/page_utils.dart @@ -44,10 +44,7 @@ abstract final class PageUtils { RouteObserver(); static RelativeRect menuPosition(Offset offset) { - final uiScale = Pref.uiScale; - final dx = offset.dx / uiScale; - final dy = offset.dy / uiScale; - return .fromLTRB(dx, dy, dx, 0); + return .fromLTRB(offset.dx, offset.dy, offset.dx, 0); } static Future imageView({ diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index cd8c3ebca..33abe64a0 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -649,10 +649,8 @@ abstract final class Pref { static double get defaultTextScale => _setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0); - static double uiScale = _setting.get( - SettingBoxKey.uiScale, - defaultValue: 1.0, - ); + static double get uiScale => + _setting.get(SettingBoxKey.uiScale, defaultValue: 1.0); static bool get dynamicsWaterfallFlow => _setting.get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true);