【Android】Ripple使用总结及ClickableSpan的冲突解决

本文详细介绍了Android中Ripple效果的设置、生效条件、无边界Ripple、硬件加速的影响,以及如何处理与ClickableSpan的冲突,并提供了Ripple动画自动播放的原理分析。同时,给出了GitHub源码链接和示例效果图。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章目录
  1. 1. Ripple效果的设置
  2. 2. Ripple的生效
  3. 3. 不适用Ripple的场景
  4. 4. 无边界的Ripple (unbounded ripple)
  5. 5. 硬件加速开关对无边界Ripple的影响
  6. 6. 子层(Child Layer)
  7. 7. Mask层(Mask Layer)
  8. 8. 与ClickableSpan冲突
  9. 9. Ripple动画的自动播放

GitHub源码:Ripple Demo
RippleDrawable官方文档链接:RippleDrawable
效果图如下:

Ripple_effect


Ripple效果的设置

可以在XML布局文件中对 View 的 android:background 属性进行赋值.
android:foreground 的Ripple支持仅支持 FrameLayout 或其子类如support-v7中的 CardView.
android:foreground 的Ripple使用场景为当点击不透明的Image时,见效果图中的Ripple by 'foreground' Only FrameLayout Support
也可以在代码中动态设置.
Ripple_setting


Ripple的生效

当 View 有设置 OnClickListener 的情况下被点击, 或者获得/失去焦点变化时,将出现Ripple效果.


不适用Ripple的场景

  • 点击之后就立马消失的组件(setVisibility:gone invisible 或 remove).
    因为当组件恢复为visiable后,未播放完的Ripple动画会继续播放,会产生疑惑。

无边界的Ripple (unbounded ripple)

见效果图中第一行Ripple NO Child Layers or Mask (/drawable/ripple.xml)

       
1
2
       
< ! - - An unbounded red ripple . - - />
< ripple android:color="#ffff0000" />

ripple标签内只指定一个android:color属性时,则该ripple效果的绘制会溢出其所在View的边界,直接绘制在父控件的背景之上。
如果父控件没有设置背景,则会进一步绘制在父控件的上一级父控件的背景之上。

如在Demolayout/layout_toolbar.xml,把作为rootViewLinearLayout的属性android:background="@android:color/background_dark"删除,则会出现下图的效果:
unbounded ripple atop granddad' background


硬件加速开关对无边界Ripple的影响

在Android 3.0 (API level 11)引入的硬件加速功能默认在application/Activity/View这三个层级上都是开启的。
但如果手贱关闭了,则无边界Ripple不会生效。
见效果图中的第二行Ripple NO Child Layers or Mask but HARDWARE OFF


子层(Child Layer)

由于View在不同的交互下有不同的state,常见的为pressed和’focused’或normal这三种状态.
所以Ripple通过多个item来表示不同state下的显示,每个item都是一个子层(Child Layer),能够直接显示colorshapedrawable/image 及 selector.

Ripple存在一个或多个子层时,则ripple效果则被限定在当前View的边界内了.无边界效果(unbounded ripple)失效.

       
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
       
// ↓↓↓ Ripple With Child Layer(Color Red) and Mask
<ripple android:color="@android:color/holo_green_light">
<item android:id="@android:id/mask"
android:drawable= "@android:color/holo_red_light" />
</ripple >
// ↓↓↓ Ripple With Shape and Mask
<ripple android:color="@android:color/holo_green_light">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="@android:color/holo_red_light" />
<corners android:radius="30dp" />
</shape>
</item>
</ripple >
// ↓↓↓ Ripple With Picture and Mask
<ripple android:color="@android:color/holo_green_light">
<item android:id="@android:id/mask"
android:drawable= "@drawable/google" />
</ripple >
// ↓↓↓ Ripple With Selector
// ↓↓↓ the drawing region will be drawn from RED gradient to GREEN.
<ripple android:color="@android:color/holo_green_light">
<item>
<selector>
<item android:drawable="@android:color/holo_red_light"
android:state_pressed= "true"/>
<item android:drawable="@android:color/transparent"/>
</selector>
</item>
</ripple >

Mask层(Mask Layer)

可以设置指定子层itemandroid:id="@android:id/mask"来设定当前RippleMask.
Mask的内容并不会被绘制到屏幕上.它的作用是限定Ripple效果的绘制区域.

  • mask所在的的子层限制了Ripple效果的最大范围只能是View的边界,不会扩散到父组件.
  • 控制ripple效果区域的细节显示.
    细节显示可以通过Ripple With Picture and Mask来理解.本处中用于显示的是一张背景透明的彩色Google图片,但Ripple的扩散过程中只在有颜色的区域中慢慢扩散,透明区域则仍是透明.

google.png
preview


与ClickableSpan冲突

如果Layout有包含ClickableSpanTextView,则发现该Layout设置Ripple的效果无法响应.
这个现象可以推断出MotionEvent这个事件在TextView这一层级被消耗了.下一步应该为找出该事件为什么被消耗?
通过debug源码,发现当点击事件传递到TextView时,会进一步传递给LinkMovementMethod::onTouchEvent(),如果点击位置处于ClickableSpan以外,则返回Touch.onTouchEvent(widget, buffer, event);
该方法在处理MotionEvent::ACTION_DOWN时默认返回true,导致Ripple失效.见下图(android(level 23) source code ):
ripple.not.active.reason

那么解决思路也就简单了,重写LinkedMovementMethod::onTouchEvent()方法,当且仅当点击到ClickableSpan时,才返回true即可.
核心代码如下:

       
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
       
int action = event.getAction();
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
int x = ( int) event.getX();
int y = ( int) event.getY();
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
x += getScrollX();
y += getScrollY();
Layout layout = getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
// get ClickableSpan whick were pressed
ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
if (link.length != 0) {
// if find ClickableSpan
if (action == MotionEvent.ACTION_UP) {
link[ 0].onClick( this);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[ 0]),
buffer.getSpanEnd(link[ 0]));
}
// consume DOWN or other action
return true;
} else {
// if none
Selection.removeSelection(buffer);
}
// deliver to parent view
return false;

当然,在Demo中,为了进一步简化,直接把LinkedMovementMethod::onTouchEvent()写到了RippleTextView::onTouchEvent()中去.具体见源码.


Ripple动画的自动播放

       
1
2
3
4
5
       
// 开始自动播放
rippleDrawable.setState( new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled});
// 恢复初始状态
rippleDrawable.setState( new int[]{android.R.attr.state_enabled});

原理见源码:
theory


About Sodino

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值