preface
In the process of mobile development, screen adaptation is often needed to solve the problem that a fixed design size presents different effects on different devices. While there are many proven solutions for screen adaptation in Android development, there doesn’t seem to be any good ones for Flutter, so this article will explore a very low-cost screen adaptation solution for Flutter.
Effect without adaptation:
However, for visual designers, the desired effect is as follows:
Consider why the same control in Flutter looks so different on different devices.
How are dimensions calculated in Flutter?
Two concepts are introduced here: physical pixels and logical pixels.
- Physical pixels, also known as device pixels, are the basic units of the screen and the dimensions we see. The iPhone 13’s screen, for example, has 1,170 pixels in width and 2,532 pixels in height.
- Logical pixels, also known as device – or resolution-independent pixels. Flutter, as a cross-platform framework, must extract a new unit to fit a different platform, causing confusion if the original unit concept is used.
The physical pixel is the product of the logical pixel value and the devicePixelRatio (hereafter referred to as DPR). * * the
Physical pixel px = logical pixel * devicePixelRatioCopy the code
In Flutter, devicePixelRatio is provided by the UI. Window class.Window is the interface that the Flutter Framework connects to the host operating system. Therefore, the devicePixelRatio attribute in the DART code is exactly what the engine layer gets from the original lifecycle. This value corresponds to Density in Android and to [UIScreen mainScreen].scale in iOS. The reason why the same logical pixel looks different on phones with different resolutions is that each device may have a different DPR.
Mainstream solutions on the Web
Flutter_screenutil: a popular screen adaptation scheme on the Internet. The main principle is equal scaling. It first obtains the size ratio of the actual device and the prototype device, and then ADAPTS according to PX.
The core code is as follows:
/// Get the ratio of the actual size to the UI design using width as an example
double get scaleWidth => _screenWidth / uiSize.width;
/// Device width adaptation based on UI design Take width as an example
double setWidth(num width) => width * scaleWidth;
Copy the code
Usage code:
/// Use 1
Container(
width: ScreenUtil().setWidth(50),
height:ScreenUtil().setHeight(200),/// Use 2
Container(
width: 50.w,
height:200.h
)
Copy the code
This scheme has great limitations, requiring extension functions to be added to every place used, which is too intrusive, seriously affecting the use of the look and feel, and difficult to maintain in the later stage. This is often the most widely used method on the web. So do we need to adapt one by one, each value using extension methods to change?
Refer to the headlines of Android native adaptation solution a very low cost Android screen adaptation, think can be adapted to a wide dimension, and then in a unified entry to complete the adaptation work.
Lower cost scheme exploration
Option 1: Modify from the SDK layer
The RuntimeController calls CreateRunningRootIsolate to return a DartIsolate object every time a Flutter engine starts. At the same time through FlushRuntimeStateToIsolate method calls to SetViewportMetrics call to Window UpdateWindowMetrics method to update the properties of the Window.
The engine startup process is shown below :(refer to Gityuan’s in-depth understanding of Flutter engine startup)
Since the window properties can be updated, we should update the Window properties again after the engine calls UpdateWindowMetrics. Window is a SingletonFlutterWindow type, which is a subclass of FlutterWindow, and FlutterWindow is a concrete implementation class of FlutterView.
According to the explanation in FlutterView source code, we located the value of devicePixelRatio:
double get devicePixelRatio => viewConfiguration.devicePixelRatio
Copy the code
The viewConfiguration here is obtained in the FlutterWindow class
class FlutterWindow extends FlutterView {
FlutterWindow._(this._windowId, this.platformDispatcher);
/// The opaque ID for this view.
final Object _windowId;
@override
final PlatformDispatcher platformDispatcher;
@override
ViewConfiguration get viewConfiguration {
assert(platformDispatcher._viewConfigurations.containsKey(_windowId));
return platformDispatcher._viewConfigurations[_windowId]!;
}
}
Copy the code
ViewConfiguration is the configuration of the Platform View, which directly affects the visual effect we can see. The main fields are as follows:
const ViewConfiguration({
this.window.// The ratio of physical pixels to logical pixels is explained above
this.devicePixelRatio = 1.0.// The position and size of the View rendered by Flutter in the Native platform
this.geometry = Rect.zero,
this.visible = false.// The size of each side and the margin that can display the content
this.viewInsets = WindowPadding.zero,
// Sum viewInsets and padding
this.viewPadding = WindowPadding.zero,
this.systemGestureInsets = WindowPadding.zero,
// The system UI display area such as the status bar, this part of the area should not display the content, otherwise it may be overwritten
this.padding = WindowPadding.zero,
});
Copy the code
Although the official comment states that this is an immutable view configuration, we can modify the source code by compiling the source code. The compilation process can be described in the build Flutter Engine source code compilation environment. We add set code to FlutterWindow to override the ViewConfiguratiion value
/// provide a method to change devicePixelRatio of the window
void setViewConfiguration(ViewConfiguration viewConfiguration) {
assert(platformDispatcher._viewConfigurations.containsKey(_windowId));
platformDispatcher._viewConfigurations[_windowId] = viewConfiguration;
}
Copy the code
Then called when App launch window. SetViewConfiguration method, update devicePixelRatio values.
The code is as follows (for example, the width size of the design drawing is 375) :
@override
Widget build(BuildContext context2) {
/// 375 is the number of your design size
final modifiedViewConfiguration = window.viewConfiguration.copyWith(
devicePixelRatio: window.physicalSize.width/375);
window.setViewConfigureation(modifiedViewConfiguration);
return MaterialApp(
home: MyApp()
);
}
Copy the code
After the devicePixelRatio was successfully replaced, we found that the UI worked as expected. However, this will bring maintenance problems after our SDK upgrade, so is there a solution that does not need to worry about the maintenance of THE SDK version and can meet our needs?
Solution 2: Modify from the application layer
Let’s look at the startup process of the Flutter APP:
- Flutter start
voidrunApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() .. scheduleAttachRootWidget(app) .. scheduleWarmUpFrame(); }Copy the code
- At startup, we initialize the WidgetsFlutterBinding.
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance!;
}
}
Copy the code
WidgetsFlutterBinding inherits from BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding and WidgetsBinding 7 mixins. RendererBinding: a link between the render tree and the Flutter engine that holds the renderView, the root node of the render tree
Initialization code for RendererBinding:
@override
void initInstances() {
super.initInstances();
_instance = this;
_pipelineOwner = PipelineOwner(
onNeedVisualUpdate: ensureVisualUpdate,
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
);
window. onMetricsChanged = handleMetricsChanged .. onTextScaleFactorChanged = handleTextScaleFactorChanged .. onPlatformBrightnessChanged = handlePlatformBrightnessChanged .. onSemanticsEnabledChanged = _handleSemanticsEnabledChanged .. onSemanticsAction = _handleSemanticsAction; initRenderView(); _handleSemanticsEnabledChanged();assert(renderView ! =null);
addPersistentFrameCallback(_handlePersistentFrameCallback);
initMouseTracker();
if(kIsWeb) { addPostFrameCallback(_handleWebFirstFrame); }}Copy the code
In the handleMetricsChanged method, you can see how to get the Configuration value of the renderView.
/// Called when the system metrics change.
///
/// See [dart:ui.PlatformDispatcher.onMetricsChanged].
@protected
void handleMetricsChanged() {
assert(renderView ! =null);
renderView.configuration = createViewConfiguration();
scheduleForcedFrame();
}
Copy the code
Now the idea is obvious: override createViewConfiguration. Extend a subclass of WidgetsFlutterBinding, override createViewConfiguration in that subclass, and create a new runApp method to start our APP.
Custom WidgetsFlutterBinding subclass (for example, design width size 375) :
class MyWidgetsFlutterBinding extends WidgetsFlutterBinding{ @override ui.ViewConfiguration createViewConfiguration() { return ui.ViewConfiguration( devicePixelRatio: ui.window.physicalSize.width / 375, ); }}Copy the code
Then we’ll create a new runMyApp method to implement our APP’s call to MyWidgetsFlutterBinding:
void runMyApp(Widget app) { MyWidgetsFlutterBinding.ensureInitialized() .. scheduleAttachRootWidget(app) .. scheduleWarmUpFrame(); } void main() { runMyApp(MyApp()); }Copy the code
After the change, the test found that the DPR was successfully changed and the UI effect met our requirements.
Problems and modifications
When we put it into practice in the project, we found that both Plan 1 and Plan 2 would cause new problems:
- The screen size obtained by MediaQuery does not match.
When we use mediaQuery.of (context).size to get the screen size, mediaQuery.of (context) actually returns a MediaQueryData type.
The main properties of MediaQueryData are as follows
Const MediaQueryData({this.size = sie.zero, this.devicepixelRatio = 1.0,.. })Copy the code
It is found that the devicePixelRatio property is also useful here, so we can also change the value of MediaQueryData at the root of the MaterialApp to make the Size meet our requirements. The modified code is as follows (taking the width and size of the design drawing as 375 for example) :
@override Widget build(BuildContext ctx) { return MaterialApp( builder: (context, widget) { return MediaQuery( child: widget, data: MediaQuery.of(context).copyWith( size: Size(375, window.physicalSize.height / (window.physicalSize.width / 375)), devicePixelRatio: Window. PhysicalSize. Width / 375 / / / set text size does not change with system setup textScaleFactor: 1.0)); }, home: Home() ); }Copy the code
- An error occurred in the Widget click event area.
If we look at the code for WidgetsFlutterBinding, we see that the mixin class he mixed in has a GestureBinding related to gestures.
The GestureBinding initialization code is as follows:
@override
void initInstances() {
super.initInstances();
_instance = this;
ui.window.onPointerDataPacket = _handlePointerDataPacket;
}
Copy the code
The code is very succinct, where onPointerDataPacket is the system defined callback function:
/// Signature for [PlatformDispatcher.onPointerDataPacket].
typedef PointerDataPacketCallback = void Function(PointerDataPacket packet);
Copy the code
So the function of this code should be to get the UI. Window to the PointerDataPacket method and point to the _handlePointerDataPacket method of the GestureBinding.
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
// We convert pointer data to logical pixels so that e.g. the touch slop can be
// defined in a device-independent manner.
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, ui.window.devicePixelRatio));
if(! locked) _flushPointerEventQueue(); }Copy the code
As you can see, the devicePixelRatio property of window is also used here, so we can also change the value of window onPointerDataPacket in our subclass. The complete code of the WidgetsFlutterBinding subclass after the change (for example, with a design width of 375) :
import 'dart:collection';
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// Custom WidgetsFlutterBinding subclass
class MyWidgetsFlutterBinding extends WidgetsFlutterBinding {
final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
/// Width dimension of design drawing
final int designWidth = 375;static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null) MyWidgetsFlutterBinding();
return WidgetsBinding.instance;
}
@override
void initInstances() {
super.initInstances();
window.onPointerDataPacket = _handlePointerDataPacket;
}
@override
ViewConfiguration createViewConfiguration() {
return ViewConfiguration(
size: Size(
designWidth, window.physicalSize.width / designWidth * window.physicalSize.height),
devicePixelRatio: window.physicalSize.width / designWidth,
);
}
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.
_pendingPointerEvents.addAll(PointerEventConverter.expand(
packet.data, window.physicalSize.width / designWidth));
if(! locked) _flushPointerEventQueue(); }void _flushPointerEventQueue() {
assert(! locked);while(_pendingPointerEvents.isNotEmpty) handlePointerEvent(_pendingPointerEvents.removeFirst()); }}Copy the code
Dart calls the complete code in main.dart (for example, with a design width of 375) :
voidrunMyApp(Widget app) { MyWidgetsFlutterBinding.ensureInitialized() .. scheduleAttachRootWidget(app) .. scheduleWarmUpFrame(); }void main() {
runMyApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext ctx) {
return MaterialApp(
builder: (context, widget) {
return MediaQuery(
child: widget,
data: MediaQuery.of(context).copyWith(
size: Size(375.window.physicalSize.height / (window.physicalSize.width / 375)),
devicePixelRatio: window.physicalSize.width / 375./// The text size does not change with system Settings
textScaleFactor: 1.0)); }, home: Home() ); }}Copy the code
The code changes were relatively minor, involving almost no business code changes, no changes to the SDK layer, and no code invasions.
conclusion
Although there may be new problems in the scheme, it is still the simplest and reasonable scheme at present. Later, it is necessary to further study the source code and call process under FlutterWindow, find a reasonable entry point, and try to find a better adaptation scheme to make adaptation more leisurely and elegant.
The resources
- Flutter for Android developers
- The Flutter screen ADAPTS to the font size
- Build the Flutter Engine source code compilation environment
- A very low cost Android screen adaptation
- In-depth understanding of Flutter engine startup
Recommended reading
Chapter 2 of the JVM series – Class files to virtual Machines
Dapr Combat (part 1)
Dapr Combat part ii
DS version control core principles revealed
DS 2.0 era API operation posture
, recruiting
Zhengcaiyun Technology team (Zero) is a passionate, creative and executive team based in picturesque Hangzhou. The team has more than 300 r&d partners, including “old” soldiers from Alibaba, Huawei and NetEase, as well as newcomers from Zhejiang University, University of Science and Technology of China, Hangzhou Electric And other universities. Team in the day-to-day business development, but also in cloud native, chain blocks, artificial intelligence, low code platform system, middleware, data, material, engineering platform, the performance experience, visualization technology areas such as exploration and practice, to promote and fell to the ground a series of internal technical products, continue to explore new frontiers of technology. In addition, the team is involved in community building, Currently, There are Google Flutter, SciKit-Learn, Apache Dubbo, Apache Rocketmq, Apache Pulsar, CNCF Dapr, Apache DolphinScheduler, and Alibaba Seata and many other contributors to the excellent open source community. If you want to change something that’s been bothering you, want to start bothering you. If you want to change, you’ve been told you need more ideas, but you don’t have a solution. If you want change, you have the power to make it happen, but you don’t need it. If you want to change what you want to accomplish, you need a team to support you, but you don’t have the position to lead people. If you want to change the original savvy is good, but there is always a layer of fuzzy window…… If you believe in the power of believing, believing that ordinary people can achieve extraordinary things, believing that you can meet a better version of yourself. If you want to be a part of the process of growing a technology team with deep business understanding, sound technology systems, technology value creation, and impact spillover as your business takes off, I think we should talk. Any time, waiting for you to write something and send it to [email protected]
Wechat official account
The article is published synchronously, the public number of political cloud technology team, welcome to pay attention to