A ripple effect demo
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
.idea
app
gradle/wrapper
.gitattributes
.gitignore
README.md
build.gradle
gradle.properties
gradlew
gradlew.bat
preview.gif
settings.gradle

README.md

Ripple

A Ripple effect demo

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

Ripple_effect


Ripple效果的设置

可以在XML布局文件中对 Viewandroid: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)

    <!-- 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/imageselector.

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

// ↓↓↓ 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即可.
核心代码如下:


                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动画的自动播放

// 开始自动播放
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