Android 中实现5.0按钮水波纹反馈效果

自从android L(android 5.0)出来了, 在界面上有了很大的改动,变得扁平化了,很多控件增加了不错的效果,相信大家对view的点击出现会波纹效果都有所体验吧,点击一个view,然后一个水波纹就会从点击处扩散开来。首先,先说下L上的实现,这种波纹效果,L上提供了一种动画,叫做Reveal效果,其底层是通过拿到view的canvas然后不断刷新view来完成的,这种效果需要view的支持,而在低版本上没有view的支持,因此,Reveal效果没法直接在低版本运行。但是,我们了解其效果、其原理后,还是可以通过模拟的方式去实现这种效果,平心而论,写出一个具有波纹效果的自定义view不难,或者说很简单,但是,view的子类很多,如果要一一去实现button、edit等控件,这样比较繁琐,于是,我们想是否有更简单的方式呢?其实是有的,我们可以写一个自定义的layout,然后让layout中所有可点击的元素都具有波纹效果,这样做,就大大简化了整个过程。接下来本文就会分析这个layout的实现。

效果:


上面的机器 有些卡顿。大概效果就是这样了!


实现过程

实现过程主要是如下几个问题的解决:

1. 如何得知用户点击了哪个元素

2. 如何取得被点击元素的信息

3. 如何通过layout进行重绘绘制水波纹

4. 如果延迟up事件的分发

下面一一进行分析

如何得知用户点击了哪个元素

这个问题好弄,为了得知用户点击了哪个元素(这个元素一般来说要是可点击的,否则是无意义的),我们要提前拦截所有的点击事件,于是,我们应该重写layout中的dispatchTouchEvent方法,注意,这里不推荐用onInterceptTouchEvent,因为onInterceptTouchEvent不是一直会被回调的,具体原因请参看我之前写的view系统解析系列。然后当用户点击的时候,会有一系列的down、move、up事件,我们要在down的时候来确定事件落在哪个元素上,down的元素就是用户点击的元素,当然为了严谨,我们还要判断up的时候是否也落在同一个元素上面,因为,系统click事件的判断规则就是:down和up同时落在同一个可点击的元素上。

 @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            View touchTarget = getTouchTarget(this, x, y);
            if (touchTarget.isClickable() && touchTarget.isEnabled()) {
                mTouchTarget = touchTarget;
                initParametersForChild(event, touchTarget);
                postInvalidateDelayed(INVALIDATE_DURATION);
            }
        } else if (action == MotionEvent.ACTION_UP) {
            mIsPressed = false;
            postInvalidateDelayed(INVALIDATE_DURATION);
            mDispatchUpTouchEventRunnable.event = event;
            postDelayed(mDispatchUpTouchEventRunnable, 400);
            return true;
        } else if (action == MotionEvent.ACTION_CANCEL) {
            mIsPressed = false;
            postInvalidateDelayed(INVALIDATE_DURATION);
        }

        return super.dispatchTouchEvent(event);
    }


通过上述代码,我们可以知道,当down的时候,我们取出点击事件的屏幕坐标,然后去遍历view树找到用户所点击的那个view,代码如下,就是判断事件的坐标是否落在view的范围内,这个不再多说了,比较好理解。需要注意的是,事件的坐标我们不能用getX和getY,而要用getRawX和getRawY,二者的区别是:前者是相对于被点击view的坐标,后者是相对于屏幕的坐标,而我们的目标view具体位于layout的哪一层我们无法知道,所以,必须用屏幕的绝对坐标来进行计算。而有了事件的坐标,再根据view在屏幕中的绝对坐标,只要判断事件的xy是否落在view的上下左右四个角之内,就可以知道事件是否落在view上,从而取出用户所点击的那个view。

private View getTouchTarget(View view, int x, int y) {
        View target = null;
        ArrayList<View> TouchableViews = view.getTouchables();
        for (View child : TouchableViews) {
            if (isTouchPointInView(child, x, y)) {
                target = child;
                break;
            }
        }

        return target;
    }

    private boolean isTouchPointInView(View view, int x, int y) {
        int[] location = new int[2];
        view.getLocationOnScreen(location);
        int left = location[0];
        int top = location[1];
        int right = left + view.getMeasuredWidth();
        int bottom = top + view.getMeasuredHeight();
        if (view.isClickable() && y >= top && y <= bottom
                && x >= left && x <= right) {
            return true;
        }
        return false;
    }

如何取得被点击元素的信息

这个比较简单,被点击元素的信息有:宽、高、left、top、right、bottom,获取它们的代码如下:

       int[] location = new int[2];
        mTouchTarget.getLocationOnScreen(location);
        int left = location[0] - mLocationInScreen[0];
        int top = location[1] - mLocationInScreen[1];
        int right = left + mTouchTarget.getMeasuredWidth();
        int bottom = top + mTouchTarget.getMeasuredHeight();


说明:mTouchTarget指的是用户点击的那个view

如何通过layout进行重绘绘制水波纹

这个会水波纹比较简单,只要用drawCircle绘制一个半透明的圆环即可,这里主要说下绘制时机。一般来说,我们会选择在onDraw中去进行绘制,这是没错的,但是对于L中的效果不太适合,查看view的绘制过程,我们会明白,view的绘制大致遵循如下流程:先绘制背景,再绘制自己(onDraw),接着绘制子元素(dispatchDraw),最后绘制一些装饰等比如滚动条(onDrawScrollBars),因此,如果我们在onDraw中绘制波纹,那么由于子元素的绘制在onDraw之后,就会导致子元素盖住我们所绘制的圆环,这样,圆环就有可能看不全了,因为,把我绘制的时机很重要。根据view的绘制流程,我们选择dispatchDraw比较合适,当所有的子元素都绘制完成后,再进行波纹的绘制。读到这里,大家会更加明白,为什么我们要选择LinearLayout以及为什么不建议view的嵌套层级太深,因为如果view本身比较重或者嵌套层级太深,就会导致dispatchDraw执行的耗时增加,这样水波的绘制就会收到些许影响。因此,性能的平滑在代码中也很重要,也是需要考虑的。同时,为了不让绘制的圆环超出被点击元素的范围,我们需要对canvas进行clip。为了有波纹效果,我们需要频繁地进行layout重绘,并且在重绘的过程中改变圆环的半径,这样一个动态的水波纹就出来了。仍然,我来性能的考虑,我们选择用postInvalidateDelayed(long delayMilliseconds, int left, int top, int right, int bottom)来进行view的部分重绘,因为,其他区域是不需要重绘的,仅仅是被点击的元素所在的区域需要重绘。为什么要采用Delayed这个方法,原因是我们不能一直进行刷新,必须有一点点时间间隔,这样做的好处是:避免view的重绘抢占过多时间片从而造成潜在的间接栈溢出,因为invalidate会直接导致draw的调用。

具体代码如下:

protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (!mShouldDoAnimation || mTargetWidth <= 0 || mTouchTarget == null) {
            return;
        }

        if (mRevealRadius > mMinBetweenWidthAndHeight / 2) {
            mRevealRadius += mRevealRadiusGap * 4;
        } else {
            mRevealRadius += mRevealRadiusGap;
        }
        int[] location = new int[2];
        mTouchTarget.getLocationOnScreen(location);
        int left = location[0] - mLocationInScreen[0];
        int top = location[1] - mLocationInScreen[1];
        int right = left + mTouchTarget.getMeasuredWidth();
        int bottom = top + mTouchTarget.getMeasuredHeight();

        canvas.save();
        canvas.clipRect(left, top, right, bottom);
        canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint);
        canvas.restore();

        if (mRevealRadius <= mMaxRevealRadius) {
            postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
        } else if (!mIsPressed) {
            mShouldDoAnimation = false;
            postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
        }
    }


到此为止,这个layout我们已经实现了,但是细心的你,一定会发现,还有什么不妥的地方。比如,你可以给button加一个点击事件,当button被点击的时候起一个activity,很快你就会发现问题所在了:水波还没播完呢,activity就起来了,导致水波效果大打折扣,而仔细观察android L的效果,我们发现,L中总是要等到水波效果播放完毕才会进行下一步的行为。所以,最后一个待解决的问题也就出来了,请看下面的分析

如何延迟up事件的分发

针对上面所说的问题,如果我们能够延迟up时间的分发,比如延迟400ms,这样水波就有足够的时间去播放完毕,然后再分发up事件,这样就可以解决问题。最开始,我的确是这样做的,先看如下的代码:

else if (action == MotionEvent.ACTION_UP) {
            mIsPressed = false;
            postInvalidateDelayed(INVALIDATE_DURATION);
            mDispatchUpTouchEventRunnable.event = event;
            postDelayed(mDispatchUpTouchEventRunnable, 400);
            return true;
 }

可以发现,当up的时候,我并没有直接走系统的分发流程,只是强行消耗点up事件然后再延迟分发,请看代码:

 private class DispatchUpTouchEventRunnable implements Runnable {
        public MotionEvent event;

        @Override
        public void run() {
            if (mTouchTarget == null || !mTouchTarget.isEnabled()) {
                return;
            }

            if (isTouchPointInView(mTouchTarget, (int)event.getRawX(), (int)event.getRawY())) {
                mTouchTarget.dispatchTouchEvent(event);
            }
        }
    };


--------------------------------------------------------------------------------------------------------------------------------------------------------

这个类的全代码如下:

package com.android.xiho;  
  
import java.util.ArrayList;  
  
import com.example.myreveallayout.R;  
  
import android.annotation.TargetApi;  
import android.content.Context;  
import android.graphics.Canvas;  
import android.graphics.Paint;  
import android.os.Build;  
import android.util.AttributeSet;  
import android.view.MotionEvent;  
import android.view.View;  
import android.widget.LinearLayout;  
  
public class RevealLayout extends LinearLayout implements Runnable{  
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
      
    private float mCenterX,mCenterY;  
      
    private int[] mLocation = new int[2];  
      
    private int INVALIDATE_DURATION = 100;  
    private int mTargetHeight,mTargetWidth;  
    private int mRevealRadius = 0,mRevealRadiusGap,mMaxRadius;  
    private int mMinBetweenWidthAndHeight,mMaxBetweenWidthAndHeight;  
      
    private boolean mIsPressed;  
    private boolean mShouldDoAnimation;  
      
    private View mTargetView;  
    private DispatchUpTouchEventRunnable mDispatchUpTouchEventRunnable = new DispatchUpTouchEventRunnable();  
      
    public RevealLayout(Context context) {  
        super(context);  
        init();  
    }  
      
    public RevealLayout(Context context, AttributeSet attrs){  
        super(context,attrs);  
        init();  
    }  
      
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)  
    public RevealLayout(Context context, AttributeSet attrs, int defStyleAttr){  
        super(context,attrs,defStyleAttr);  
        init();  
    }  
  
    public void init(){  
        setWillNotDraw(false);  
        mPaint.setColor(getResources().getColor(R.color.reveal_color));  
    }  
      
    @Override  
    protected void onLayout(boolean changed, int l, int t, int r, int b) {  
        super.onLayout(changed, l, t, r, b);  
        this.getLocationOnScreen(mLocation);  
    }  
      
    @Override  
    protected void dispatchDraw(Canvas canvas) {  
        super.dispatchDraw(canvas);  
          
        if(mTargetView == null || !mShouldDoAnimation || mTargetWidth <= 0)  
            return;  
          
        if(mRevealRadius > mMinBetweenWidthAndHeight / 2)  
            mRevealRadius += mRevealRadiusGap * 4;  
        else  
            mRevealRadius += mRevealRadiusGap;  
          
        int[] location = new int[2];  
        this.getLocationOnScreen(mLocation);  
        mTargetView.getLocationOnScreen(location);  
  
        int top = location[1] - mLocation[1];  
        int left = location[0] - mLocation[0];  
        int right = left + mTargetView.getMeasuredWidth();  
        int bottom = top + mTargetView.getMeasuredHeight();  
          
        canvas.save();  
        canvas.clipRect(left, top, right, bottom);  
        canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint);  
        canvas.restore();  
          
        if(mRevealRadius <= mMaxRadius)  
            postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);  
        else if(!mIsPressed){  
            mShouldDoAnimation = false;  
            postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);  
        }  
    }  	
    @Override  
	//当手触摸的时候的点击事件
    public boolean dispatchTouchEvent(MotionEvent event) {  
        int x = (int)event.getRawX();  
        int y = (int)event.getRawY();  
        int action = event.getAction();  
        switch(action){

	    //当手按下的时候
	     case MotionEvent.ACTION_DOWN:  
                View targetView = getTargetView(this,x,y);  
                  
                if(targetView != null && targetView.isEnabled()){  
                    mTargetView = targetView;  
                    initParametersForChild(event,targetView);  
                    postInvalidateDelayed(INVALIDATE_DURATION);  
                }  
                break;  
            //当手弹起的时候     
            case MotionEvent.ACTION_UP:  
                mIsPressed = false;  
                postInvalidateDelayed(INVALIDATE_DURATION);  
                mDispatchUpTouchEventRunnable.event = event;  
                postDelayed(mDispatchUpTouchEventRunnable, 40);  
                break;  
            //当手离开的时候  
            case MotionEvent.ACTION_CANCEL:  
                mIsPressed = false;  
                postInvalidateDelayed(INVALIDATE_DURATION);  
                break;  
        }         
        return super.dispatchTouchEvent(event);  
    }  
      
    public View getTargetView(View view,int x,int y){  
        View target = null;  
        ArrayList<View> views = view.getTouchables();  
          
        for(View child : views)  
            if(isTouchPointInView(child,x,y)){  
                target = child;  
                break;  
            }  
        return target;  
    }  
      
    public boolean isTouchPointInView(View child,int x,int y){  
        int[] location = new int[2];  
        child.getLocationOnScreen(location);  
  
        int top = location[1];  
        int left = location[0];  
        int right = left + child.getMeasuredWidth();  
        int bottom = top + child.getMeasuredHeight();  
          
        if(child.isClickable() && y>=top && y<= bottom && x >= left && x<= right)  
            return true;  
        else  
            return false;  
    }  
      
    public void initParametersForChild(MotionEvent event,View view){  
        mCenterX = event.getX();  
        mCenterY = event.getY();  
        mTargetWidth = view.getMeasuredWidth();  
        mTargetHeight = view.getMeasuredHeight();  
        mMinBetweenWidthAndHeight = Math.min(mTargetWidth, mTargetHeight);  
        mMaxBetweenWidthAndHeight = Math.max(mTargetWidth, mTargetHeight);  
  
        mRevealRadius = 0;  
        mRevealRadiusGap = mMinBetweenWidthAndHeight / 8;  
  
        mIsPressed = true;  
        mShouldDoAnimation = true;  
          
        int[] location = new int[2];  
        view.getLocationOnScreen(location);  
          
        int left = location[0] - mLocation[0];  
        int mTransformedCenterX = (int)mCenterX - left;  
        mMaxRadius = Math.max(mTransformedCenterX, mTargetWidth - mTransformedCenterX);  
    }  
      
    @Override  
    public void run() {  
        super.performClick();  
    }  
  
    @Override  
    public boolean performClick() {  
        postDelayed(this,40);  
        return true;  
    }  
    private class DispatchUpTouchEventRunnable implements Runnable{  
        public MotionEvent event;  
          
        @Override  
        public void run() {  
            if(mTargetView.isEnabled() && mTargetView.isClickable())  
                return;  
              
            if(isTouchPointInView(mTargetView, (int)event.getRawX(), (int)event.getRawX()))  
                mTargetView.performClick();  
        }  
    }  
}  



布局代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    android:background="#ffffff"  
    android:orientation="vertical"  
     >  
  
    <com.android.xiho.RevealLayout  
        android:id="@+id/layout1"  
        android:layout_width="fill_parent"  
        android:layout_height="wrap_content" >  
        <Button   
            android:id="@+id/button"  
            android:layout_width="fill_parent"  
            android:layout_height="fill_parent"  
            android:background="#0ac39e"  
            android:text="Button"  
            android:textColor="#ffffff"  
            android:enabled="true"/>  
    </com.android.chaos.RevealLayout>  
      
</LinearLayout> 


这样我们就可以实现水波纹点击效果了。试试吧~


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值