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

Posted

技术标签:

【中文标题】如何在视图内的特定位置触发 Android Lollipop 的涟漪效应,而不触发触摸事件?【英文标题】:How to trigger ripple effect on Android Lollipop, in specific location within the view, without triggering touches events? 【发布时间】:2015-01-29 06:12:59 【问题描述】:

这是一个简短的问题:

假设我有一个ViewRippleDrawable 作为背景。

有没有一种简单的方法可以在不触发任何触摸或点击事件的情况下从特定位置触发波纹?

【问题讨论】:

【参考方案1】:

是的,有!为了以编程方式触发涟漪,您必须使用setState() 设置RippleDrawable 的状态。调用 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;

如您所见,如果启用和按下属性都设置了,那么波纹和背景都将被激活并显示波纹。 此外,只要您设置焦点状态,背景也会被激活。有了这个,你可以触发波纹并让背景独立改变颜色。

有兴趣可以查看RippleDrawablehere的完整源码。

【讨论】:

说,假设我有一个视图,我已经在其上处理了OnTouchListener 来处理拖动,并且我已经通过使用GestureDetectorCompatonSingleTapUp 在其@987654338 上被调用来识别单击@。出于某种原因,使用此处的任何解决方案来触发涟漪效果都不起作用,我什至将它作为前景而不是背景进行了尝试,这意味着父视图 (FrameLayout) 上的 android:foreground="?attr/actionBarItemBackground" 。知道为什么,我该怎么做? @androiddeveloper 我没有你的场景,但我有一个 onTouch,我想触发rippleDrawable。调用rippleDrawable.setState(new int[] android.R.attr.state_pressed, android.R.attr.state_enabled );在 ACTION_DOWN 上并调用rippleDrawable.setState(new int[] );在 ACTION_UP 上有效。此外,您使用的可绘制波纹可能没有使用默认状态,需要一组不同的状态来触发它。 遗憾的是很久以前我问过这个问题。对不起,谢谢。如果我再次回到这种情况,我可能会再次检查一下【参考方案2】:

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

protected void forceRippleAnimation(View view)

    Drawable background = view.getBackground();

    if(Build.VERSION.SDK_INT >= 21 && background instanceof RippleDrawable)
    
        final RippleDrawable rippleDrawable = (RippleDrawable) background;

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

        Handler handler = new Handler();

        handler.postDelayed(new Runnable()
        
            @Override public void run()
            
                rippleDrawable.setState(new int[]);
            
        , 200);
    

要以编程方式强制命令产生涟漪效果,只需调用 forceRippleAnimation(),将要产生涟漪的 View 作为参数传递。

【讨论】:

更改为 if(Build.VERSION.SDK_INT >= 21 && background instanceof RippleDrawable),因为 android 崩溃 @iscariot。好决定。刚刚改了。 将状态重置为当前值调用rippleDrawable.setState(view.getDrawableState()),而不是强制设置空状态。【参考方案3】:

这里是 Nikola 的 setHotSpot() 和 https://***.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】:

首先,您需要从视图中获取可绘制对象。

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(即设置它的View的Rect作为背景),将从中心开始产生涟漪效果。

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

【讨论】:

不错。动画是像单击还是长按(意味着选择将保留)? 您应该尝试以编程方式设置可绘制状态。喜欢:int[] state = new int[] android.R.attr.state_pressed ripple.setState(state);。我还没有测试你是否应该将它与setVisible(true,true) 结合起来。因为可绘制对象会在状态更改时自动更新/动画。 我不明白。能否请您发布两种行为的示例? @NikolaDespotoski 调用 setVisible() 实际上不会触发涟漪。结果你根本不需要打电话给setVisible()。您只需将状态设置为按下和启用。详情请看我的回答!【参考方案5】:

当从 RecyclerView 中滑出单元格时,我使用以下卢克代码的 Kotlin 变体手动显示和隐藏波纹:

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()
    

【讨论】:

【参考方案6】:

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

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

对于点击 followers/liked/subscriptions 按钮,我必须为 RecyclerView 添加 View.OnTouchListener 标题:

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())

【讨论】:

以上是关于如何在视图内的特定位置触发 Android Lollipop 的涟漪效应,而不触发触摸事件?的主要内容,如果未能解决你的问题,请参考以下文章

我应该如何获得ANDROID中动态视图的位置

如何在Android中的特定位置绘制按钮?

您可以刷新触发器内的物化视图吗?甲骨文 11g

如何禁用特定视图内的滚动?

使用ConstraintLayout将水平回收视图放置在特定位置

Xcode MapKit:如何在特定位置输入时触发事件[关闭]