问题描述
自从棒棒糖和材质设计的引入以来,Android中有一个有点广为人知的问题,那就是带有透明背景的升高的View
暴露出它们背后的阴影绘制的丑陋的人工制品。正如你可能预料到的,多年来,这里发布了许多关于它的问题,尽管它们都是由于相同的潜在原因,但产生的视觉效果可能会有一些不同:这些视觉故障都是由于阴影因渐变缩小而增加而引起的,这会导致其内部边界在View
的范围内向内收缩,这在背景不透明的情况下通常是看不到的。那里不进行裁剪,我在源代码中遇到的每一项相关检查都会在看到该制品时完全关闭阴影,因此裁剪似乎是故意省略的,可能是为了提高效率。
事实上,关闭它或以某种方式完全避免它似乎是用户找到的唯一普遍有效的解决方案:
- How to set semi transparent background color for Android CardView?
- CardView background alpha colour not working correctly
- How do I create a CardView with a semitransparent background color and a corner radius?
- Cardview - rounded corners with transparent background
- Weird view behavior when an elevation and a transparent background color are used on API 21
- Remove background colour from FloatingActionButton
- Android Floating Action Button Semi Transparent Background Color
- How to set border and background color of Floating Action Button with transparency through xml?
- How to center image in FloatingActionButton behind transparent background?
- How to remove those dark circular background from floating action button?
- Android transparency and Shadows
- a shadow appear below material button after setting background color tint in android
- Multiple layers of shadows in android recyclerview elevation
- How to adjust shadow in translucent navbar?
- Transparent white button looks bad in Android Material Design
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.gradle
或settings.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
上名为clipOutlineShadow
的Boolean
-Value扩展属性公开,其行为与任何其他此类var
属性相同。例如:yourElevatedView.clipOutlineShadow = true
就是这样。其他一切都是自动处理的,结果如下所示:
如果您可以修改代码以添加必要的行,那么这可能就是您需要知道的全部内容。图书馆里的大多数其他东西都是为了避免不得不这样做。我仍然建议您查看那里的自述文件,特别是the Notes section,以了解可能存在的问题。
通胀帮手
该项目的一个主要目标是能够在对现有设置进行最少更改的情况下应用此功能。为此,它的其余功能主要用于将LayoutInflator
帮助器插入到膨胀管道中,以便有选择地启用对膨胀的View
的修复。有三种一般配置需要略有不同的方法。对于这个答案,我们假设我们使用的是AppCompatActivity
和材质组件主题,因为这可能是目前最常见的默认设置。另外两个-AppCompatActivity
with AppCompat theme和platform 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
实例作为其内部背景的实现。RenderNodeShadow
和RenderNodeController
总是首选,但如果有必要,ViewShadow
和ViewController
只是稍微贵一点。在深入到实现中的差异之前,我们将快速介绍基本类型。OverlayController
是一个接口,它定义基本的添加和删除方法,维护影子实例的列表,触发其更新,并在需要时负责分离自身。OverlayShadow
是一个抽象类,它处理禁用目标的阴影、跟踪目标的Outline
值以及准备用于裁剪的Path
对象。
ViewOutlineProvider
包装在一个自定义实现中--ProviderWrapper
--该实现在原始提供程序中设置了Outline
的Alpha之后将其置零。此方法保留了Outline
的所有其他内容,如果View
将其用于任何其他用途,如裁剪本身。包装器还带有一个简单的回调函数,允许我们在View
自己的Outline
每次计算时在OverlayShadow
中设置相关数据,因此我们始终是最新的,而不必在绘制例程中进行检查。如上所述,RenderNode
实现包括RenderNodeShadow
和RenderNodeController
。由于我们在此可以直接访问RenderNode
,因此RenderNodeShadow
可以使用轻量级Drawable
来剪裁和绘制RenderNode
,这些Drawable
可以直接添加到父对象的覆盖中。(我们应该能够使用单个Drawable
实例来实现这一点,但是存在不可预见的设计问题。希望它能在不久的将来进行更新。)由于Android版本之间某些类和方法的可用性不同,特定的绘制例程在版本进程中的几个点上会发生变化,但这些差异是偶然的。也就是说,PublicApiDrawable
、StubDrawable
和ReflectorDrawable
基本上都在做同样的事情。对于回退实现,ViewShadow
使用空的View
实例作为其背景RenderNode
对象。如果View
的背景Drawable
为空,则View
不会绘制阴影,因此我们将View
的背景设置为EmptyDrawable
,这基本上是一个仅用于通过空检查的缓存object
。为了在卷影View
上获得正确的Outline
,我们在其上设置了SurrogateViewProviderWrapper
,当卷影View
请求其Outline
更新时,该View
实质上将目标View
传递给它自己的提供程序,因此我们从目标而不是空卷影View
获取Outline
。
ViewController
是ViewGroup
,因为我们需要将阴影View
添加到其中,而不是直接添加到覆盖中。由于硬件加速所发生的绘制重新排序,我们不能裁剪每个阴影View
本身。我们必须对整个覆盖图绘制进行剪裁,这是在ViewGroup
的dispatchDraw()
方法中完成的。这就是叠加阴影的工作原理。虽然这个库的很大一部分是帮助通胀的人,但这些并不是这里真正关注的焦点,因为我肯定不是第一个弄明白这些技术的人。不过,如果任何人需要更多关于这方面的信息,我非常乐意根据要求添加。
目前,在我进行微调时,其中一些功能在两个实现中被拆分和复制。当设计成为最终版本时,此描述将相应更新。
奖金问答
这是不是太夸张了?
不。实际上,核心代码非常小,并且使用了与固有阴影相同的机制,只是稍微重新安排了一下。它并不比任何其他提供平台以外功能的UI装饰库更差,比如Shimmer和诸如此类的东西。
您真的希望有人会使用它吗?
不完全是。我想大多数人都不会认为这样的事情根本没有必要,而且平台中一定有一些简单的开关来正确解决这个问题。是啊,我也是。如果你找到了,让我知道。
这篇关于如何在不更改现有设置的情况下修复透明/半透明视图上的立面阴影?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!