网易首页 > 网易号 > 正文 申请入驻

FlutterComponent最佳实践之Widget尺寸

0
分享至

点击上方蓝字关注我,知识会给你力量

在Flutter和在Native中,对一个Widget的尺寸测量,一直都是一个非常麻烦的事情,大部分时间,我们都是按照约束和具体的尺寸来进行布局,但有些时候,我们不得不拿到动态的Widget尺寸来实现自己的一些布局策略。通常来说,我们会有三方面的需求。

  • 测量自己的尺寸

  • 测量Parent的尺寸

  • 测量Child的尺寸

测量自己的尺寸

要获取你自身的Widget尺寸,其实只需要通过RenderBox即可获取。

Size s = (context.findRenderObject() as RenderBox).size;
// 空安全写法
Size s = (context.findRenderObject() as RenderBox?)?.size ?? Size.zero;

❝ 不过要注意的是,findRenderObject不能写在build方法中,因为这个时候,renderobject还未挂载。 ❞

测量Parent尺寸

对于Parent来说,我们可以通过LayoutBuilder来快速获得它的约束范围,从而获取Parent的尺寸,代码如下。

return LayoutBuilder(
builder: (context , constraints ) {
print('-----$constraints');
},

这是一个很方便的功能,因为你可以根据当前宽度和比例来调整当前Widget的尺寸,从而更加符合约束的视觉限制。

测量Child尺寸

测量Child的尺寸要比上面两种要复杂一点,我们一般还是会通过findRenderObject来获取尺寸信息,然后将其通过回调传递给当前Widget。

class MeasurableWidget extends StatefulWidget {
const MeasurableWidget({
Key? key,
required this.child,
required this.onSized,
}) : super(key: key);
final Widget child;
final void Function(Size size) onSized;

@override
_MeasurableWidgetState createState() => _MeasurableWidgetState();
}

class _MeasurableWidgetState extends State {
bool _hasMeasured = false;

@override
Widget build(BuildContext context) {
Size size = (context.findRenderObject() as RenderBox?)?.size ?? Size.zero;
if (size != Size.zero) {
widget.onSized.call(size);
} else if (!_hasMeasured) {
// Need to build twice in order to get size
scheduleMicrotask(() => setState(() => _hasMeasured = true));
}
return widget.child;
}
}

我们创建一个MeasurableWidget,用来测量Child的尺寸,并传入回调来获取尺寸,使用代码如下。

MeasurableWidget(
onSized: (Size size) {
print('====$size');
},
child: const Text(
'xxxx',
),

这个方法其实遇到了和「测量自身」一样的问题,那就是build的时候,RenderObject未挂载,所以这里需要Render两次才能获取最终的尺寸,这样其实并不是很优雅,虽然大部分时候,局部Context的刷新并不太耗性能,但是还是应该尽可能的减少刷新的次数。

那么获取Child的尺寸有什么用呢?通过获取Child的尺寸,我们可以根据尺寸来做一些偏移,例如下面的示例。

Size _widgetSize = Size.zero;
Widget build(BuildContext context){
Offset o = Offset(_widgetSize.size.width/2, _widgetSize.size.height/2);
return Transform.translate(
offset: o,
child: MeasurableWidget(child: ..., onSized: _handleWidgetSized);

void _handleWidgetSized(Size value) => setState(()=>_widgetSize = value);

优化

那么我们是否有办法来避免这个「两次刷新」呢?答案是肯定的,我们不能一次性获取尺寸的原因,实际上就是RenderObject没挂载好,所以,我们可以自定义一个RenderObject,给它设置回调来获取尺寸。

首先,我们先定义一个RenderProxyBox,并不需要修改什么逻辑,只要在其performLayout方法中,通过WidgetsBinding.instance.addPostFrameCallback来增加一个回调监听即可。

class MeasureSizeRenderObject extends RenderProxyBox {
MeasureSizeRenderObject(this.onChange);

void Function(Size size) onChange;

Size _prevSize = Size.zero;

@override
void performLayout() {
super.performLayout();
Size newSize = child?.size ?? Size.zero;
if (_prevSize == newSize) return;
_prevSize = newSize;
WidgetsBinding.instance.addPostFrameCallback((_) => onChange(newSize));
}
}

接下来,再定义一个SingleChildRenderObjectWidget来承载它即可。

class MeasurableWidget extends SingleChildRenderObjectWidget {
const MeasurableWidget({
Key? key,
required this.onChange,
required Widget child,
}) : super(key: key, child: child);

final void Function(Size size) onChange;

@override
RenderObject createRenderObject(BuildContext context) => MeasureSizeRenderObject(onChange);
}

使用也很简单,使用MeasurableWidget包裹下就好了。

MeasurableWidget(
onChange: (Size size) {
print('====$size');
},
child: const Text(
'xxxx',
),

是不是有点意思,其核心原理还是通过WidgetsBinding.instance.addPostFrameCallback来获取尺寸回调的时机,但是封装了一层,就优雅了很多。

再优化

前面我们是通过自定义RenderProxyBox来处理addPostFrameCallback调用的时机问题,那么除了这种方式以为,还可以通过mixin来处理这个问题,代码如下所示。

mixin MeasurableMixinextends StatefulWidget> on State {
@override
BuildContext get context;

@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterRendering);
super.initState();
}

void _afterRendering(Duration timeStamp) {
RenderObject? renderObject = context.findRenderObject();
if (renderObject != null) {
Size size = renderObject.paintBounds.size;
var box = renderObject as RenderBox;
onSized(
Rect.fromLTWH(
box.localToGlobal(Offset.zero).dx,
box.localToGlobal(Offset.zero).dy,
size.width,
size.height,
),
);
} else {
onSized(Rect.zero);
}
}

void onSized(Rect rect);
}

typedef OnSized = void Function(Rect rect);

那么有了这个mixin之后,就可以很方便的封装一个Widget,来创建类似前面的回调。

class MeasurableWidget extends StatefulWidget {
final Widget child;

final OnSized onSized;

const MeasurableWidget({
Key? key,
required this.child,
required this.onSized,
}) : super(key: key);

@override
State createState() {
return _MeasurableWidgetState();
}
}

class _MeasurableWidgetState extends State with MeasurableMixin {
@override
Widget build(BuildContext context) => widget.child;

@override
void onSized(Rect rect) => widget.onSized(rect);
}

这样我们就可以很方便的使用它了。

MeasurableWidget(
onSized: (Rect rect) {
print('------$rect');
},
child: const Text(
'xxxx',
),

可以发现,其实我们解决问题的方法有很多,但殊途同归,有很多时候,我们都可以从不同角度去解决同一个问题,这样对我们不仅仅是技术的提高,也是认知的提高。

通过Key

前面我们在获取尺寸的时候,要么是在Build之后通过context获取,要么就是创建Custom RenderObject来增加监听,这些方法的本质,实际上都是通过WidgetsBinding.instance.addPostFrameCallback获取刷新时机,再通过findRenderObject来获取尺寸,所以,借助Key,我们可以在不自定义Custom RenderObject的前提下,获取尺寸的一般方法。

❝ 要注意的是,未渲染的Widget,通过GlobalKey获取的currentContext为null。 ❞
final GlobalKey globalKey = GlobalKey();
var showSize = 'show me Text!';

void getSizeWithContext() {
final containerWidth = globalKey.currentContext?.size?.width;
final containerHeight = globalKey.currentContext?.size?.height;
print('Context Container Width $containerWidth\n'
'Context Container Height $containerHeight');
}

void getSizeWithRenderBox() {
RenderBox? box = globalKey.currentContext?.findRenderObject() as RenderBox?;
final containerWidth = box?.size.width;
final containerHeight = box?.size.height;
print('Context Container Width $containerWidth\n'
'Context Container Height $containerHeight');
}

void getSizeWithPaintBounds() {
RenderObject? box = globalKey.currentContext?.findRenderObject();
print('PaintBounds Container Width ${box?.paintBounds.width}\n'
'PaintBounds Container Height ${box?.paintBounds.height}');
}

@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(getPositionWithPostFrameCallback);
super.initState();
}

getPositionWithPostFrameCallback(_) => getSizeWithRenderBox();

前两种方式,无非是通过GlobalKey来获取BuildContext和RenderBox,其本质是一样的。但是这些方法都只限制于获取Box模型中的尺寸,如果在Sliver结构中国,则只能通过其内部的容器Widget来间接获取其尺寸。

Size Notifications

在Flutter中,Notifications是向上冒泡的,如果你需要某些尺寸,并在多个层级的Widget上传递,Notifications就是一个最好的选择,你需要做的只是定义一些自定义Notifications。

首先,我们创建一个Notifications。

class WidgetMeasuredNotification extends Notification {
WidgetMeasuredNotification(this.size);

final Size size;
}

然后在获取到尺寸的地方通过dispatch将size分发出来,在需要监听的地方,使用NotificationListener来做监听即可。

NotificationListener(
onNotification: (notification) {
print('=====${notification.size}');
return true;
},
child: MeasurableWidget(
onSized: (Size size) {
WidgetMeasuredNotification(size).dispatch(context);
},
child: const Text(
'xxxxx',
),
),

这样就可以将Size在Widget Tree上传递了。这样的好处就是Widget和监听者之间没有太多的耦合,即使跨越多个层级,你依然可以获取这些通知,它的使用场景很多,例如在一些菜单动画中,你需要在MenuController和被选中的MenuButtons之间获取这种尺寸的处理。

MediaQuery.of(context)

MediaQuery.of(context)是我们经常访问的一个代码,用来获取到设备相关的一些尺寸信息,但是它的调用稍微复杂一点,比如。

MediaQuery.of(context).size.height

类似的还有很多,所以我们可以借助Dart的extension来对BuildContext进行拓展。

extension SizedContext on BuildContext {
/// Returns same as MediaQuery.of(context)
MediaQueryData get mq => MediaQuery.of(this);

/// Returns if Orientation is landscape
bool get isLandscape => mq.orientation == Orientation.landscape;

/// Returns same as MediaQuery.of(context).size
Size get sizePx => mq.size;

/// Returns same as MediaQuery.of(context).size.width
double get widthPx => sizePx.width;

/// Returns same as MediaQuery.of(context).height
double get heightPx => sizePx.height;

/// Returns diagonal screen pixels
double get diagonalPx {
final Size s = sizePx;
return sqrt((s.width * s.width) + (s.height * s.height));
}

/// Returns fraction (0-1) of screen width in pixels
double widthPct(double fraction) => fraction * widthPx;

/// Returns fraction (0-1) of screen height in pixels
double heightPct(double fraction) => fraction * heightPx;
}

这样在使用的时候,可以直接通过context来引用。

context.sizePx
context.mq.padding

等等。

❝ 要注意的是,MediaQuery.of(context).size.height在release mode下第一次获取的值可能是0,所以需要对这种情况进行下处理,避免出现0/0的问题。 ❞

向大家推荐下我的网站 https://www.yuque.com/xuyisheng 点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问



本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。

作者:徐宜生

更文不易,点个“三连”支持一下

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
真被张召忠说中了?掏空国库建造的2艘航母,如今彻底沦为废铁

真被张召忠说中了?掏空国库建造的2艘航母,如今彻底沦为废铁

高山非凡创作
2024-05-16 07:48:51
温州小伙在网吧猝死超24小时才被发现?老板:店员以为他在睡觉

温州小伙在网吧猝死超24小时才被发现?老板:店员以为他在睡觉

极目新闻
2024-06-11 21:10:30
太阳报:全队训练时仅肖穿绿色外套,是为提醒队友不要让他受伤

太阳报:全队训练时仅肖穿绿色外套,是为提醒队友不要让他受伤

懂球帝
2024-06-12 19:24:07
刚刚!俄罗斯遭遇屈辱叛变,盟友军援乌克兰,俄方已不抱任何期望

刚刚!俄罗斯遭遇屈辱叛变,盟友军援乌克兰,俄方已不抱任何期望

文雅笔墨
2024-06-12 12:13:07
苹果AI,苹果比AI更重要

苹果AI,苹果比AI更重要

蓝鲸财经
2024-06-12 09:23:24
4-1!1-0!亚洲最神秘球队逆袭,力压国足宿敌出线,剑指世界杯!

4-1!1-0!亚洲最神秘球队逆袭,力压国足宿敌出线,剑指世界杯!

绿茵舞着
2024-06-13 02:27:35
厉害了!57岁斯坦福博士参加2024山西高考,语文考到自己的课题

厉害了!57岁斯坦福博士参加2024山西高考,语文考到自己的课题

亿通电子游戏
2024-06-12 15:34:55
高材生陈恂敏:95年抢劫银行1500万,隐藏21年因同伙自首暴露

高材生陈恂敏:95年抢劫银行1500万,隐藏21年因同伙自首暴露

丹宝说文史
2023-12-16 19:10:37
打垮俄罗斯只是一方面,美援乌更深层原因曝光:与中国有关?

打垮俄罗斯只是一方面,美援乌更深层原因曝光:与中国有关?

文雅笔墨
2024-06-13 01:22:06
索斯盖特慌了!急召38岁老将入队,英格兰最大的坑终于暴露了!

索斯盖特慌了!急召38岁老将入队,英格兰最大的坑终于暴露了!

开心体育站
2024-06-12 19:08:58
匈牙利认怂?不参与北约关于乌克兰的任何决定,也不阻止这些决定

匈牙利认怂?不参与北约关于乌克兰的任何决定,也不阻止这些决定

山河路口
2024-06-12 20:56:07
若中美全面开打,中国在制裁封锁下,究竟能撑多久?

若中美全面开打,中国在制裁封锁下,究竟能撑多久?

兵国大事
2024-06-04 10:14:25
丁俊晖轰满分147分,打进2站排名赛决赛,中国一哥再冲世锦赛冠军

丁俊晖轰满分147分,打进2站排名赛决赛,中国一哥再冲世锦赛冠军

全能体育柳号
2024-06-12 07:05:09
美国:绝不谈判,也不妥协!混战已开始,中国迎来良机

美国:绝不谈判,也不妥协!混战已开始,中国迎来良机

星辰故事屋
2024-06-12 18:41:29
深圳某老牌地产公司3小时裁掉所有员工?前员工称“未办任何工作交接”,官方已下达整改通知书,办公现场已无对接人

深圳某老牌地产公司3小时裁掉所有员工?前员工称“未办任何工作交接”,官方已下达整改通知书,办公现场已无对接人

每日经济新闻
2024-06-12 22:29:10
10年前,两位“夺刀少年”因救人错过高考,拒绝保送后如今怎样了

10年前,两位“夺刀少年”因救人错过高考,拒绝保送后如今怎样了

文史达观
2024-06-09 06:45:02
六台记者:若葡萄牙欧洲杯夺冠,C罗就将夺得今年的金球奖

六台记者:若葡萄牙欧洲杯夺冠,C罗就将夺得今年的金球奖

懂球帝
2024-06-12 21:54:09
同为09届球员,为何霍勒迪可以续约1.45亿,而哈登要1亿都很难?

同为09届球员,为何霍勒迪可以续约1.45亿,而哈登要1亿都很难?

开心体育站
2024-06-12 20:44:59
解放军再围台,美军下狠心保卫赖清德,台海巡署对大陆渔民下死手

解放军再围台,美军下狠心保卫赖清德,台海巡署对大陆渔民下死手

葛剑生
2024-06-11 10:37:38
美国人又怒了,他们震怒的原因非常简单!NASA又一谎言被拆穿

美国人又怒了,他们震怒的原因非常简单!NASA又一谎言被拆穿

虎哥说航天
2024-06-11 22:05:02
2024-06-13 04:28:49
Android群英传
Android群英传
Android群英传
439文章数 921关注度
往期回顾 全部

科技要闻

谁是苹果AI的“中国合伙人”?

头条要闻

顶头上司落马3周后退休副省长被查 任内曾被环保问责

头条要闻

顶头上司落马3周后退休副省长被查 任内曾被环保问责

体育要闻

国足,别辜负这场奇迹!

娱乐要闻

黄一鸣再次录视频表态孩子是王思聪的

财经要闻

徽商银行的影子 借基金向地方城投放贷?

汽车要闻

理想汽车周销量突破1万辆 单周销量首超宝马奥迪

态度原创

游戏
教育
房产
旅游
数码

《中国式家长》重新上架Steam 更新至2.0.0.0版本

教育要闻

求最小值经典题目,分子变一变,均值不等式轻松求解

房产要闻

今日正式执行!海南下调房贷利率!暂不涉及存量贷款!

旅游要闻

日本“黑道大哥”现街道 警察保持随时监控

数码要闻

部分采用 N3B,英特尔 Arrow Lake-U/H/HX 移动处理器现身海关

无障碍浏览 进入关怀版