opt scale

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-01-11 19:53:11 +08:00
parent 09bd1edeb3
commit 4a2679a589
6 changed files with 142 additions and 39 deletions

View File

@@ -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<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
/// 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());
}
}
}

View File

@@ -4,6 +4,7 @@ import 'package:PiliPlus/build_config.dart';
import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/custom_toast.dart'; import 'package:PiliPlus/common/widgets/custom_toast.dart';
import 'package:PiliPlus/common/widgets/mouse_back.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/http/init.dart';
import 'package:PiliPlus/models/common/theme/theme_color_type.dart'; import 'package:PiliPlus/models/common/theme/theme_color_type.dart';
import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart';
@@ -46,7 +47,7 @@ import 'package:window_manager/window_manager.dart' hide calcWindowPosition;
WebViewEnvironment? webViewEnvironment; WebViewEnvironment? webViewEnvironment;
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); ScaledWidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
tmpDirPath = (await getTemporaryDirectory()).path; tmpDirPath = (await getTemporaryDirectory()).path;
appSupportDirPath = (await getApplicationSupportDirectory()).path; appSupportDirPath = (await getApplicationSupportDirectory()).path;
@@ -57,6 +58,7 @@ void main() async {
if (kDebugMode) debugPrint('GStorage init error: $e'); if (kDebugMode) debugPrint('GStorage init error: $e');
exit(0); exit(0);
} }
ScaledWidgetsFlutterBinding.instance.setScaleFactor(Pref.uiScale);
if (PlatformUtils.isDesktop) { if (PlatformUtils.isDesktop) {
final customDownPath = Pref.downloadPath; final customDownPath = Pref.downloadPath;
@@ -298,29 +300,16 @@ class MyApp extends StatelessWidget {
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final textScaler = TextScaler.linear(Pref.defaultTextScale); final textScaler = TextScaler.linear(Pref.defaultTextScale);
if (uiScale != 1.0) { if (uiScale != 1.0) {
// Apply full UI scaling for desktop
final actualSize = mediaQuery.size;
final scaledSize = actualSize / uiScale;
child = MediaQuery( child = MediaQuery(
data: mediaQuery.copyWith( data: mediaQuery.copyWith(
// Tell child the logical size it should layout to
size: scaledSize,
textScaler: textScaler, textScaler: textScaler,
size: mediaQuery.size / uiScale,
padding: mediaQuery.padding / uiScale, padding: mediaQuery.padding / uiScale,
viewPadding: mediaQuery.viewPadding / uiScale,
viewInsets: mediaQuery.viewInsets / uiScale, viewInsets: mediaQuery.viewInsets / uiScale,
viewPadding: mediaQuery.viewPadding / uiScale,
devicePixelRatio: mediaQuery.devicePixelRatio * uiScale,
), ),
// Use OverflowBox to let child layout to scaledSize, child: child!,
// 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,
),
),
); );
} else { } else {
child = MediaQuery( child = MediaQuery(

View File

@@ -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/pages/video/widgets/header_control.dart';
import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@@ -295,11 +294,9 @@ class LiveRoomChatPanel extends StatelessWidget {
TapUpDetails details, TapUpDetails details,
DanmakuMsg item, DanmakuMsg item,
) { ) {
final uiScale = Pref.uiScale; final dx = details.globalPosition.dx;
final dx = details.globalPosition.dx / uiScale;
final renderBox = itemContext.findRenderObject() as RenderBox; final renderBox = itemContext.findRenderObject() as RenderBox;
final dy = final dy = renderBox.localToGlobal(renderBox.size.bottomLeft(.zero)).dy;
renderBox.localToGlobal(renderBox.size.bottomLeft(.zero)).dy / uiScale;
final autoScroll = final autoScroll =
liveRoomController.autoScroll && liveRoomController.autoScroll &&
!liveRoomController.disableAutoScroll.value; !liveRoomController.disableAutoScroll.value;

View File

@@ -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/custom_toast.dart';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.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/common/widgets/stateful_builder.dart';
import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/main.dart';
import 'package:PiliPlus/models/common/dynamic/dynamic_badge_mode.dart'; import 'package:PiliPlus/models/common/dynamic/dynamic_badge_mode.dart';
@@ -56,12 +57,6 @@ List<SettingsModel> get styleSettings => [
needReboot: true, needReboot: true,
), ),
], ],
NormalModel(
title: '界面缩放',
getSubtitle: () => '当前缩放比例:${Pref.uiScale.toStringAsFixed(2)}',
leading: const Icon(Icons.zoom_in_outlined),
onTap: _showUiScaleDialog,
),
SwitchModel( SwitchModel(
title: '横屏适配', title: '横屏适配',
subtitle: '启用横屏布局与逻辑,平板、折叠屏等可开启;建议全屏方向设为【不改变当前方向】', subtitle: '启用横屏布局与逻辑,平板、折叠屏等可开启;建议全屏方向设为【不改变当前方向】',
@@ -116,6 +111,12 @@ List<SettingsModel> get styleSettings => [
Get.forceAppUpdate(); Get.forceAppUpdate();
}, },
), ),
NormalModel(
title: '界面缩放',
getSubtitle: () => '当前缩放比例:${Pref.uiScale.toStringAsFixed(2)}',
leading: const Icon(Icons.zoom_in_outlined),
onTap: _showUiScaleDialog,
),
NormalModel( NormalModel(
title: '页面过渡动画', title: '页面过渡动画',
leading: const Icon(Icons.animation), leading: const Icon(Icons.animation),
@@ -846,10 +847,10 @@ void _showUiScaleDialog(
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
Pref.uiScale = 1.0;
GStorage.setting.delete(SettingBoxKey.uiScale).whenComplete(() { GStorage.setting.delete(SettingBoxKey.uiScale).whenComplete(() {
setState(); setState();
Get.appUpdate(); Get.appUpdate();
ScaledWidgetsFlutterBinding.instance.setScaleFactor(1.0);
}); });
}, },
child: const Text('重置'), child: const Text('重置'),
@@ -866,11 +867,11 @@ void _showUiScaleDialog(
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
Pref.uiScale = uiScale;
GStorage.setting.put(SettingBoxKey.uiScale, uiScale).whenComplete( GStorage.setting.put(SettingBoxKey.uiScale, uiScale).whenComplete(
() { () {
setState(); setState();
Get.appUpdate(); Get.appUpdate();
ScaledWidgetsFlutterBinding.instance.setScaleFactor(uiScale);
}, },
); );
}, },

View File

@@ -44,10 +44,7 @@ abstract final class PageUtils {
RouteObserver<PageRoute>(); RouteObserver<PageRoute>();
static RelativeRect menuPosition(Offset offset) { static RelativeRect menuPosition(Offset offset) {
final uiScale = Pref.uiScale; return .fromLTRB(offset.dx, offset.dy, offset.dx, 0);
final dx = offset.dx / uiScale;
final dy = offset.dy / uiScale;
return .fromLTRB(dx, dy, dx, 0);
} }
static Future<void> imageView({ static Future<void> imageView({

View File

@@ -649,10 +649,8 @@ abstract final class Pref {
static double get defaultTextScale => static double get defaultTextScale =>
_setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0); _setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);
static double uiScale = _setting.get( static double get uiScale =>
SettingBoxKey.uiScale, _setting.get(SettingBoxKey.uiScale, defaultValue: 1.0);
defaultValue: 1.0,
);
static bool get dynamicsWaterfallFlow => static bool get dynamicsWaterfallFlow =>
_setting.get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true); _setting.get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true);