服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

FlutterComponent最佳实践之Widget尺寸

日期: 来源:群英传收集编辑:徐宜生

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


在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 欢迎大家访问



往期推荐


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

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

相关阅读

  • 【滋味】春天,尝尝这道用枸杞头制作的早餐

  • 枸杞头就是枸杞芽,即枸杞最上端的嫩叶。初春的枸杞头略带苦味,但很是爽口,后味微甜,加入普通的食材中,就能成就一道营养、精致的菜肴!今天,就给大家介绍一道简单又好吃的早餐——枸
  • 在线可玩的 ChatGPT

  • 这段时间发布了很多AI产品,包括GPT4,MidjourneyV5 ,文心一言,Microsoft 365 Copilot,ai产品要百花齐放了,这里就分享几个基于chatgpt的网站,在线可玩。ChatGPT prompts这个网站收集
  • 【该劝退了】国内音视频开发的前景怎么样?

  • 作者:cfwang链接:https://www.zhihu.com/question/464940771/answer/2939864399在知乎上看到的这个问题 国内音视频开发的前景怎么样 ?,除了这位 cfwang 老哥是在认真回答,其他全
  • 今年面试好激烈!

  • 金三银四过去一半,市场火热,但是大家就业压力却没有缓解多少。很多粉丝后台留言,Java程序员面临的竞争太激烈了……我自己也有实感,多年身处一线互联网公司,虽没有直面过求职跳槽
  • 正式宣布:与台湾“断交”!

  • 当地时间3月25日,洪都拉斯外交部发表声明,正式宣布与中国台湾断绝“外交关系”。声明说,世界上只有一个中国,中华人民共和国是代表中国的唯一合法政府,台湾是中国领土不可分割的
  • 利用TCP 打洞实现公网访问

  • 本文我们简单的说说利用Python进行Nat打洞,实现内网端口映射。无需公网IP,无需公网服务器。有手即可!克隆项目git clone https://github.com/MikeWang000000/Natter.git#检查网

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • FlutterComponent最佳实践之Widget尺寸

  • 点击上方蓝字关注我,知识会给你力量在Flutter和在Native中,对一个Widget的尺寸测量,一直都是一个非常麻烦的事情,大部分时间,我们都是按照约束和具体的尺寸来进行布局,但有些时候,
  • 【提示】北外滩历史风貌项目弘安里正式亮相!

  • 虹口区新闻办介绍,3月25日,历史风貌项目弘安里正式对外亮相。该项目位于虹口17街坊,是虹口区旧改新机制首批试点地块之一,也是上海市第二批历史风貌保护街坊。弘安里属于石库门
  • 【滋味】春天,尝尝这道用枸杞头制作的早餐

  • 枸杞头就是枸杞芽,即枸杞最上端的嫩叶。初春的枸杞头略带苦味,但很是爽口,后味微甜,加入普通的食材中,就能成就一道营养、精致的菜肴!今天,就给大家介绍一道简单又好吃的早餐——枸
  • 在线可玩的 ChatGPT

  • 这段时间发布了很多AI产品,包括GPT4,MidjourneyV5 ,文心一言,Microsoft 365 Copilot,ai产品要百花齐放了,这里就分享几个基于chatgpt的网站,在线可玩。ChatGPT prompts这个网站收集