如何触发Android Lollipop上的涟漪效应,在视图内的特定位置,而不触发触摸事件?

时间:2014-12-01 09:13:41

标签: android android-5.0-lollipop rippledrawable

这是一个简短的问题:

假设我有View RippleDrawable作为背景。

是否有一种简单的方法可以触发特定位置的纹波而不会触发任何触摸或点击事件?

6 个答案:

答案 0 :(得分:27)

是的!为了以编程方式触发纹波,您必须使用RippleDrawable设置setState()的状态。致电setVisible()工作!

解决方案

要显示纹波,您必须同时将状态设置为按下并启用:

rippleDrawable.setState(new int[] { android.R.attr.state_pressed, android.R.attr.state_enabled });

只要设置了这些状态,就会显示纹波。如果要再次隐藏纹波,请将状态设置为空int[]

rippleDrawable.setState(new int[] {  });

您可以通过调用setHotspot()来设置发出波纹的点。

工作原理

我已经调试了很多并且上下研究了RippleDrawable的源代码,直到我意识到涟漪实际上是在onStateChange()中触发的。调用setVisible()没有任何效果,也不会导致实际出现任何涟漪。

RippleDrawable源代码的相关部分是:

@Override
protected boolean onStateChange(int[] stateSet) {
    final boolean changed = super.onStateChange(stateSet);

    boolean enabled = false;
    boolean pressed = false;
    boolean focused = false;

    for (int state : stateSet) {
        if (state == R.attr.state_enabled) {
            enabled = true;
        }
        if (state == R.attr.state_focused) {
            focused = true;
        }
        if (state == R.attr.state_pressed) {
            pressed = true;
        }
    }

    setRippleActive(enabled && pressed);
    setBackgroundActive(focused || (enabled && pressed));

    return changed;
}

如您所见,如果同时设置了enabled和pressed属性,则会激活纹波和背景,并显示纹波。 此外,只要您设置了聚焦状态,背景也将被激活。通过这种方式,您可以触发纹波并使背景颜色独立。

如果您有兴趣,可以查看RippleDrawable here的完整源代码。

答案 1 :(得分:14)

我将@Xaver Kapeller和@Nikola Despotoski的答案合并/组合在上面:

import java.awt.GridLayout;
import javax.swing.*;

public class StackingLabels extends JPanel {
    public static final String[] TEXTS = { "Sunday", "Monday", "Tuesday",
            "Wednesday", "Thursday", "Friday", "Saturday" };

    public StackingLabels() {
        setLayout(new GridLayout(0, 1));
        for (String text : TEXTS) {
            add(new JLabel(text));
        }
    }

    private static void createAndShowGui() {
        StackingLabels mainPanel = new StackingLabels();

        JFrame frame = new JFrame("StackingLabels");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.getContentPane().add(mainPanel);
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGui();
            }
        });
    }
}

要以编程方式强制对命令施加涟漪效果,只需调用forceRippleAnimation(),将您想要的视图作为参数传递。

答案 2 :(得分:7)

首先,您需要从View中获取drawable。

private void forceRippleAnimation(View v, float x, float y){
   Drawable background = v.getBackground();
   if(background instanceof RippleDrawable){
     RippleDrawable ripple = (RippleDrawable)background;
     ripple.setHotspot(x, y);
     ripple.setVisible (true, true);
   }

}

方法setHotspot(x,y);用于设置涟漪动画开始的位置,否则如果未设置,RippleDrawable将占用它所在的Rect(即Rect它被设置为背景的视图)并将从中心开始涟漪效果。

setVisible(true, true)将使drawable可见,最后一个参数将强制动画,无论当前可绘制状态如何。

答案 3 :(得分:7)

以下是Nikola的setHotSpot()和https://stackoverflow.com/a/25415471/1474113

的组合
private void forceRipple(View view, int x, int y) {
    Drawable background = view.getBackground();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && background instanceof RippleDrawable) {
        background.setHotspot(x, y);
    }
    view.setPressed(true);
    // For a quick ripple, you can immediately set false.
    view.setPressed(false);
}

答案 4 :(得分:0)

我使用Luke代码的以下Kotlin变体在将细胞从RecyclerView中刷出时手动显示和隐藏波纹:

fun View.showRipple() {
    if (Build.VERSION.SDK_INT >= 21 && background is RippleDrawable) {
        background.state = intArrayOf(android.R.attr.state_pressed, android.R.attr.state_enabled)
    }
}

fun View.hideRipple() {
    if (Build.VERSION.SDK_INT >= 21 && background is RippleDrawable) {
        background.state = intArrayOf()
    }
}

答案 5 :(得分:0)

谢谢各位!您的回答有助于解决我的问题,但我必须从多个答案中构建解决方案。

我的问题是透明的 RecyclerView 项目(标题)和它后面的按钮。您可能会在图片上看到这种情况:

How application looks

对于点击关注者/喜欢/订阅按钮,我必须为View.OnTouchListener标题添加RecyclerView

class ProfileHeaderHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

    private val clickView: View = itemView.findViewById(R.id.profile_header_view)

    fun bind(callback: View.OnTouchListener) {
        clickView.setOnTouchListener(callback)
    }

    fun unbind() {
        clickView.setOnTouchListener(null)
    }
}

Fragment 内部模拟按钮点击和涟漪效果:

/**
 * [View.performClick] should call inside [onTouch].
 */
override fun onTouch(view: View?, ev: MotionEvent?): Boolean {
    if (ev == null) return true

    val followersContainer = followersContainer ?: return true
    val likeContainer = likeContainer ?: return true
    val subscriptionsContainer = subscriptionsContainer ?: return true
    
    when {
        ev.onView(followersContainer) -> if (followersContainer.isClick(ev)) {
            followersContainer.performClick()
        }
        ev.onView(likeContainer) -> if (likeContainer.isClick(ev)) {
            likeContainer.performClick()
        }
        ev.onView(subscriptionsContainer) -> if (subscriptionsContainer.isClick(ev)) {
            subscriptionsContainer.performClick()
        }
    }

    return true
}

/**
 * [MotionEvent.ACTION_UP] - user make click (remove ripple)
 * [MotionEvent.ACTION_DOWN] - user start click (show ripple)
 * [MotionEvent.ACTION_CANCEL] - user move touch outside of [View] (remove ripple)
 */
private fun View.isClick(ev: MotionEvent): Boolean {        
    when (ev.action) {
        MotionEvent.ACTION_UP -> {
            changeRippleState(ev, isTouch = false)
            return true
        }
        MotionEvent.ACTION_DOWN -> changeRippleState(ev, isTouch = true)
        MotionEvent.ACTION_CANCEL -> changeRippleState(ev, isTouch = false)
    }
    
    return false
}

/**
 * [this] show have android:background with ripple drawable (?attr/selectableItemBackground).
 */
private fun View.changeRippleState(ev: MotionEvent, isTouch: Boolean) {
    val rippleDrawable = background as? RippleDrawable ?: return

    /**
     * For emulate click position.
     */
    rippleDrawable.setHotspot(ev.rawX, ev.rawY)
    isPressed = isTouch
}

我检查了另一种为 RippleDrawable 设置按下状态的方法,它也有效:

rippleDrawable.state = if (isTouch) {
    intArrayOf(android.R.attr.state_pressed, android.R.attr.state_enabled)
} else {
    intArrayOf()
}

检查 MotionEvent 的扩展发生在 View 内部或外部:

fun MotionEvent?.onView(view: View?): Boolean {
    if (view == null || this == null) return false

    return Rect().apply {
        view.getGlobalVisibleRect(this)
    }.contains(rawX.toInt(), rawY.toInt())
}