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

实现思想

首先我们自定义一个layout,这里我们选取LinearLayout,至于原因,文章下面会进行分析。当用户点击一个可点击的元素时,比如button,我们需要得到用户点击的元素的信息,包含:用户点击了哪个元素、用户点击的那个元素的宽、高、位置信息等。得到了button的信息后,我就可以确定水波纹的范围,然后通过layout进行重绘去绘制水波纹,这样水波纹效果就实现了,当然,这只是大概步骤,中间还是有一些细节需要处理的。

layout的选取

既然我们打算实现一个自定义layout,那我们要选取那个layout呢,LinearLayout、RelativeLayout、FrameLayout?我这里选用LinearLayout。为什么呢?也许有人会问,不应该用RelativeLayout吗?因为RelativeLayout比较强大,可以实现复杂的布局,但LinearLayout和FrameLayout就不行。没错,RelativeLayout是强大,但是考虑到水波效果是通过频繁刷新layout来实现的,由于频繁重绘,因此,我们要考虑性能问题,RelativeLayout的性能是最差的(因为做的事情多),因为,为了性能,我们选择LinearLayout,至于FrameLayout,它功能太简单了,不太适合使用。当实现复杂布局的时候,我们可以在具有波纹效果的元素外部包裹LinearLayout,这样重绘的时候不至于有过重的任务。

根据上面的分析,我们定义如下的layout:

public class RevealLayout extends LinearLayout implements Runnable

实现过程

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

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

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

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

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

下面一一进行分析

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

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

[java] view plain copy
  1. @Override
  2. public boolean dispatchTouchEvent(MotionEvent event) {
  3. int x = (int) event.getRawX();
  4. int y = (int) event.getRawY();
  5. int action = event.getAction();
  6. if (action == MotionEvent.ACTION_DOWN) {
  7. View touchTarget = getTouchTarget(this, x, y);
  8. if (touchTarget.isClickable() && touchTarget.isEnabled()) {
  9. mTouchTarget = touchTarget;
  10. initParametersForChild(event, touchTarget);
  11. postInvalidateDelayed(INVALIDATE_DURATION);
  12. }
  13. } else if (action == MotionEvent.ACTION_UP) {
  14. mIsPressed = false;
  15. postInvalidateDelayed(INVALIDATE_DURATION);
  16. mDispatchUpTouchEventRunnable.event = event;
  17. postDelayed(mDispatchUpTouchEventRunnable, 400);
  18. return true;
  19. } else if (action == MotionEvent.ACTION_CANCEL) {
  20. mIsPressed = false;
  21. postInvalidateDelayed(INVALIDATE_DURATION);
  22. }
  23. return super.dispatchTouchEvent(event);
  24. }

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

[html] view plain copy
  1. private View getTouchTarget(View view, int x, int y) {
  2. View target = null;
  3. ArrayList<View> TouchableViews = view.getTouchables();
  4. for (View child : TouchableViews) {
  5. if (isTouchPointInView(child, x, y)) {
  6. target = child;
  7. break;
  8. }
  9. }
  10. return target;
  11. }
  12. private boolean isTouchPointInView(View view, int x, int y) {
  13. int[] location = new int[2];
  14. view.getLocationOnScreen(location);
  15. int left = location[0];
  16. int top = location[1];
  17. int right = left + view.getMeasuredWidth();
  18. int bottom = top + view.getMeasuredHeight();
  19. if (view.isClickable() && y >= top && y <= bottom
  20. && x >= left && x <= right) {
  21. return true;
  22. }
  23. return false;
  24. }

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

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

[java] view plain copy
  1. int[] location = new int[2];
  2. mTouchTarget.getLocationOnScreen(location);
  3. int left = location[0] - mLocationInScreen[0];
  4. int top = location[1] - mLocationInScreen[1];
  5. int right = left + mTouchTarget.getMeasuredWidth();
  6. 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的调用。

具体代码如下:

[java] view plain copy
  1. protected void dispatchDraw(Canvas canvas) {
  2. super.dispatchDraw(canvas);
  3. if (!mShouldDoAnimation || mTargetWidth <= 0 || mTouchTarget == null) {
  4. return;
  5. }
  6. if (mRevealRadius > mMinBetweenWidthAndHeight / 2) {
  7. mRevealRadius += mRevealRadiusGap * 4;
  8. } else {
  9. mRevealRadius += mRevealRadiusGap;
  10. }
  11. int[] location = new int[2];
  12. mTouchTarget.getLocationOnScreen(location);
  13. int left = location[0] - mLocationInScreen[0];
  14. int top = location[1] - mLocationInScreen[1];
  15. int right = left + mTouchTarget.getMeasuredWidth();
  16. int bottom = top + mTouchTarget.getMeasuredHeight();
  17. canvas.save();
  18. canvas.clipRect(left, top, right, bottom);
  19. canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint);
  20. canvas.restore();
  21. if (mRevealRadius <= mMaxRevealRadius) {
  22. postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
  23. } else if (!mIsPressed) {
  24. mShouldDoAnimation = false;
  25. postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
  26. }
  27. }

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

如何延迟up事件的分发

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

[java] view plain copy
  1. else if (action == MotionEvent.ACTION_UP) {
  2. mIsPressed = false;
  3. postInvalidateDelayed(INVALIDATE_DURATION);
  4. mDispatchUpTouchEventRunnable.event = event;
  5. postDelayed(mDispatchUpTouchEventRunnable, 400);
  6. return true;
  7. }

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

[java] view plain copy
  1. private class DispatchUpTouchEventRunnable implements Runnable {
  2. public MotionEvent event;
  3. @Override
  4. public void run() {
  5. if (mTouchTarget == null || !mTouchTarget.isEnabled()) {
  6. return;
  7. }
  8. if (isTouchPointInView(mTouchTarget, (int)event.getRawX(), (int)event.getRawY())) {
  9. mTouchTarget.dispatchTouchEvent(event);
  10. }
  11. }
  12. };

到此为止,上述几个问题都已经分析完毕了,我们就可以轻易地实现水波纹的点击效果了。

水波纹点击效果的实现相关推荐

  1. Android 水波纹点击效果(Ripple Effect)

    本文介绍的是Android5.0中其中一个炫酷的效果,点击水波纹扩散效果(Ripple Effect). 以下介绍的实现方式都是调用Android5.0的新API,并非自定义实现,所以只支持在Andr ...

  2. android l 效果,[原]Android L中水波纹点击效果的实现

    博主参加了2014 CSDN博客之星评选,帮我投一票吧. 前言 前段时间android L(android 5.0)出来了,界面上做了一些改动,主要是添加了若干动画和一些新的控件,相信大家对view的 ...

  3. css 点击效果_使用CSS实现逼真的水波纹点击效果

    这篇文章特别介绍如何使用CSS来完成水波纹的效果. div的层层叠叠 虽然webkit具有遮罩的能力(webkit mask),不过webkit虽然强大,但在跨浏览器上总是它的罩门,况且在性能上也是往 ...

  4. Android L中水波纹点击效果的实现

    博主参加了2014 CSDN博客之星评选,帮我投一票吧. 点击给我投票 前言 前段时间android L(android 5.0)出来了,界面上做了一些改动,主要是添加了若干动画和一些新的控件,相信大 ...

  5. 点击水波纹效果html5,使用CSS实现逼真的水波纹点击效果

    这篇文章特别介绍如何使用CSS来完成水波纹的效果. div的层层叠叠 虽然webkit具有遮罩的能力(webkit mask),不过webkit虽然强大,但在跨浏览器上总是它的罩门,况且在性能上也是往 ...

  6. android 自定义水波纹点击效果Button

    welcome 效果 ; 技术基础思路 自定义 Button 自定义 Drawable 项目源码 点击查看详情 自定义button 其实这只是一些说法 自定义button,我们只需要将子类继承 but ...

  7. Flutter中为图片设置波纹点击效果

    题记 -- 执剑天涯,从你的点滴积累开始,所及之处,必精益求精,即是折腾每一天. 重要消息 网易云[玩转大前端]配套课程 EDU配套 教程 Flutter开发的点滴积累系列文章 为图片添加水波纹点击效 ...

  8. android自定义控件几种,Android 自定义View一个控件搞定多种水波纹涟漪扩散效果 - CSDN博客...

    效果图 实现思路 这个效果实现起来并不难,重要的是思路 此View满足了多种水波纹涟漪扩散效果,这要求它能满足很多的变化 根据上面的样式,可以看出此View需要满足以下变化 圆圈从中心可循环向外扩散 ...

  9. android水波纹点击动画,android 控件点击水波纹效果的几种方案

    目前我所知道的至少有三种可以实现点击水波纹的效果 第一种:安卓自带的方法 在安卓中有自带的一种属性,可以实现水波纹的效果,就是在所需要点击的控件属性加上如下代码: android:background ...

最新文章

  1. 深度学习学习指南-工具篇
  2. IBM苏中:怎样利用深度学习、增强学习等方法提高信息处理效率
  3. No identifier specified for entity
  4. 应收应付重组配置和操作解析
  5. 记计算机三级网络技术考试经历(附题库)
  6. SAP C4C url Mashup的跳转工作原理 - 新的浏览器窗口是如何打开的
  7. Spring范围代理
  8. 歌谣带你看java面试题
  9. 警方通报李彦宏被泼水事件进展:嫌疑人被行政拘留5日
  10. Java基础学习总结(159)——JDK15 正式发布了!新增14个新特性
  11. 创业冲突的五种解决方法是_当创始合伙人发生冲突时,最好的解决方法4和5
  12. git远程仓库中master及其余分支间代码的合并
  13. MULTISIM安装下载
  14. 计算机exsl表f4代表锁定,Excel中F4技巧,相对引用、绝对引用和混合引用
  15. 关于关于_WIN32_WINNT的说明
  16. spark on hive 的部署,和spark on hive (ha)在本地测试步骤
  17. 苹果xr黑屏转圈圈解决方法_苹果iPhone XR升级iOS 12.3后黑屏转圈圈怎么办?附解决办法...
  18. 百度SEO和谷歌SEO有什么区别?
  19. [转]杂谈如何绕过WAF(Web应用防火墙)
  20. 关于JavaScript中的空格。

热门文章

  1. 无线传感器网络—拓扑管理
  2. 项目实训- 基于unity的2D多人乱斗闯关游戏设计与开发(十1、FPS多人化——IK)
  3. 2020年“泰迪杯”数据分析职业技能大赛B题疫情数据分析
  4. Thrift功能简介
  5. 神经网络训练是什么意思,神经网络训练准确率
  6. 质疑贴——对《新版微软一站式示例代码库》中的一个示例的质疑
  7. JavaScript断点调试与console.log(..)输出不一致
  8. 前端移动端web开发(一)
  9. 互联网原型设计,选择原型工具OR手绘?
  10. ROS2机器人笔记20-08-18