博主参加了2014 CSDN博客之星评选,帮我投一票吧。html
点击给我投票
java
前段时间android L(android 5.0)出来了,界面上作了一些改动,主要是添加了若干动画和一些新的控件,相信你们对view的点击效果-水波纹颇有印象吧,点击一个view,而后一个水波纹就会从点击处扩散开来,本文就来分析这种效果的实现。首先,先说下L上的实现,这种波纹效果,L上提供了一种动画,叫作Reveal效果,其底层是经过拿到view的canvas而后不断刷新view来完成的,这种效果须要view的支持,而在低版本上没有view的支持,所以,Reveal效果无法直接在低版本运行。可是,咱们了解其效果、其原理后,仍是能够经过模拟的方式去实现这种效果,平心而论,写出一个具备波纹效果的自定义view不难,或者说很简单,可是,view的子类不少,若是要一一去实现button、edit等控件,这样比较繁琐,因而,咱们想是否有更简单的方式呢?实际上是有的,咱们能够写一个自定义的layout,而后让layout中全部可点击的元素都具备波纹效果,这样作,就大大简化了整个过程。接下来本文就会分析这个layout的实现,在此以前,咱们先看下效果。android
首先咱们自定义一个layout,这里咱们选取LinearLayout,至于缘由,文章下面会进行分析。当用户点击一个可点击的元素时,好比button,咱们须要获得用户点击的元素的信息,包含:用户点击了哪一个元素、用户点击的那个元素的宽、高、位置信息等。获得了button的信息后,我就能够肯定水波纹的范围,而后经过layout进行重绘去绘制水波纹,这样水波纹效果就实现了,固然,这只是大概步骤,中间仍是有一些细节须要处理的。canvas
既然咱们打算实现一个自定义layout,那咱们要选取那个layout呢,LinearLayout、RelativeLayout、FrameLayout?我这里选用LinearLayout。为何呢?也许有人会问,不该该用RelativeLayout吗?由于RelativeLayout比较强大,能够实现复杂的布局,但LinearLayout和FrameLayout就不行。没错,RelativeLayout是强大,可是考虑到水波效果是经过频繁刷新layout来实现的,因为频繁重绘,所以,咱们要考虑性能问题,RelativeLayout的性能是最差的(由于作的事情多),由于,为了性能,咱们选择LinearLayout,至于FrameLayout,它功能太简单了,不太适合使用。当实现复杂布局的时候,咱们能够在具备波纹效果的元素外部包裹LinearLayout,这样重绘的时候不至于有太重的任务。ide
根据上面的分析,咱们定义以下的layout:布局
public class RevealLayout extends LinearLayout implements Runnable
post
实现过程主要是以下几个问题的解决:性能
1. 如何得知用户点击了哪一个元素动画
2. 如何取得被点击元素的信息this
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; }
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
这个会水波纹比较简单,只要用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时间的分发,好比延迟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); } } };
到此为止,上述几个问题都已经分析完毕了,咱们就能够轻易地实现水波纹的点击效果了。
本文中的demo源码暂时未开放到互联网上,请加群 215680213 ,在群共享中下载源码。