- 1. Ripple效果的设置
- 2. Ripple的生效
- 3. 不适用Ripple的场景
- 4. 无边界的Ripple (unbounded ripple)
- 5. 硬件加速开关对无边界Ripple的影响
- 6. 子层(Child Layer)
- 7. Mask层(Mask Layer)
- 8. 与ClickableSpan冲突
- 9. Ripple动画的自动播放
GitHub源码:Ripple Demo
RippleDrawable官方文档链接:RippleDrawable
效果图如下:
Ripple效果的设置
可以在XML布局文件中对 View
的 android:background
属性进行赋值.android:foreground
的Ripple支持仅支持 FrameLayout
或其子类如support-v7中的 CardView
.android:foreground
的Ripple使用场景为当点击不透明的Image
时,见效果图中的Ripple by 'foreground' Only FrameLayout Support
也可以在代码中动态设置.
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
的边界,直接绘制在父控件的背景之上。
如果父控件没有设置背景,则会进一步绘制在父控件的上一级父控件的背景之上。
如在Demo
中layout/layout_toolbar.xml
,把作为rootView
的LinearLayout
的属性android:background="@android:color/background_dark"
删除,则会出现下图的效果:
硬件加速开关对无边界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),能够直接显示color
、shape
、drawable/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)
可以设置指定子层item
的android:id="@android:id/mask"
来设定当前Ripple
的Mask
.Mask
的内容并不会被绘制到屏幕上.它的作用是限定Ripple
效果的绘制区域.
- mask所在的的子层限制了
Ripple
效果的最大范围只能是View
的边界,不会扩散到父组件. - 控制
ripple
效果区域的细节显示.
细节显示可以通过Ripple With Picture and Mask
来理解.本处中用于显示的是一张背景透明的彩色Google
图片,但Ripple
的扩散过程中只在有颜色的区域中慢慢扩散,透明区域则仍是透明.
与ClickableSpan冲突
如果Layout
有包含ClickableSpan
的TextView
,则发现该Layout
设置Ripple
的效果无法响应.
这个现象可以推断出MotionEvent
这个事件在TextView
这一层级被消耗了.下一步应该为找出该事件为什么被消耗?
通过debug源码,发现当点击事件传递到TextView
时,会进一步传递给LinkMovementMethod::onTouchEvent()
,如果点击位置处于ClickableSpan
以外,则返回Touch.onTouchEvent(widget, buffer, event);
该方法在处理MotionEvent::ACTION_DOWN
时默认返回true
,导致Ripple
失效.见下图(android(level 23) source code ):
那么解决思路也就简单了,重写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});
|
About Sodino