本文介绍了如何在不更改现有设置的情况下修复透明/半透明视图上的立面阴影?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

自从棒棒糖和材质设计的引入以来,Android中有一个有点广为人知的问题,那就是带有透明背景的升高的View暴露出它们背后的阴影绘制的丑陋的人工制品。正如你可能预料到的,多年来,这里发布了许多关于它的问题,尽管它们都是由于相同的潜在原因,但产生的视觉效果可能会有一些不同:

这些视觉故障都是由于阴影因渐变缩小而增加而引起的,这会导致其内部边界在View的范围内向内收缩,这在背景不透明的情况下通常是看不到的。那里不进行裁剪,我在源代码中遇到的每一项相关检查都会在看到该制品时完全关闭阴影,因此裁剪似乎是故意省略的,可能是为了提高效率。

事实上,关闭它或以某种方式完全避免它似乎是用户找到的唯一普遍有效的解决方案:

对于一些常见的设置,有各种定制的解决方案可用,一些用户忽略了它,或者没有意识到它是什么,有些人甚至在他们的设计中集成了这种效果,但我仍然没有找到一个真正修复它的例子。我还没有深入研究原生图形代码,但我在SDK源代码中也找不到任何针对它的实例,除了在阴影可能可见的情况下禁用它之外,如果我们不能在应用程序级别做到这一点,那么低级图形东西能做什么就无关紧要了。

似乎没有太多关于一般问题的信息,但最近一些顽强且可能相当英俊的开发人员在this old question concerning CardView上分享了一些关于它的信息,包括几种解决方法和一些说明性的示例。但是,由于View的阴影是由其父级处理的,因此that answer中给出的所有示例都是修改或附加子绘图的自定义ViewGroup

遗憾的是,这意味着由于各种原因,这些特定的解决方法在许多情况下不可用;例如,如果您不能更改现有的层次结构,如果您不知道最终的父级是什么,等等。

有没有什么方法可以在不使用这些特定示例的情况下应用这些解决办法?


图片修改自Elevation + transparency bug on Android Lollipop,由makovkastar修改,授权使用CC BY-SA 3.0

图片修改自How to create a shape with a transparent background and a shadow, but the shadow should not be visible behind the shape outline?,由timothyjc修改,授权使用CC BY-SA 4.0

图片修改自CardView with transparent background issue on > API 21,由rosenthal修改,授权使用CC BY-SA 3.0

推荐答案

是的,通过禁用View的固有阴影并将裁剪的副本绘制到其父对象的覆盖图上,我们能够对许多(大多数?)用例实施常规修复,这些用例可以完全在外部应用,而不必干扰现有的设置。完整的解决方案非常复杂,因此第一部分仅演示了我根据它组合的the library的基本用法。对于感兴趣的人,下面是内部工作原理的详细解释。

库使用情况

下载

最初几个版本可通过JitPack获得。只需将其Maven URL添加到相应repositories块的末尾;例如,在项目的build.gradlesettings.gradle中:

repositories {
    google()
    mavenCentral()
    maven { url "https://jitpack.io" }
}

并为repo的当前版本添加依赖项:

dependencies {
    …
    implementation 'com.github.zed-alpha:shadow-gadgets:[latest-release]'
}

除非另有说明,否则最新版本始终为#.#.#格式,可在on the repo's Releases page找到。

代码

该库的主要功能通过View上名为clipOutlineShadowBoolean-Value扩展属性公开,其行为与任何其他此类var属性相同。例如:

yourElevatedView.clipOutlineShadow = true

就是这样。其他一切都是自动处理的,结果如下所示:

如果您可以修改代码以添加必要的行,那么这可能就是您需要知道的全部内容。图书馆里的大多数其他东西都是为了避免不得不这样做。我仍然建议您查看那里的自述文件,特别是the Notes section,以了解可能存在的问题。

通胀帮手

该项目的一个主要目标是能够在对现有设置进行最少更改的情况下应用此功能。为此,它的其余功能主要用于将LayoutInflator帮助器插入到膨胀管道中,以便有选择地启用对膨胀的View的修复。有三种一般配置需要略有不同的方法。对于这个答案,我们假设我们使用的是AppCompatActivity和材质组件主题,因为这可能是目前最常见的默认设置。另外两个-AppCompatActivity with AppCompat themeplatform Activity and theme-在图书馆的自述文件中介绍。

确定配置后,首先要考虑的是可以修改什么来将LayoutInflator帮助器附加到Activity。这可以通过主题属性设置或在onCreate()中添加帮助器函数调用的代码中完成。

  • 主题属性

    <style name="Theme.YourApp" parent="Theme.MaterialComponents…">
        …
        <!-- You can use either the fully qualified class name… -->
        <item name="viewInflaterClass">com.zedalpha.shadowgadgets.inflation.MaterialComponentsShadowHelper</item>
    
        <!-- Or, a string resource of that name is also provided, as a convenience. -->
        <item name="viewInflaterClass">@string/material_components_shadow_helper</item>
    </style>
    
  • 帮助器函数

    class MaterialComponentsActivity : AppCompatActivity(R.layout.activity_material_components) {
        override fun onCreate(savedInstanceState: Bundle?) {
            attachMaterialComponentsShadowHelper()
            super.onCreate(savedInstanceState)
            …
        }
    }
    
帮助器将单独对库的clipOutlineShadow属性设置为true的任何<View>应用修复程序。例如:

<com.google.android.material.button.MaterialButton
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/translucent_button"
    …
    app:clipOutlineShadow="true" />

此方法似乎是最直接、最熟悉的,默认行为也是如此。

标记匹配器

如果您不能修改布局文件,则所有阴影辅助对象都接受TagMatcher的列表,该列表可用于根据现有属性(如ID或标记名)选择某些View。同样,此功能设计为严格通过资源设置,或在代码中动态设置。

  • 资源

    可以在R.xml文件中定义匹配器,其中包含一小部分可用标记和属性,这些标记和属性非常不言自明。例如:

    <matchers
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    
        <id android:id="@id/translucent_button" />
        <id android:name="translucent_button" />
        <id android:name="translucent_" app:matchRule="startsWith" />
        <name android:name="Button" app:matchRule="endsWith" />
    </matchers>
    
    所有这些示例都将匹配上面显示的<com.google.android.material.button.MaterialButton>(可能还有其他标记)。解析器非常宽松,除了<id><name>标记以及显示的三个属性之外,忽略所有其他内容。The library's README有更多详细信息。

    R.xml可以通过主题属性指定给帮助器:

    <style name="Theme.YourApp" parent="Theme.MaterialComponents…">
        …
        <item name="shadowTagMatchers">@xml/matchers</item>
    </style>
    

    或在清单中使用<meta-data>元素:

    <application …>
    
        <activity
            android:name=".MaterialComponentsActivity"
            android:theme="@style/Theme.MaterialComponents…">
            <meta-data
                android:name="com.zedalpha.shadowgadgets.SHADOW_TAG_MATCHERS"
                android:resource="@xml/matchers" />
        </activity>
    
    </application>
    
    如果没有为帮助器提供代码中的TagMatcher列表,它将自动在资源中查找,主题属性是第一次尝试。如果该属性不存在,它将在<activity>元素中查找<meta-data>标记,然后在<application>元素中查找<meta-data>

  • 在代码中

    所有attach*ShadowHelper()函数都有接受List<TagMatcher>参数的重载,并且提供了两个助手函数来生成ID和名称匹配器,方法与在XML中类似。例如,此列表等同于上面显示的<matchers>XML:

    class MaterialComponentsActivity : AppCompatActivity(R.layout.activity_material_components) {
        override fun onCreate(savedInstanceState: Bundle?) {
            attachMaterialComponentsShadowHelper(
                listOf(
                    idMatcher(R.id.translucent_button),
                    idMatcher(matchName = "translucent_button"),
                    idMatcher(matchName = "translucent_", matchRule = MatchRule.StartsWith),
                    nameMatcher("Button", MatchRule.EndsWith)
                )
            )
            super.onCreate(savedInstanceState)
            …
        }
    }
    
    如果您需要与那些基本选项不同的匹配选项,TagMatcher是一个简单的接口,您可以自己实现。有关更多详细信息,请参阅the README

希望各种选项不会太令人困惑。基本上,首先选择是在主题中还是在代码中应用帮助器。然后决定是否可以更改布局以添加app:clipOutlineShadow属性。如果你能做到,你就完了。如果不能,则只需担心TagMatcher

库的demo模块是一个应用程序,其中每个选项都有非常简单的示例。

解决方案详细信息

请原谅下面描述的有些脱节。这里涉及到几个不同的活动部分,我认为如果我从高级描述开始逐一解释并深入到细节,而不是将每个类和函数作为一个整体依次呈现,会更有意义。

如开头所述,此修复是通过禁用目标View的固有阴影并将其剪辑的副本绘制到父ViewGroup的覆盖上来完成的。首次应用时,将为父ViewGroup创建OverlayController,并为目标创建OverlayShadow。这些对象作为键标记存储在各自的View上,以便于访问,因此我们不必在其他任何地方跟踪它们。同一父级中的任何其他目标都将添加到现有的OverlayController中,如果删除了所有目标,则该控制器将自行分离。

在目标View上设置OnAttachStateChangeListener,以便在发生相应的连接事件时在其控制器中添加和删除它。这允许我们随时在View上设置clipOutlineShadow属性,而不必担心它在那个时刻是否有父级,或者如果它稍后被分离,我们是否需要清理任何东西。这还意味着,如果将覆盖阴影的目标从一个父级移动到另一个父级,覆盖阴影将自动重新定位。

实际阴影本身是平台的RenderNode类的属性,我们使用它们来绘制裁剪的副本。不幸的是,RenderNode直到API级别29才被添加到SDK中,但幸运的是,它在以前的版本中只被标记为@hide,否则就可以公开访问。这意味着我们可以简单地在compileOnly模块中为旧类及其方法提供存根,该模块允许我们在library中访问它们,就像它们在SDK中可用一样。

当我们处理旧版本上的非SDK接口时,如果在API级别29之前尝试利用失败,我们将退回到使用空的View实例作为其内部背景的实现。RenderNodeShadowRenderNodeController总是首选,但如果有必要,ViewShadowViewController只是稍微贵一点。

在深入到实现中的差异之前,我们将快速介绍基本类型。OverlayController是一个接口,它定义基本的添加和删除方法,维护影子实例的列表,触发其更新,并在需要时负责分离自身。OverlayShadow是一个抽象类,它处理禁用目标的阴影、跟踪目标的Outline值以及准备用于裁剪的Path对象。

要禁用目标的固有阴影,我们将其ViewOutlineProvider包装在一个自定义实现中--ProviderWrapper--该实现在原始提供程序中设置了Outline的Alpha之后将其置零。此方法保留了Outline的所有其他内容,如果View将其用于任何其他用途,如裁剪本身。包装器还带有一个简单的回调函数,允许我们在View自己的Outline每次计算时在OverlayShadow中设置相关数据,因此我们始终是最新的,而不必在绘制例程中进行检查。

如上所述,RenderNode实现包括RenderNodeShadowRenderNodeController。由于我们在此可以直接访问RenderNode,因此RenderNodeShadow可以使用轻量级Drawable来剪裁和绘制RenderNode,这些Drawable可以直接添加到父对象的覆盖中。(我们应该能够使用单个Drawable实例来实现这一点,但是存在不可预见的设计问题。希望它能在不久的将来进行更新。)由于Android版本之间某些类和方法的可用性不同,特定的绘制例程在版本进程中的几个点上会发生变化,但这些差异是偶然的。也就是说,PublicApiDrawableStubDrawableReflectorDrawable基本上都在做同样的事情。

对于回退实现,ViewShadow使用空的View实例作为其背景RenderNode对象。如果View的背景Drawable为空,则View不会绘制阴影,因此我们将View的背景设置为EmptyDrawable,这基本上是一个仅用于通过空检查的缓存object。为了在卷影View上获得正确的Outline,我们在其上设置了SurrogateViewProviderWrapper,当卷影View请求其Outline更新时,该View实质上将目标View传递给它自己的提供程序,因此我们从目标而不是空卷影View获取Outline

ViewControllerViewGroup,因为我们需要将阴影View添加到其中,而不是直接添加到覆盖中。由于硬件加速所发生的绘制重新排序,我们不能裁剪每个阴影View本身。我们必须对整个覆盖图绘制进行剪裁,这是在ViewGroupdispatchDraw()方法中完成的。

这就是叠加阴影的工作原理。虽然这个库的很大一部分是帮助通胀的人,但这些并不是这里真正关注的焦点,因为我肯定不是第一个弄明白这些技术的人。不过,如果任何人需要更多关于这方面的信息,我非常乐意根据要求添加。


目前,在我进行微调时,其中一些功能在两个实现中被拆分和复制。当设计成为最终版本时,此描述将相应更新。

奖金问答

  • 这是不是太夸张了?

    不。实际上,核心代码非常小,并且使用了与固有阴影相同的机制,只是稍微重新安排了一下。它并不比任何其他提供平台以外功能的UI装饰库更差,比如Shimmer和诸如此类的东西。

  • 您真的希望有人会使用它吗?

    不完全是。我想大多数人都不会认为这样的事情根本没有必要,而且平台中一定有一些简单的开关来正确解决这个问题。是啊,我也是。如果你找到了,让我知道。

这篇关于如何在不更改现有设置的情况下修复透明/半透明视图上的立面阴影?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

11-02 01:55