Flutter UI Widget FittedBox 详解与原理 | Flutter

411 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

先介绍 FittedBox 的定义、参数,然后用丰富的示例对使用进行说明,最后解释其实现原理和一些扩展概念。在做兼容性,适老化等场景较有用。

介绍

FittedBox 可以缩放和定位 child。可以缩放任何Widget,较常使用场景为宽高大小确定下缩放文本,或者一些 UI 兼容场景。

官方定义

/// Scales and positions its child within itself according to [fit].

参数

fit :为 BoxFit 类型,支持类型为 fill, contain, cover, fitWidth, fitHeight, none, scaleDown,用于表示 child 如何在 布局阶段如何分配空间

alignment :为 AlignmentGeometry 类型,用于表示在 父布局的边界范围内,如何排列。

clipBehavior :剪切内容的方式,默认为不剪切。

使用

Row(

  children: [

    Image.asset('assets/images/icon_flutter.png'),

    Text(

      'The quick brown fox jumps over the lazy dog',

    )

  ],

)

如上场景,左图右文,文字过长时超出屏幕。这时有两种方式可以使文字完全展示,换行或缩小。

换行

给右面的文字固定宽度,使文字可以自动换行。先使用Expanded 把文字包一下,效果如下

Row(

  children: [

    Image.asset('assets/images/icon_flutter.png'),

    const Expanded(

      child: Text(

        'The quick brown fox jumps over the lazy dog',

      ),

    )

  ],

)

文本通过换行的方式完全展示出来了。

缩小

传统做法

假如不希望换行,单行完全展示文本呢。如在启动页或者产品展示页,单行可以使页面更简洁大方。

有种较复杂的实现方法,

  • 首先获得文本框的宽度
  • 给文字设定最小字号,使用 TextPaint 绘制指定字号文本时测量指定字号的宽度
  • 将字号逐渐增大,当测量宽度超过字的最大宽度时,即设定字号为上一个字号为目标文本大小。

这种方式可以解决问题,但是实现较复杂,用到的较底层API较多。特殊场景比较适合使用这种较高成本的方式,如一屏内尽量展示最大字体的展示所有省份,或者所有首字母进行筛选。

FittedBox

前面看到使用 FittedBox 可以缩放内容,我们用FittedBox 包住Text来试一下。

Row(

  children: [

    Image.asset('assets/images/icon_flutter.png'),

    const FittedBox(

      child: Text(

        'The quick brown fox jumps over the lazy dog',

      ),

    )

  ],

)

结果文本仍然超出屏幕了,这是为什么?

我们可以看一下 FittedBox 对应的 RenderFittedBox 的代码

@override

void performLayout() {

  if (child != null) {

    child!.layout(const BoxConstraints(), parentUsesSize: true);

    ...

  } else {

    size = constraints.smallest;

  }

}

其中BoxConstraints默认为

const BoxConstraints({

  this.minWidth = 0.0,

  this.maxWidth = double.infinity,

  this.minHeight = 0.0,

  this.maxHeight = double.infinity,

})

child 在布局时传入了 BoxConstraintsparentUsesSizeBoxConstraints 的的宽度是未指定,父布局是 Row 宽度也是未指定,所以 文本在绘制还是会超出屏幕。

根据 parentUsesSize 字段说明只要再给 FittedBox 加个宽限制,child 绘制时便会有依据了。

我们给 FittedBox 添加个 Expanded

Row(

  children: [

    Image.asset('assets/images/icon_flutter.png'),

    const Expanded(

      child: FittedBox(

        fit: BoxFit.scaleDown,

        child: Text(

          'The quick brown fox jumps over the lazy dog',

        ),

      ),

    )

  ],

)

效果如上,可以看到 文本缩小了,在单行完全展示了。

所以在 使用 FittedBox 时因为其无宽高限制,应当给 FittedBox 指定宽高以便内容的缩放。

原理

FittedBox 如何缩放的

FittedBox 对应的 RendObjectRenderFittedBox。看下其中的实现

首先看下大小不一致的 FittedBox 和 child 的缩放比例。

void _updatePaintData() {

 ....

 ....

    // 用  FittedBox 的大小比子组件的大小,从而得到子组件的缩放比例

    final Size childSize = child!.size;

    final FittedSizes sizes = applyBoxFit(_fit, childSize, size);

    final double scaleX = sizes.destination.width / sizes.source.width;

    final double scaleY = sizes.destination.height / sizes.source.height;

    final Rect sourceRect =

        _resolvedAlignment!.inscribe(sizes.source, Offset.zero & childSize);

    final Rect destinationRect =

        _resolvedAlignment!.inscribe(sizes.destination, Offset.zero & size);

    _hasVisualOverflow = sourceRect.width < childSize.width ||

        sourceRect.height < childSize.height;



....

....



    // 根据缩放比例得到 转换的 transform,之后的绘制和触摸实践传递到都依赖这个 transform

    _transform = Matrix4.translationValues(

        destinationRect.left, destinationRect.top, 0.0)

      ..scale(scaleX, scaleY, 1.0)

      ..translate(-sourceRect.left, -sourceRect.top);

    assert(_transform!.storage.every((double value) => value.isFinite));

  }

}

看下绘制阶段如何使用 transform



TransformLayer? _paintChildWithTransform(

    PaintingContext context, Offset offset) {

...

...

...

// 这句代码是变换绘制的关键

    return context.pushTransform(

      needsCompositing,

      offset,

      _transform!,

      super.paint,

      oldLayer: layer is TransformLayer ? layer! as TransformLayer : null,

    );

...

...

...

  return null;

}

通过以上代码可以判断通过绘制阶段进行矩阵变换可以做到缩放子组件。

FittedBox 缩放后触摸事件如何传递

当子组件使用 transform 变换了绘制后,触摸事件如何和变换了位置的widget相关联,答案是触摸事件的位置也通过 transform 进行变换。代码如何

@override

bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {

 

... 

  return result.addWithPaintTransform(

    transform: _transform,

    position: position,

    hitTest: (BoxHitTestResult result, Offset position) {

    //这里的 position 是经过 transform,能够对应变换后的组件。

      return super.hitTestChildren(result, position: position);

    },

  );

  ... 

}

其他问题

子组件写死宽度 100,FittedBox 最终绘制出来的组件大小可能不到100,是因为缩放了。

有个场景子组件内又嵌套了很多子组件,其中有文本。当使用 FittedBox 将整个子组件进行缩放后,文本可能会变得很小。有没有可能 只缩放子组件中的非文本组件?目前来看是不可行的,FittedBox 是对子组件进行整体的缩放,没有提供过滤的方法。