Demo 下载:
https://github.com/CodingForAndroid/RippleEffect
学习 谷歌 material design的交互设计、向新技术靠拢~
由于谷歌的只有在5.0+才可以有这个效果~ 而我们手头手机大部分还是4.+的、因此自己去实现这个效果、让各种版本的都可以用 无疑是挺好的、
这个Demo 实现了以下几点功能:
①:要实现水波纹效果,首先这个View 必须是可点击的,也就是说clickable :true 才可以触发 比如默认 Button 的Clickable =true,TextView, ImageView =false,但是可以手动设置 true。
②:这个是一个布局、可以包裹 需要实现水波纹效果的 view,任何View 只要是可点击的,都可以包裹进来。
③:可以保证,当手指按下在当前View 上,如果手滑动出了当前View,不会触发该 点击事件, 也就是只有手按下,和手抬起,都是同一个View 才触发 点击事件。
④:这个效果,保证是 水波纹 结束以后,再去响应 View的点击事件 。
⑤:一个布局 包裹这么多View,怎么区分每一个的点击事件呢? 答: 根据 每个View 的id 去响应不同的 事件。
大体就这么多,具体的可以自己尝试,与补充。这个效果参考了 singwhatiwanna 写的、
大体思路 可以去看他的博客、写的很详细、http://blog.csdn.net/singwhatiwanna/article/details/42614953、自己整理 ,为了积累加深一遍。
水波纹效果、就是一圈一圈 向外扩散的圆、实现思路 、就是 以手指触摸的位置为圆心、不断的改变圆的半径、向外画圆、以此达到效果。
而为每一个View 去实现这样一个效果、显然比较费精力,有这样一个布局,可以让其包裹的子View 实现 Ripple 效果,就比较符合我们的需求。
实现思想
首先我们自定义一个layout,这里我们选取LinearLayout,至于原因,文章下面会进行分析。当用户点击一个可点击的元素时,比如button,我们需要得到用户点击的元素的信
息,包含:用户点击了哪个元素、用户点击的那个元素的宽、高、位置信息等。得到了button的信息后,我就可以确定水波纹的范围,然后通过layout进行重绘去绘制水波纹,
这样水波纹效果就实现了,当然,这只是大概步骤,中间还是有一些细节需要处理的。
实现过程
实现过程主要是如下几个问题的解决:
①. 如何得知用户点击了哪个元素
②. 如何取得被点击元素的信息
③. 如何通过layout进行重绘绘制水波纹
④. 如果延迟up事件的分发
如何得知用户点击了哪个元素
这个问题好弄,为了得知用户点击了哪个元素(这个元素一般来说要是可点击的,否则是无意义的),我们要提前拦截所有的点击事件,于是,我们应该重写layout中的
dispatchTouchEvent方法,注意,这里不推荐用onInterceptTouchEvent,因为onInterceptTouchEvent不是一直会被回调的。
然后当用户点击的时候,会有一系列的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);
- }
- }
上面就是实现水波纹的大体步骤,到了这里,我们的布局已经可以让子View 具备水波纹效果了。
如何实现事件的监听
但是 如果 我们要实现点击事件的话,此时当你点击View水波还没有结束,就已经响应了点击事件,所以我们还有最后一步要做,等水波纹绘制结束后,去响应点击事件,如何
控制呢?如果绘制完了,能告诉我们,然后我们再去响应点击事件就容易多了。
按这个思路,我们写一个回调函数,当绘制完成后,告诉我们一声,我们去响应点击事件。
-
- private OnRippleCompleteListener onCompletionListener;
-
- public void setOnRippleCompleteListener(OnRippleCompleteListener listener) {
- this.onCompletionListener = listener;
- }
我们可以仿照我们给View setOnClickListener()的方式 设置一个 完成后的监听。
-
-
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- super.dispatchDraw(canvas);
-
- if (mTargetView == null || !mShouldDoAnimation || mTargetWidth <= 0)
- return;
-
- if (mRevealRadius > mMinBetweenWidthAndHeight /4)
- 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);
-
- if (onCompletionListener != null&&onOneView)
- onCompletionListener.onComplete(mTargetView.getId());
-
- }
-
- }
这样在我们DispatchDraw 方法 ,判断 当圆的半径足够大了,那我们就让我们的监听事件告诉我们,可以相应点击事件了。
当然这里还有一点要注意的,要保证 我们的View 是同一个,如果,你手指按下在id为xxxx1的Button 上,然后滑动到id 为xxxxx2的Button,当然不能响应点击事件,因此我们
加了一个onOneView。
Android交流群:230274309 一起分享,一起进步!少划水,多晒干货!!欢迎大家!!!