11

本文讲述flutter中获取元素的探索之旅,并总结获取元素大小的方法。

前言

Flutter的布局体系中,带有大小尺寸的元素并不多,比如SizedBox,ConstrainedBox,Padding等,通过精确设置元素大小来获取某个容器的大小这种方法无论在哪种布局体系中都是不大现实的。那么flutter怎么获取元素大小呢?

探索之旅:

和大小有关的类和方法、属性

在Flutter中,所有的元素都是Widget,那么通过Wiget能不能获得大小呢?看下Widget的属性和方法哪个和大小有关的:看了一遍源码之后结论是没有,但是Widget有个createElement方法返回了一个Element。

Element是什么?看下Element的注释:

An instantiation of a [Widget] at a particular location in the tree.

在“渲染”树中的实际位置的一个Widget实例,显然在渲染过程中,flutter实际使用的是Element,那么就必须要知道Element的大小。

这个是Element的定义,Element实现了BuildContext

abstract class Element extends DiagnosticableTree implements BuildContext {

注意到BuildContext的属性和方法中,有findRenderObject和size方法

abstract class BuildContext {

  RenderObject findRenderObject();
...
  Size get size;
...
  void visitAncestorElements(bool visitor(Element element));
  void visitChildElements(ElementVisitor visitor);

这个size貌似可以获取到元素大小,visitAncestorElements和visitChildElements提供了遍历元素的方法,先放一放,看下RenderObject的方法和属性哪些是和大小有关的:

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {

...
/// Whether the constraints are the only input to the sizing algorithm (in
  /// particular, child nodes have no impact).
bool get sizedByParent => false;
...
 /// An estimate of the bounds within which this render object will paint.
  Rect get paintBounds;
...
  /// The bounding box, in the local coordinate system, of this
  /// object, for accessibility purposes.
  Rect get semanticBounds;
...
}
 

这里有三个方法和大小有关,先记下。

获取到Element

和大小有关的方法,大概这些,那么怎么来获取到Element呢?显然Flutter的布局体系不允许我们在build的时候保存一份Widget的实例的引用,只能使用Flutter提供的Key体系,看下Key的说明:

/// A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s.

Key是一个Widget、Element、SemanticsNode的标志。

这里我只找到了一种方法来获取:使用GlobalKey,
类似这样:


class _MyState extends State<MyWidget>{
    GlobalKey _myKey = new GlobalKey();
    
    ...
    Widget build(){
        return new OtherWidget(key:_myKey);
    }

    ...
    void onTap(){
        _myKey.currentContext;
    }
}

通过GlobalKey的currentContext方法找到当前的Element,这里如果有其他的方法,麻烦朋友在下面评论区留言。

下面到了实验时间:

实验一:非ScrollView

在这个实验中,所有元素都是可视的。


  GlobalKey _myKey = new GlobalKey();

  @override
  Widget build(BuildContext context) {

    return new Scaffold(
      appBar: new AppBar(

      ),
      body: new Column(

        children: <Widget>[
          new Container(
            key:_myKey,
            color:Colors.black12,
            child: new Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                new Text("获取大小",style: new TextStyle(fontSize: 10.0),),
                new Text("获取大小",style: new TextStyle(fontSize: 12.0),),
                new Text("获取大小",style: new TextStyle(fontSize: 15.0),),
                new Text("获取大小",style: new TextStyle(fontSize: 20.0),),
                new Text("获取大小",style: new TextStyle(fontSize: 31.0),),
                new Text("获取大小",style: new TextStyle(fontSize: 42.0),),
              ],
            ),

          ),

          new Padding(padding: new EdgeInsets.only(top:100.0),child: new RaisedButton(onPressed:(){

            RenderObject renderObject = _myKey.currentContext.findRenderObject();
            print("semanticBounds:${renderObject.semanticBounds.size} paintBounds:${renderObject.paintBounds.size} size:${_myKey.currentContext.size}");


          }, child: new Text("获取大小"), ),)



        ],


      )
    );

  }

打印结果:

flutter: semanticBounds:Size(168.0, 182.0) paintBounds:Size(168.0, 182.0) size:Size(168.0, 182.0)

结论:在一般情况下(不在ScrollView中,不是ScrollView),可以通过BuildContext的size方法获取到大小,也可以通过renderObject的paintBounds和semanticBounds获取大小。

实验二:含有ScrollView

不是所有元素都可视,有些被ScrollView遮挡住了。

 GlobalKey _myKey = new GlobalKey();
  GlobalKey _myKey1 = new GlobalKey();
  List<Color> colors = [ Colors.greenAccent,Colors.blueAccent,Colors.redAccent ];

  List<Widget> buildRandomWidgets(){
    List<Widget> list = [];

    for(int i=0; i < 100; ++i){

      list.add(new SizedBox(
        height: 20.0,
        child: new Container(
          color:  colors[ i %colors.length ]  ,
        ),
      ));
    }

    return list;
  }

  @override
  Widget build(BuildContext context) {

    return new Scaffold(
      appBar: new AppBar(

      ),
      body: new Column(

        children: <Widget>[

          new Expanded(child: new SingleChildScrollView(
            child: new Container(
              key:_myKey,
              color:Colors.black12,
              child: new Column(
                mainAxisSize: MainAxisSize.min,
                children: buildRandomWidgets(),
              ),
            ),

          )),
          new SizedBox(child:new Container(color:Colors.black),height:10.0),
          new Expanded(child: new ListView(
            key:_myKey1,
            children: <Widget>[
              new Container(
                child:new Column(
                  mainAxisSize: MainAxisSize.min,
                  children:  buildRandomWidgets(),
                ),
              )
            ],

          )),

          new Padding(padding: new EdgeInsets.only(top:10.0),child: new RaisedButton(onPressed:(){

            RenderObject renderObject = _myKey.currentContext.findRenderObject();
            print("semanticBounds:${renderObject.semanticBounds.size} paintBounds:${renderObject.paintBounds.size} size:${_myKey.currentContext.size}");

            renderObject = _myKey1.currentContext.findRenderObject();
            print("semanticBounds:${renderObject.semanticBounds.size} paintBounds:${renderObject.paintBounds.size} size:${_myKey1.currentContext.size}");

          }, child: new Text("获取大小"), ),)



        ],


      )
    );

  }

输出

flutter: semanticBounds:Size(375.0, 2000.0) paintBounds:Size(375.0, 2000.0) size:Size(375.0, 2000.0)
flutter: semanticBounds:Size(375.0, 2000.0) paintBounds:Size(375.0, 2000.0) size:Size(375.0, 2000.0)

注意ScrollView的元素如果不在渲染树中,GlobalKey.currentContext是null

结论:即使在ScrollView中,也一样。

实验三:含有Sliver系列的固定头部等元素:


  @override
  Widget build(BuildContext context) {

    return new Scaffold(
      appBar: new AppBar(

      ),
      body: new Column(

        children: <Widget>[

          new Expanded(child: new CustomScrollView(

            slivers: <Widget>[

              new SliverPersistentHeader(delegate:new _MyFixHeader(),pinned: true,floating: true,),

              new SliverList(
                  key:_myKey,
                  delegate: new SliverChildBuilderDelegate(  (BuildContext context,int index){

                return new Column(
                  mainAxisSize: MainAxisSize.min,
                  children: buildRandomWidgets(),
                );

              },childCount: 1))


            ],


          )),

          new Padding(padding: new EdgeInsets.only(top:10.0),child: new RaisedButton(onPressed:(){

            RenderObject renderObject = _myKey.currentContext.findRenderObject();
            print("semanticBounds:${renderObject.semanticBounds.size} paintBounds:${renderObject.paintBounds.size} size:${_myKey.currentContext.size}");

           // renderObject = _myKey1.currentContext.findRenderObject();
          //  print("semanticBounds:${renderObject.semanticBounds.size} paintBounds:${renderObject.paintBounds.size} size:${_myKey1.currentContext.size}");

          }, child: new Text("获取大小"), ),)



        ],


      )
    );

  }

_MySliverHeader:


class _MySliverHeader extends SliverPersistentHeaderDelegate{
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return new Container(
        color: Colors.grey,
    );
  }

  // TODO: implement maxExtent
  @override
  double get maxExtent => 200.0;

  // TODO: implement minExtent
  @override
  double get minExtent => 100.0;

  @override
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }

}

打印:

把key换到内部的Column上:

return new Column(
                  key:_myKey,
                  mainAxisSize: MainAxisSize.min,
                  children: buildRandomWidgets(),
                );

结果:

flutter: semanticBounds:Size(375.0, 2000.0) paintBounds:Size(375.0, 2000.0) size:Size(375.0, 2000.0)

结论:SliverList等Sliver系列的Widget,不能直接使用上述方法获得大小,必须用内部的容器间接获取

总结一下

1 、可以使用GlobalKey找到对应的元素的BuildContext对象
2 、通过BuildContext对象的size属性可以获取大小,Sliver系列Widget除外
3 、可以通过findRender方法获取到渲染对象,然后使用paintBounds获取到大小。

交流qq群: 854192563


jzoom
1.2k 声望334 粉丝

A simple way to solving problems is using tools like docker/Spring boot/React Native/React/Vue… Technology should not become a bottleneck in thinking.