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

 

文章目录
  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

猜你喜欢

转载自blog.csdn.net/sodino/article/details/53910068