【Flutter 组件集录】Opacity| 8月更文挑战

1,706 阅读7分钟
前言:

这是我参与8月更文挑战的第 7 天,活动详情查看:8月更文挑战。为应掘金的八月更文挑战,我准备在本月挑选 31 个以前没有介绍过的组件,进行全面分析和属性介绍。这些文章将来会作为 Flutter 组件集录 的重要素材。希望可以坚持下去,你的支持将是我最大的动力~

本系列组件文章列表
1.NotificationListener2.Dismissible3.Switch
4.Scrollbar5.ClipPath6.CupertinoActivityIndicator
7.Opacity8.FadeTransition9. AnimatedOpacity
10. FadeInImage11. Offstage12. TickerMode
13. Visibility14. Padding15. AnimatedContainer
16.CircleAvatar17.PhysicalShape18.Divider
19.Flexible、Expanded 和 Spacer 20.Card

一、认识 Opacity 组件

Opacity 组件定义在框架中的 widgets/basic.dart 中,说明它是一个比较基础的组件。其作用是:将一个组件 以指定透明度 opacity 进行透明处理


1. Opacity 基本信息

下面是 Opacity 组件类的定义构造方法,可以看出它继承自 SingleChildRenderObjectWidget。实例化时必须传入 opacity 入参,还可以传入一个 child 组件。

Opacity 组件功能很单一,用起来也非常简单。另外,将 widget 透明还有其他的手段,比如设置颜色的 alpha 值。那使用Opacity 组件有什么优劣,又有什么必要性呢,何时该用,何时不该用呢?本文将从源码的角度去重新审视 Opacity 组件,让你认识到一个更全面的 Opacity


2.Opacity 的使用

Opacity 组件的用法非常简单,只要指定 opacity 即可,下面是 0.1 ~ 0.8 间隔 0.1 的透明度效果。

class OpacityTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 10,
      runSpacing: 20,
      children: [0.1,0.2,0.3, 0.4, 0.5,0.6, 0.7, 0.8]
          .map((opacity) => Opacity(
                opacity: opacity,
                child: Image.asset(
                  'assets/images/icon_head.webp',
                  width: 80,
                  height: 80,
                ),
              ))
          .toList(),
    );
  }
}

3.基于颜色的透明度

对于 图片的透明度 ,我们可以使用 color + colorBlendMode 实现,效果和 Opacity 是一样的。

class OpacityTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 10,
      runSpacing: 20,
      children: [0.1,0.2,0.3, 0.4, 0.5,0.6, 0.7, 0.8]
          .map((opacity) =>
              Image.asset(
                  'assets/images/icon_head.webp',
                  width: 80,
                  height: 80,
                  color: Color.fromRGBO(255, 255, 255, opacity),
                  colorBlendMode: BlendMode.modulate
                ),
              )
          .toList(),
    );
  }
}

那这两种方式有什么差异呢?在此之前,先看一下 Opacity 的源码是如何实现的。


二、 Opacity 的源码实现

1. Opacity 组件属性

Opacity 最主要的属性是 double 型的 opacity ,可以传入一个 child 组件。通过断言可以看出 opacity 不可为空,且取值范围在 [0.0~1.0] 之间。


2.Opacity 维护的 RenderObject

Opacity 继承自 SingleChildRenderObjectWidget ,作为 RenderObjectWidget 一族,有着创建和维护 RenderObject 的使命。

其中Opacity#createRenderObject 返回的是 RenderOpacity ,也就是说透明化功能的具体实现,是由 RenderOpacity 类对象完成的。Opacity 最为组件,只是起到一个 配置作用


3.认识 RenderOpacity

RenderOpacityRenderObject 的后代,也就是渲染对象,它承担着布局绘制的任务。在构造方法中时会根据传入的 opacity_alpha 成员进行初始化。

其中getAlphaFromOpacityColor 类的静态方法,作用就是将 0 ~ 1opacity 转换为 0 ~ 255alpha 值。

---->[Color#getAlphaFromOpacity]----
static int getAlphaFromOpacity(double opacity) {
  assert(opacity != null); // ignore: unnecessary_null_comparison
  return (opacity.clamp(0.0, 1.0) * 255).round();
}

4.RenderOpacity#paint 方法

RenderOpacity 最重要的是绘制的处理,如下是 RenderOpacity#paint 的逻辑。可以看出,当 child 非空时,如果 _alpha = 0 就什么都不需要画,直接返回。如果 _alpha = 255 ,则直接绘制 child

从这里可以了解到,通过 Opacity 组件,设置为 0 透明,可以既保留 child 组件的占位,又可以避免绘制 child ,是一种非常好的隐藏组件的方式。如果不需要占位,通过 if 是更好的选择,如果需要占位 Opacity 会略胜一筹。比较 用 if 需要在 else 给占位组件。

最后,如果有透明度时,会通过 context.pushOpacity 进行处理。那 pushOpacity 方法做了什么呢?


三、认识 PaintingContext#pushOpacity

1.断点调试

下面通过一个最精简的 demo 来调试看看运行时的情况。

void main() => runApp(OpacityTest());

class OpacityTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Opacity(
        opacity: 0.3,
        child: Image.asset(
        'assets/images/icon_head.webp',
        width: 80,
        height: 80,
    ));
  }
}

断点信息如下,左侧栈帧显示绘制一帧到RenderOpacity#paint 时,方法的入栈情况。RenderObject 有一个 ContainerLayer 型的 _layer 属性,默认为 null。下一步将执行 context.pushOpacity 。这里的 context 对象类型为 PaintingContext,入参的 _alpha = 77oldLayer = null


PaintingContext#pushOpacity 入栈后,如果 oldLayer 为空,就会创建 OpacityLayer ,并为其设置 alphaoffset,然后执行 pushLayer 方法。


childLayer 有孩子的话,会移除其孩子,这里刚创建的 OpacityLayer ,没有孩子,不会走入 if 代码块。

然后通过 appendlayer 将过程创建的 OpacityLayer 添加到 _containerLayer 中。


2.层的合成

drawFrame 中的 flushLayoutflushCompositingBitsflushPaint 完成后,此时屏幕并不会出现内容。还需要通过 renderView.compositeFrame() 将数据传送给 GPU

之前的方法的处理完后,元素树渲染树都已经形成。作为渲染树最顶层的节点,renderView 对象会通过 compositeFrame 来合成帧。RenderObject 对象中有 Layer 成员,如下 renderViewlayerTransformLayer ,它也有 childparent 属性,形成树状结构。

上图的层中只有两个节点,TransformLayer和它的孩子 OpacityLayer。然后执行 layer!.buildScene(builder) ,创建 ui.Scene 对象。

其中 addToScene 方法会让孩子执行 addToScene,这样所有的 Layer 就添加到了 Scene 中。


3. ui.Scene 的用途

通过 renderView 中持有的 TransformLayer 调用 buildScene 方法,就可以将其下所以的 Layer 合并到一个 Scene 中,并返回 ui.Scene 对象,最后通过 _window.render 方法进行渲染 ui.Scene 对象。

渲染的方法是一个平台的 native 方法。总的来看,使用 Opacity 组件,最终是通过一个 OpacityLayer 层实现的透明功能。


四、再看 Opacity 的优劣

Opacity 组件功能实现的方式是直接用 OpacityLayer 完成的,这样在合成帧的时候就会多一个层,会有些许的昂贵,但是存在就有存在的价值 。使用颜色进行透明是有局限性的,有些透明度必须要 Opacity 组件。 如果是 ImageColor 需要透明,那么杀鸡焉用牛刀,颜色透明足以。

但当整个 item 或是整个页面需要透明化,使用 Opacity 组件就会很方便和简洁。不然你需要对所有关于颜色的地方进行透明度处理。这样处理会让代码很复杂,可读性差,而且这样的操作可能会让一些对象无法保持 const 常量,比如一个 const Text("XXX"),为了让其颜色透明,需要使用 Texstyle 定义颜色。而这个透明度需要运行时获取,那么这个 Text 无法保持 const,还多出样式处理的代码。

Layer 的层级结构设计出来也是为了使用的,多一个 Layer,也就是在合成时多一些方法的出入栈,并没有想象中的那么重。所以 Opacity 该用的时候还是要用的,但对于单一的图片、颜色的透明处理,能省则省,就不需要用了。

Widget buildItem() {
  return Container(
      height: 80,
      margin: const EdgeInsets.all(10),
      alignment: Alignment.center,
      decoration:  BoxDecoration(
        border: Border.all(color: Colors.red),
        borderRadius: BorderRadius.circular(10)
      ),
      child:  Row(
            children: [
              const SizedBox(width: 15),
              Image.asset(
                'assets/images/icon_head.webp',
                width: 60,
                height: 60,
              ),
              const SizedBox(width: 15,),
              Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text( '张风捷特烈',
                    style:  TextStyle(fontSize: 16),
                  ),
                  const Text( '编程之王',
                    style:  TextStyle(fontSize: 14,color: Colors.grey),
                  ),
                ],
              ),
              const Spacer(),
              const Icon(Icons.ac_unit_outlined, color: Colors.blue , size: 24),
              const SizedBox(width: 5,),
              const Icon(Icons.alt_route_rounded, color:  Colors.blue , size: 24,),
              const SizedBox(width: 20,),
            ],
          ),
    );
}

有人也许会灵光一现,不是有个 ColorFiltered 吗,指定 ColorFilter 透明不就行了吗。当你瞄一眼 ColorFiltered 的源码,就会发现,它的原理也是通过添加一个 ColorFilterLayer 实现的,所以这样反而会弄巧成拙。

当你了解了这些,对 Opacity 这个组件就会有全面的认识,好与不好,只是使用的场景的优劣,工具本无罪。在使用时留意一下即可,问一下,“我是否可以很方便的通过颜色的透明度实现透明效果”。能则不用 Opacity ,反则用之。另外不要忘记 Opacity 对于单一组件显隐的优势。那本文就到这里,谢谢观看 ~


Opacity 组件 的使用方式到这里就介绍完毕,虽然是个简单的小组件,但通过其源码实现,我们还是能学到一点东西的。那本文到这里就结束了,谢谢观看,明天见~