05_Flutter屏幕适配

05_Flutter屏幕适配

一.屏幕适配方案

通过指定基准屏宽度,进行适配,基准屏宽度取决于设计图的基准宽度,以iphone 14 pro max为例,

devicePixelRatio = 物理宽度 / 逻辑宽度(基准宽度)

iphone 14 pro max的物理尺寸宽度为1290,基准屏尺寸375,也就是逻辑尺寸,因此可以得到像素比devicePixelRatio为3.44。

也就是说1个逻辑像素 = 3.4个物理像素。这样就把多样化的物理尺寸宽度都统一成了375的逻辑像素。搭建界面的时候以375的逻辑宽度去搭建即可。

二.确定新的逻辑尺寸和像素比

竖屏状态下,Flutter默认的逻辑像素的计算规则是:

逻辑宽度 = 物理宽度 / 像素比

Flutter默认的像素比使用的是像素密度,就是我们平时常说的一倍屏、二倍屏、三倍屏。三倍屏的像素密度是3.0…

因此,我们需要修改默认的逻辑尺寸,将逻辑宽度统一成375。首先确定新的像素比devicePixelRatio。

新的像素比 = 物理宽度 / 375

从而确定新的逻辑尺寸为:

新的逻辑尺寸 = 默认的逻辑尺寸 / 新的像素比
三.默认的逻辑尺寸和像素比的确定过程

那么接下来的问题就是怎么将Flutter默认的逻辑尺寸和像素比修改为新的逻辑尺寸和像素比了,查看源码可以知道,runApp时首先会示例化一个WidgetsFlutterBinding的单例对象。

void runApp(Widget app) {
  final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
  assert(binding.debugCheckZone('runApp'));
  binding
    ..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
    ..scheduleWarmUpFrame();
}

也就是通过WidgetsFlutterBinding.ensureInitialized()来实例话这个静态单例。后续我们可以通过WidgetsBinding.instance拿到这个对象:

class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding._instance == null) {
      WidgetsFlutterBinding();
    }
    return WidgetsBinding.instance;
  }
}

而WidgetsFlutterBinding是继承了BindingBase的,因此WidgetsFlutterBinding示例化的同时,会调用BindingBase的构造方法,接着看BindingBase的构造方法:

BindingBase() {
  ...
  initInstances();
  ...
}

BindingBase的构造方法中,会调用initInstances(),initInstances()调用的同时,会调用RendererBinding的initInstances()方法,接着看RendererBinding的initInstances方法:


void initInstances() {
  super.initInstances();
  ...
  initRenderView();
  ...
}

RendererBinding的initInstances方法中,会调用initRenderView方法,接着看RendererBinding的initRenderView方法:

void initRenderView() {
	...
	renderView = RenderView(configuration: createViewConfiguration(), view: platformDispatcher.implicitView!);
	...
}

RendererBinding的initRenderView方法会创建一个RenderView对象,同时RendererBinding为renderView提供了set方法,这就意味着我们可以在外部重新设置renderView的值,创建RenderView的时候会传入ViewConfiguration,和一个FlutterView对象,通过这个FlutterView对象,我们可以获取到设备的物理尺寸以及像素密度,以Android为例,这个FlutterView对象就对应着Acrivity的DecorView。接着看createViewConfiguration方法:

ViewConfiguration createViewConfiguration() {
  final FlutterView view = platformDispatcher.implicitView!;
  final double devicePixelRatio = view.devicePixelRatio;
  return ViewConfiguration(
    size: view.physicalSize / devicePixelRatio,
    devicePixelRatio: devicePixelRatio,
  );
}

可以看到,ViewConfiguration对象的创建过程,会传递默认的像素比,以及确定默认的逻辑尺寸,这里就是我们第一个需要修改的地方,那么怎么修改,毫无疑问,需要把RendererBinding的renderView的值替换成我们自己创建的,这样我们就可以根据自己计算的逻辑尺寸和像素比去创建ViewConfiguration了。

四.MediaQuery的确定过程

回到runApp的源码:

void runApp(Widget app) {
  ...
  binding
    ..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
  ...
}

WidgetsFlutterBinding示例化完成后,会通过WidgetsFlutterBinding的wrapWithDefaultView方法包装MaterialApp。接着看WidgetsFlutterBinding的wrapWithDefaultView方法:

Widget wrapWithDefaultView(Widget rootWidget) {
  return View(
    view: platformDispatcher.implicitView!,
    child: rootWidget,
  );
}

可以看到,这里使用了View包装MaterialApp,那么接着看View的build方法:


Widget build(BuildContext context) {
  return _ViewScope(
    view: view,
    child: MediaQuery.fromView(
    	view: view,
    	child: child,
    ),
  );
}

MediaQuery的build过程:


Widget build(BuildContext context) {
  MediaQueryData effectiveData = _data!;
  if (!kReleaseMode && _parentData == null && effectiveData.platformBrightness != debugBrightnessOverride) {
    effectiveData = effectiveData.copyWith(platformBrightness: debugBrightnessOverride);
  }
  return MediaQuery(
    data: effectiveData,
    child: widget.child,
  );
}

看到这里,就可以知道,可以通过在MaterialApp外部包裹一个MediaQuery组件,同时传入新的逻辑尺寸和像素比。这是第二个需要修改的地方。

五.修改默认的逻辑尺寸和像素比

这里就直接上代码了:

class ScreenAdapterBinding extends StatelessWidget {

  final double baseScreenWidth;
  final Widget child;

  const ScreenAdapterBinding({
    super.key,
    this.baseScreenWidth = 375,
    required this.child
  });

  
  Widget build(BuildContext context) {
    return _ScreenAdapterScope(
      baseScreenWidth: baseScreenWidth,
      view: View.of(context),
      child: child,
    );
  }

}

class _ScreenAdapterScope extends StatefulWidget {

  final double baseScreenWidth;
  final FlutterView view;
  final Widget child;


  const _ScreenAdapterScope({
    this.baseScreenWidth = 375,
    required this.view,
    required this.child,
  });

  
  State<StatefulWidget> createState() => _ScreenAdapterScopeState();

}

class _ScreenAdapterScopeState extends State<_ScreenAdapterScope> with WidgetsBindingObserver {

  MediaQueryData? _parentData;
  MediaQueryData? _data;

  get _devicePixelRatio {
    final FlutterView view = widget.view;
    //物理尺寸
    final Size physicalSize = view.physicalSize;
    //新的像素密度
    double baseWidth = widget.baseScreenWidth;

    double targetPixelRatio = physicalSize.width / baseWidth;

    if(targetPixelRatio == null || targetPixelRatio <= 0) {
      targetPixelRatio = view.devicePixelRatio;
    }

    return targetPixelRatio;
  }

  Size get _size {
    final FlutterView view = widget.view;
    return view.physicalSize / _devicePixelRatio;
  }

  void _updateParentData() {
    _parentData = MediaQuery.maybeOf(context);
    _data = null; // _updateData must be called again after changing parent data.
  }

  void _updateData() {
    WidgetsBinding.instance.renderView.configuration = ViewConfiguration(
        size: _size,
        devicePixelRatio: _devicePixelRatio
    );

    final MediaQueryData newData = MediaQueryData.fromView(widget.view, platformData: _parentData).copyWith(
      size: _size,
      devicePixelRatio: _devicePixelRatio,
    );

    if (newData != _data) {
      setState(() {
        _data = newData;
      });
    }
  }

  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    _updateParentData();
    _updateData();
    assert(_data != null);
  }

  
  void didUpdateWidget(_ScreenAdapterScope oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (_data == null || oldWidget.view != widget.view) {
      _updateParentData();
      _updateData();
    }
    assert(_data != null);
  }

  
  void didChangeAccessibilityFeatures() {
    if (_parentData == null) {
      _updateData();
    }
  }

  
  void didChangeMetrics() {
    _updateData();
  }

  
  void didChangeTextScaleFactor() {
    if (_parentData == null) {
      _updateData();
    }
  }

  
  void didChangePlatformBrightness() {
    if (_parentData == null) {
      _updateData();
    }
  }

  
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    MediaQueryData effectiveData = _data!;
    if (!kReleaseMode && _parentData == null && effectiveData.platformBrightness != debugBrightnessOverride) {
      effectiveData = effectiveData.copyWith(platformBrightness: debugBrightnessOverride);
    }
    return MediaQuery(
      data: effectiveData,
      child: widget.child,
    );
  }

}

使用的时候,只需要将MaterialApp使用ScreenAdapterBinding包裹即可:

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return ScreenAdapterBinding(
        baseScreenWidth: 375,
        child:
        MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        )
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        alignment: Alignment.center,
        color: Colors.white,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              width: 375,
              height: 100,
              color: Colors.red,
            ),
            Container(
              width: 370,
              height: 100,
              color: Colors.red,
              margin: const EdgeInsets.only(top: 20),
            )
          ],
        ),
      ),//
      floatingActionButton: FloatingActionButton(
        onPressed: () {

        },
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

在这里插入图片描述

可以看到第一个Container,宽度为375,刚好能够铺满屏幕,第二个Container,宽度为370,没有铺满屏幕,说明默认的逻辑尺寸和像素比已经被修改为了我们自己确定的结果。但是有个问题,那就是点击事件失效了。

六.修复点击事件

这里就不绕弯了,首先看GestureBinding的initInstances方法


void initInstances() {
  ...
  platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;
}

接着看GestureBinding的_handlePointerDataPacket方法:

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.
    try {
      _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, platformDispatcher.implicitView!.devicePixelRatio));
      if (!locked) {
        _flushPointerEventQueue();
      }
    } catch (error, stack) {
      FlutterError.reportError(FlutterErrorDetails(
        exception: error,
        stack: stack,
        library: 'gestures library',
        context: ErrorDescription('while handling a pointer data packet'),
      ));
    }
  }

可以看到,这里在计算点击的触摸坐标时,还使用的是默认的像素比去计算的,因此,这里需要把默认的像素密度替换。直接上代码:

class _ScreenAdapterScopeState extends State<_ScreenAdapterScope> with WidgetsBindingObserver {

  ...

  final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();

  void _handlePointerDataPacket(PointerDataPacket packet) {
    try {
      _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, _devicePixelRatio));
      if (!WidgetsBinding.instance.locked) {
        _flushPointerEventQueue();
      }
    } catch (error, stack) {
      FlutterError.reportError(FlutterErrorDetails(
        exception: error,
        stack: stack,
        library: 'gestures library',
        context: ErrorDescription('while handling a pointer data packet'),
      ));
    }
  }

  void _flushPointerEventQueue() {
    assert(!WidgetsBinding.instance.locked);

    while (_pendingPointerEvents.isNotEmpty) {
      WidgetsBinding.instance.handlePointerEvent(_pendingPointerEvents.removeFirst());
    }
  }

  void _updateParentData() {
    _parentData = MediaQuery.maybeOf(context);
    _data = null; // _updateData must be called again after changing parent data.
  }

  void _updateData() {
    WidgetsBinding.instance.renderView.configuration = ViewConfiguration(
        size: _size,
        devicePixelRatio: _devicePixelRatio
    );

    final MediaQueryData newData = MediaQueryData.fromView(widget.view, platformData: _parentData).copyWith(
      size: _size,
      devicePixelRatio: _devicePixelRatio,
    );

    WidgetsBinding.instance.platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;

    if (newData != _data) {
      setState(() {
        _data = newData;
      });
    }
  }

  ...

}

在这里插入图片描述

完美搞定。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/564872.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

创新入门|解锁您的潜在市场:探秘付费点击广告(PPC)的秘密武器

在我们的营销领域&#xff0c;按点击付费 &#xff08;PPC&#xff09; 广告是增加流量、提高知名度并最终将点击转化为客户的基石策略。这种有针对性的广告模式&#xff0c;即企业只在点击广告时付费&#xff0c;彻底改变了公司投资在线推广的方式。尽管它看起来很简单&#x…

手写Promise实现

手写Promise实现 一、前言二、代码三、测试四、测试结果 一、前言 阅读参考资料&#xff0c;本文整理出使用 构造函数 手撕出 Promise 的方法&#xff0c;在整理过程中不断添加注解以示思路。有错请指出哟&#xff0c;一起进步&#xff01;&#xff01;&#xff01;class 实现 …

2024接口自动化测试入门基础知识【建议收藏】

接口自动化测试是指通过编写测试脚本和使用相关工具&#xff0c;对软件系统的接口进行自动化测试的过程。 今天本文从4个方面来介绍接口自动化测试入门基础知识 一、接口自动化测试是什么&#xff1f; 二、接口自动化测试流程&#xff1f; 三、接口自动化测试核心知识点有那些…

开始Java之旅

1.Java语言 java是一门优秀的程序设计语言&#xff0c;并且是一种半编译型&#xff0c;半解释型语言。 Java 语言源于 1991 年 4 月&#xff0c;Sun 公司 James Gosling博士 领导的绿色计划(Green Project) 开始启动&#xff0c;此计划最初的目标是开发一种能够在各种消费性电…

Threejs绘制传送带

接下来会做一个MES场景下的数字孪生&#xff0c;所以开始做车间相关的模型&#xff0c;不过还是尽量少用建模&#xff0c;纯代码实现&#xff0c;因为一方面可以动态使用&#xff0c;可以调节长度和宽度等&#xff0c; 下面这节就做一个简单的传送带&#xff0c;这是所有车间都…

学之思考试系统环境启动QA

学之思考试系统环境启动Q&A 目录 学之思考试系统环境启动Q&A后台代码启动失败:前台代码启动失败常见解决方式参考资料后台代码启动失败: 后端代码启动不成功,不能够自动导入maven,配置依赖; 使用idea打开到:\xzs-master\xzs-mysql-master\source\xzs这个路径下;…

函数的创建和调用及删除

Oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 函数和存储过程非常类似&#xff0c;也是可以存储在 Oracle 数据库中的 PL/SQL代码块&#xff0c;但是有返回值。 可以把经常使用的功能定义为一个函数&#xff0c;就像系统…

使用Flask部署ppocr模型_3

PaddleOCR环境搭建、模型训练、推理、部署全流程&#xff08;Ubuntu系统&#xff09;_1_paddle 多进程推理-CSDN博客 PP-Structure 文档分析-CSDN博客 Pycharm的Terminal进入创建好的虚拟环境 有时候Pycharm的terminal中显示的是硬盘中的项目路径&#xff0c;但没有进入创建好…

Python 开发实现登陆和注册模块

Python 开发实现登陆和注册模块 一、案例介绍 本例设计一个用户登录和注册模块&#xff0c;使用Tkinter框架构建界面&#xff0c;主要用到画布、文本框、按钮等组件。涉及知识点&#xff1a;Python Tkinter界面编程、pickle数据存储。本例实现了基本的用户登录和注册互动界面…

ic基础|时序篇:握手协议valid和ready的时序优化

大家好&#xff0c;我是数字小熊饼干&#xff0c;一个练习时长两年半的ic打工人。我在两年前通过自学跨行社招加入了IC行业。现在我打算将这两年的工作经验和当初面试时最常问的一些问题进行总结&#xff0c;并通过汇总成文章的形式进行输出&#xff0c;相信无论你是在职的还是…

网络安全的守护者:防火墙的五个主要功能解析

防火墙是一种网络安全设备&#xff0c;用于保护计算机网络免受未经授权的访问、攻击和恶意软件的侵害。它通过监控、过滤和控制网络流量&#xff0c;实施安全策略&#xff0c;防止不安全的数据包进入或离开受保护的网络。 防火墙的五个主要功能&#xff1a; 1. 访问控制&#…

Web入门-Tomecat

黑马程序员JavaWeb开发教程 文章目录 一、简介1、Web服务器2、Tomcat 二、基本使用三、入门程序解析 一、简介 1、Web服务器 对HTTP协议操作进行封装&#xff0c;简化web程序开发部署Web项目&#xff0c;对外提供网上信息浏览服务 2、Tomcat 概念&#xff1a;Tomcat是Apach…

(回溯)记忆化搜索和dp

动态规划的核心就是 状态的定义和状态的转移 灵神 的 回溯改递归思路 首先很多动态规划问题都可以采用 回溯 的思想 回溯主要思想就是把 一个大问题分解成小问题 比如 采用子集类回溯问题中的核心思想-> 选或不选 或者 选哪个 记忆化搜索之后 我们可以发现 每个新节点依…

深度图转点云

一、理论分析 二、其他分析 1、相机内参 相机内参主要是四个参数fx,fy,u0,v0。要明白相机内参就是相机内部参数&#xff0c;是参考像素坐标系而言&#xff0c;有了这个前提&#xff0c;这四个参数也就很好理解了。 &#xff08;1&#xff09;首先&#xff0c;。其中F是相机的…

sora related

官方https://openai.com/research/video-generation-models-as-world-simulators 概述&#xff1a; sora可以生成变长的、不同分辨率的最长可到1分钟的视频&#xff1b;整体流程是 v i d e o c o m p r e s s i o n n e r w o r k ( v i d e o → l a t e n t ) p a t c h i…

HarmonyOS ArkUI实战开发-NAPI数据类型

在前两篇文章里笔者简单介绍了 NAPI 工程结构以及生成的 cpp 源码部分&#xff0c;其中 JS 应用层传递过来的数据被封装在了 napi_value 中&#xff0c;使用前先要转换成对应的 C/C 数据类型&#xff0c;C/C 端的数据也要转换成 napi_value 数据类型传递给 JS 应用层&#xff0…

大模型改变了NLP的游戏规则了吗

NLP已经死了吗&#xff1f; 自从 ChatGPT 横空出世以来&#xff0c;自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09; 研究领域就出现了一种消极的声音&#xff0c;认为大模型技术导致 NLP “死了”。在某乎上就有一条热门问答&#xff0c;大…

mac上VMware fusion net模式无法正常使用的问题

更新时间&#xff1a;2024年04月22日21:39:04 1. 问题 环境&#xff1a; intel芯片的macbook pro VMware fusion 13.5.1 无法将“Ethernet0”连接到虚拟网络“/dev/vmnet8”。在这里显示这个之后&#xff0c;应该是vmnet8的网段发生了冲突&#xff0c;所以导致无法正常使用…

一篇文章带您了解操作系统的体系结构

操作系统的体系结构有哪些&#xff1f; 我们可以利用时钟中断实现计时功能。 原语是一种特殊的程序&#xff0c;具有原子性。也就是说&#xff0c;这段程序的运行必须一气呵成&#xff0c;不能中断。 内核是操作系统最基本&#xff0c;最核心的部分。 实现操作系统内核功能的…

Opencv Python图像处理笔记一:图像、窗口基本操作

文章目录 前言一、输入输出1.1 图片读取显示保存1.2 视频读取保存1.3 文件读取保存 二、GUI2.1 窗口2.2 轨迹条2.3 画图2.4 鼠标回调 三、图像入门操作3.1 颜色空间转化3.2 通道分离合并3.3 添加边框3.4 算数操作 四、二值化4.1 普通4.2 自适应4.3 Otsu 参考 前言 随着人工智能…