【问题标题】:How can we fix the material shadow glitch on transparent/translucent Composables?我们如何修复透明/半透明 Composable 上的材质阴影故障?
【发布时间】:2022-06-29 03:33:07
【问题描述】:

如果您还不知道,Android 的材质阴影存在缺陷,Material Design 附带的材质阴影及其表面、光照和高程概念。此外,如果您不知道的话,Compose 使用了许多与 View 框架相同的图形 API,包括负责上述阴影的 API,因此它与 Views 存在相同的故障,至少目前是这样。

Card()FloatingActionButton()ExtendedFloatingActionButton()Surface() 显示有和没有半透明背景。

由于我不会进入这里的原因,*我不认为有任何适当的解决方法 - 即,我不认为该平台提供任何方法或配置通过它来剪辑或以其他方式删除该工件 - 所以我们留下了解决方法。此外,一个主要要求是阴影完全按照平台的正常显示,因此任何使用其他技术绘制阴影的方法(如均匀渐变或模糊等)都是不可接受的。

鉴于此,我们能否在 Compose 中创建一个稳健、普遍适用的解决方案?

我个人采用了一种整体方法,即禁用原始阴影并在其位置绘制一个剪裁的副本。 (我意识到简单地打一个洞并不是阴影的实际工作方式,但这似乎是主要的预期效果。)我在下面的答案中分享了一个 Compose 版本的示例,但主要动机这个问题是为了在将其放入图书馆之前检查更好的想法。

我确信我的示例中存在可以改进的技术细节,但我主要对根本不同的方法或建议感到好奇。我不感兴趣,例如,以某种方式使用drawBehind()Canvas() 来代替做本质上相同的事情,或者重构参数只是为了插入内容等等。我更多地考虑以下方面:

  • 您能否设计一些其他(更高效)的方法来修剪该工件,而无需创建和剪切单独的阴影对象?使用Views,我发现的唯一方法是绘制View 两次,其中一次绘制内容被剪辑,另一次绘制禁用阴影。不过,考虑到开销,我最终决定不这样做。

  • 是否可以将其提取为Modifier 和扩展名,类似于*GraphicsLayerModifiers 和shadow()/graphicsLayer()?我还没有完全理解 Compose 的所有概念和功能,但我不这么认为。

  • 是否有任何其他方法可以使其普遍适用,而无需额外布线?在我的示例中,阴影对象依赖于三个可选参数,它们具有来自目标可组合的默认值,除了用另一个可组合包装目标之外,我想不出任何方法来获得这些参数。


*my question here 中概述了这些原因。

【问题讨论】:

    标签: android kotlin android-jetpack-compose


    【解决方案1】:

    我们将在这个本地示例中使用FloatingActionButton(),因为它支持我们需要考虑的几乎所有选项,但这应该适用于任何 Composable。为方便起见,如果您想尝试除 FloatingActionButton() 之外的其他方法,我已经使用此解决方案包装了几个更常见的并将它们组装在 this GitHub gist 中。

    我们希望我们的包装器 Composable 充当替代品,因此它的参数列表和默认值完全从 FloatingActionButton() 复制:

    @Composable
    fun ClippedShadowFloatingActionButton(
        onClick: () -> Unit,
        modifier: Modifier = Modifier,
        interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
        shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
        backgroundColor: Color = MaterialTheme.colors.secondary,
        contentColor: Color = contentColorFor(backgroundColor),
        elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
        content: @Composable () -> Unit
    ) {
        Layout(
            {
                ClippedShadow(
                    elevation = elevation.elevation(interactionSource).value,
                    shape = shape,
                    modifier = modifier
                )
                FloatingActionButton(
                    onClick = onClick,
                    modifier = modifier,
                    interactionSource = interactionSource,
                    shape = shape,
                    backgroundColor = backgroundColor,
                    contentColor = contentColor,
                    elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
                    content = content
                )
            },
            modifier
        ) { measurables, constraints ->
            require(measurables.size == 2)
    
            val shadow = measurables[0]
            val target = measurables[1]
    
            val targetPlaceable = target.measure(constraints)
            val width = targetPlaceable.width
            val height = targetPlaceable.height
    
            val shadowPlaceable = shadow.measure(Constraints.fixed(width, height))
    
            layout(width, height) {
                shadowPlaceable.place(0, 0)
                targetPlaceable.place(0, 0)
            }
        }
    }
    

    我们基本上将 FloatingActionButton() 和我们的副本影子 Composable 包装在为设置优化的 Layout() 中。除了elevation 之外,大多数参数都原封不动地传递给包装的FloatingActionButton(),我们将其归零以禁用固有阴影。相反,我们将适当的原始高程值​​指向我们的ClippedShadow(),这里使用FloatingActionButtonElevationInteractionSource 参数计算。像Card() 这样更简单的可组合项将在Dp 中具有可以直接传递的无状态海拔值。

    ClippedShadow() 本身是另一个自定义的Layout(),但没有内容:

    @Composable
    fun ClippedShadow(elevation: Dp, shape: Shape, modifier: Modifier = Modifier) {
        Layout(
            modifier
                .drawWithCache {
                    // Naive cache setup similar to foundation's Background.
                    val path = Path()
                    var lastSize: Size? = null
    
                    fun updatePathIfNeeded() {
                        if (size != lastSize) {
                            path.reset()
                            path.addOutline(
                                shape.createOutline(size, layoutDirection, this)
                            )
                            lastSize = size
                        }
                    }
    
                    onDrawWithContent {
                        updatePathIfNeeded()
                        clipPath(path, ClipOp.Difference) {
                            this@onDrawWithContent.drawContent()
                        }
                    }
                }
                .shadow(elevation, shape)
        ) { _, constraints ->
            layout(constraints.minWidth, constraints.minHeight) {}
        }
    }
    

    我们只需要它的影子和Canvas 访问,我们通过两个简单的Modifier 扩展来获得。 drawWithCache() 让我们保留一个简单的 Path 缓存,用于剪辑和恢复整个内容绘制,shadow() 是不言自明的。将此 Composable 分层放置在目标后面,其自身的阴影被禁用,我们得到了想要的效果:

    与问题一样,前三个是Card()FloatingActionButton()ExtendedFloatingActionButton(),但包含在我们的修复中。为了证明InteractionSource/elevation 重定向按预期工作,this brief gif 并排显示两个具有完全透明背景的FloatingActionButton()s;右边的那个应用了我们的修复。

    对于上面修复图像中的第四个示例,我们使用了单独的 ClippedShadow(),只是为了说明它也可以单独工作:

    ClippedShadow(
        elevation = 10.dp,
        shape = RoundedCornerShape(10.dp),
        modifier = Modifier.size(FabSize)
    )
    

    就像常规的可组合项一样,它应该适用于任何对当前 API 级别有效的Shape。任意凸形Paths 适用于所有相关版本,从 API 级别 29 (Q) 开始,凹形也可以。

    【讨论】:

      【解决方案2】:

      Compose for Desktop 也有同样的问题。我还没有在 Android 上尝试过,但是在桌面上,如果一个形状的路径由两个轮廓组成(它也可能是像甜甜圈这样的穿孔形状变体),那么它的阴影将是实心的。因此,对于自定义形状的情况,您可以使用变通方法并在远处绘制一个小点。如果点小于一个像素,它将无法工作,但它很可能会在屏幕外。

      class CustomShapeForShadow(private val path: Path) : Shape {
        override fun createOutline(
          size: Size,
          layoutDirection: LayoutDirection,
          density: Density
        ): Outline {
          val p2 = Path().apply {
              addRect(Rect(Offset(-1000000f, -1000000f), Size(10f, 10f)))
          }
          val pathRez = Path()
          pathRez.op(path, p2, PathOperation.Union)
          return Outline.Generic(path = pathRez)
        }
      }
      

      这个解决方案看起来不太好。但至少在第一次近似中,它运行良好。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2022-06-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-10
        • 1970-01-01
        • 2018-05-10
        • 1970-01-01
        相关资源
        最近更新 更多