点击上方蓝字关注我,知识会给你力量
在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<MeasurableWidget> {
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 MeasurableMixin<T extends StatefulWidget> on State<T> {
@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<StatefulWidget> createState() {
return _MeasurableWidgetState();
}
}
class _MeasurableWidgetState extends State<MeasurableWidget> with MeasurableMixin<MeasurableWidget> {
@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<WidgetMeasuredNotification>(
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 欢迎大家访问
往期推荐
更文不易,点个“三连”支持一下