自定义Layout实现Android 5.0 Material Design的点击任意View的水波效果

前言

自从Android 5.0问世以后,它的UI风格受到了大家普遍的赞美,简单、动感十足,但是由于工作比较忙,本人对于Android 5.0并没有太多的关注。前几天在知名博主任玉刚 (  博客地址 ) 帅哥的群中有同学问到实现Android 5.0 Material Design中的点击任意View时产生水波的效果,刚哥表示已经实现水波效果,但是需要过段时间才能开出来。刚好本人在昨天写了声波支付的波纹效果,于是今天按照刚哥给出的实现思路弄了一下,于是也就有了今天的文章。可能效果不是很好,分享出来一是自我学习,二也是希望分享一下思路。

从目前的一些实现来看,主要有那么两个实现思路,第一种就是自定义View,比如继承Button,在Button的onDraw里面再动态绘制一层背景,然后改变背景的大小以及颜色,达到动态效果,这种实现使用比较局限,自定义一种类型的View,那么就只有这种View能够产生波纹效果;另一种是自定义布局,然后该布局中只有一个视图,也是同样的方法绘制背景,然后动画,但是也有局限性,就是一个布局中只能放一个视图,只有这个视图能够产生水波效果!

现实的情况是我们需要所有的视图在点击时都产生波纹效果,那么问题就来了,如何实现呢?

代码实现

其实大家的实现思路都是类似的,这是适用性、复杂度的问题。

我的实现思路是自定义一个布局,然后在用户触摸该布局时,通过该触摸点的坐标找到对应的子视图,找到该视图后我们在布局的dispatchDraw函数中裁剪一块区域,并且在这块区域中绘制波纹效果,使得背景图层的半径逐渐增大、透明度逐渐减小。这样点击某个视图时它的上面就产生了一个逐渐变大、颜色变浅的背景图层,不管是任何视图都会有这个动态效果!效果完成之后清除掉背景图层即可。

直接上代码吧。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. /* 
  2.  * The MIT License (MIT) 
  3.  * 
  4.  * Copyright (c) 2015 bboyfeiyu@gmail.com ( mr.simple ) 
  5.  * 
  6.  * Permission is hereby granted, free of charge, to any person obtaining a copy 
  7.  * of this software and associated documentation files (the "Software"), to deal 
  8.  * in the Software without restriction, including without limitation the rights 
  9.  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
  10.  * copies of the Software, and to permit persons to whom the Software is 
  11.  * furnished to do so, subject to the following conditions: 
  12.  * 
  13.  * The above copyright notice and this permission notice shall be included in 
  14.  * all copies or substantial portions of the Software. 
  15.  * 
  16.  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
  17.  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
  18.  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
  19.  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
  20.  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
  21.  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
  22.  * THE SOFTWARE. 
  23.  */  
  24.   
  25. package org.simple;  
  26.   
  27. import android.content.Context;  
  28. import android.content.res.TypedArray;  
  29. import android.graphics.Canvas;  
  30. import android.graphics.Color;  
  31. import android.graphics.Paint;  
  32. import android.graphics.Point;  
  33. import android.graphics.RectF;  
  34. import android.util.AttributeSet;  
  35. import android.view.MotionEvent;  
  36. import android.view.View;  
  37. import android.view.ViewGroup;  
  38. import android.widget.RelativeLayout;  
  39.   
  40. import org.simple.materiallayout.R;  
  41.   
  42. /** 
  43.  * MaterialLayout是模拟Android 5.0中View被点击的波纹效果的布局,与其他的模拟Material 
  44.  * Desigin效果的View不同,所有在MaterialLayout布局下的子视图被点击时都会产生波纹效果,而不是某个特定的View才会有这样的效果. 
  45.  *  
  46.  * @author mrsimple 
  47.  */  
  48. public class MaterialLayout extends RelativeLayout {  
  49.   
  50.     private static final int DEFAULT_RADIUS = 10;  
  51.     private static final int DEFAULT_FRAME_RATE = 10;  
  52.     private static final int DEFAULT_DURATION = 200;  
  53.     private static final int DEFAULT_ALPHA = 255;  
  54.     private static final float DEFAULT_SCALE = 0.8f;  
  55.     private static final int DEFAULT_ALPHA_STEP = 5;  
  56.   
  57.     /** 
  58.      * 动画帧率 
  59.      */  
  60.     private int mFrameRate = DEFAULT_FRAME_RATE;  
  61.     /** 
  62.      * 渐变动画持续时间 
  63.      */  
  64.     private int mDuration = DEFAULT_DURATION;  
  65.     /** 
  66.      *  
  67.      */  
  68.     private Paint mPaint = new Paint();  
  69.     /** 
  70.      * 被点击的视图的中心点 
  71.      */  
  72.     private Point mCenterPoint = null;  
  73.     /** 
  74.      * 视图的Rect 
  75.      */  
  76.     private RectF mTargetRectf;  
  77.     /** 
  78.      * 起始的圆形背景半径 
  79.      */  
  80.     private int mRadius = DEFAULT_RADIUS;  
  81.     /** 
  82.      * 最大的半径 
  83.      */  
  84.     private int mMaxRadius = DEFAULT_RADIUS;  
  85.   
  86.     /** 
  87.      * 渐变的背景色 
  88.      */  
  89.     private int mCirclelColor = Color.LTGRAY;  
  90.     /** 
  91.      * 每次重绘时半径的增幅 
  92.      */  
  93.     private int mRadiusStep = 1;  
  94.     /** 
  95.      * 保存用户设置的alpha值 
  96.      */  
  97.     private int mBackupAlpha;  
  98.   
  99.     /** 
  100.      * 圆形半径针对于被点击视图的缩放比例,默认为0.8 
  101.      */  
  102.     private float mCircleScale = DEFAULT_SCALE;  
  103.     /** 
  104.      * 颜色的alpha值, (0, 255) 
  105.      */  
  106.     private int mColorAlpha = DEFAULT_ALPHA;  
  107.     /** 
  108.      * 每次动画Alpha的渐变递减值 
  109.      */  
  110.     private int mAlphaStep = DEFAULT_ALPHA_STEP;  
  111.   
  112.     private View mTargetView;  
  113.   
  114.     /** 
  115.      * @param context 
  116.      */  
  117.     public MaterialLayout(Context context) {  
  118.         this(context, null);  
  119.     }  
  120.   
  121.     public MaterialLayout(Context context, AttributeSet attrs) {  
  122.         super(context, attrs);  
  123.         init(context, attrs);  
  124.     }  
  125.   
  126.     public MaterialLayout(Context context, AttributeSet attrs, int defStyle) {  
  127.         super(context, attrs, defStyle);  
  128.         init(context, attrs);  
  129.     }  
  130.   
  131.     private void init(Context context, AttributeSet attrs) {  
  132.         if (isInEditMode()) {  
  133.             return;  
  134.         }  
  135.   
  136.         if (attrs != null) {  
  137.             initTypedArray(context, attrs);  
  138.         }  
  139.   
  140.         initPaint();  
  141.   
  142.         this.setWillNotDraw(false);  
  143.         this.setDrawingCacheEnabled(true);  
  144.     }  
  145.   
  146.     private void initTypedArray(Context context, AttributeSet attrs) {  
  147.         final TypedArray typedArray = context.obtainStyledAttributes(attrs,  
  148.                 R.styleable.MaterialLayout);  
  149.         mCirclelColor = typedArray.getColor(R.styleable.MaterialLayout_color, Color.LTGRAY);  
  150.         mDuration = typedArray.getInteger(R.styleable.MaterialLayout_duration,  
  151.                 DEFAULT_DURATION);  
  152.         mFrameRate = typedArray  
  153.                 .getInteger(R.styleable.MaterialLayout_framerate, DEFAULT_FRAME_RATE);  
  154.         mColorAlpha = typedArray.getInteger(R.styleable.MaterialLayout_alpha, DEFAULT_ALPHA);  
  155.         mCircleScale = typedArray.getFloat(R.styleable.MaterialLayout_scale, DEFAULT_SCALE);  
  156.   
  157.         typedArray.recycle();  
  158.   
  159.     }  
  160.   
  161.     private void initPaint() {  
  162.         mPaint.setAntiAlias(true);  
  163.         mPaint.setStyle(Paint.Style.FILL);  
  164.         mPaint.setColor(mCirclelColor);  
  165.         mPaint.setAlpha(mColorAlpha);  
  166.   
  167.         // 备份alpha属性用于动画完成时重置  
  168.         mBackupAlpha = mColorAlpha;  
  169.     }  
  170.   
  171.     /** 
  172.      * 点击的某个坐标点是否在View的内部 
  173.      *  
  174.      * @param touchView 
  175.      * @param x 被点击的x坐标 
  176.      * @param y 被点击的y坐标 
  177.      * @return 如果点击的坐标在该view内则返回true,否则返回false 
  178.      */  
  179.     private boolean isInFrame(View touchView, float x, float y) {  
  180.         initViewRect(touchView);  
  181.         return mTargetRectf.contains(x, y);  
  182.     }  
  183.   
  184.     /** 
  185.      * 获取点中的区域,屏幕绝对坐标值,这个高度值也包含了状态栏和标题栏高度 
  186.      *  
  187.      * @param touchView 
  188.      */  
  189.     private void initViewRect(View touchView) {  
  190.         int[] location = new int[2];  
  191.         touchView.getLocationOnScreen(location);  
  192.         // 视图的区域  
  193.         mTargetRectf = new RectF(location[0], location[1], location[0]  
  194.                 + touchView.getWidth(), location[1] + touchView.getHeight());  
  195.   
  196.     }  
  197.   
  198.     /** 
  199.      * 减去状态栏和标题栏的高度 
  200.      */  
  201.     private void removeExtraHeight() {  
  202.         int[] location = new int[2];  
  203.         this.getLocationOnScreen(location);  
  204.         // 减去两个该布局的top,这个top值就是状态栏的高度  
  205.         mTargetRectf.top -= location[1];  
  206.         mTargetRectf.bottom -= location[1];  
  207.         // 计算中心点坐标  
  208.         int centerHorizontal = (int) (mTargetRectf.left + mTargetRectf.right) / 2;  
  209.         int centerVertical = (int) ((mTargetRectf.top + mTargetRectf.bottom) / 2);  
  210.         // 获取中心点  
  211.         mCenterPoint = new Point(centerHorizontal, centerVertical);  
  212.   
  213.     }  
  214.   
  215.     private View findTargetView(ViewGroup viewGroup, float x, float y) {  
  216.         int childCount = viewGroup.getChildCount();  
  217.         // 迭代查找被点击的目标视图  
  218.         for (int i = 0; i < childCount; i++) {  
  219.             View childView = viewGroup.getChildAt(i);  
  220.             if (childView instanceof ViewGroup) {  
  221.                 return findTargetView((ViewGroup) childView, x, y);  
  222.             } else if (isInFrame(childView, x, y)) { // 否则判断该点是否在该View的frame内  
  223.                 return childView;  
  224.             }  
  225.         }  
  226.   
  227.         return null;  
  228.     }  
  229.   
  230.     private boolean isAnimEnd() {  
  231.         return mRadius >= mMaxRadius;  
  232.     }  
  233.   
  234.     private void calculateMaxRadius(View view) {  
  235.         // 取视图的最长边  
  236.         int maxLength = Math.max(view.getWidth(), view.getHeight());  
  237.         // 计算Ripple圆形的半径  
  238.         mMaxRadius = (int) ((maxLength / 2) * mCircleScale);  
  239.   
  240.         int redrawCount = mDuration / mFrameRate;  
  241.         // 计算每次动画半径的增值  
  242.         mRadiusStep = (mMaxRadius - DEFAULT_RADIUS) / redrawCount;  
  243.         // 计算每次alpha递减的值  
  244.         mAlphaStep = (mColorAlpha - 100) / redrawCount;  
  245.     }  
  246.   
  247.     /** 
  248.      * 处理ACTION_DOWN触摸事件, 注意这里获取的是Raw x, y, 
  249.      * 即屏幕的绝对坐标,但是这个当屏幕中有状态栏和标题栏时就需要去掉这些高度,因此得到mTargetRectf后其高度需要减去该布局的top起点 
  250.      * ,也就是标题栏和状态栏的总高度. 
  251.      *  
  252.      * @param event 
  253.      */  
  254.     private void deliveryTouchDownEvent(MotionEvent event) {  
  255.         if (event.getAction() == MotionEvent.ACTION_DOWN) {  
  256.             mTargetView = findTargetView(this, event.getRawX(), event.getRawY());  
  257.             if (mTargetView != null) {  
  258.                 removeExtraHeight();  
  259.                 // 计算相关数据  
  260.                 calculateMaxRadius(mTargetView);  
  261.                 // 重绘视图  
  262.                 invalidate();  
  263.             }  
  264.         }  
  265.     }  
  266.   
  267.     @Override  
  268.     public boolean onInterceptTouchEvent(MotionEvent event) {  
  269.         deliveryTouchDownEvent(event);  
  270.         return super.onInterceptTouchEvent(event);  
  271.     }  
  272.   
  273.     @Override  
  274.     protected void dispatchDraw(Canvas canvas) {  
  275.         super.dispatchDraw(canvas);  
  276.         // 绘制Circle  
  277.         drawRippleIfNecessary(canvas);  
  278.     }  
  279.   
  280.     private void drawRippleIfNecessary(Canvas canvas) {  
  281.         if (isFoundTouchedSubView()) {  
  282.             // 计算新的半径和alpha值  
  283.             mRadius += mRadiusStep;  
  284.             mColorAlpha -= mAlphaStep;  
  285.   
  286.             // 裁剪一块区域,这块区域就是被点击的View的区域.通过clipRect来获取这块区域,使得绘制操作只能在这个区域范围内的进行,  
  287.             // 即使绘制的内容大于这块区域,那么大于这块区域的绘制内容将不可见. 这样保证了背景层只能绘制在被点击的视图的区域  
  288.             canvas.clipRect(mTargetRectf);  
  289.             mPaint.setAlpha(mColorAlpha);  
  290.             // 绘制背景圆形,也就是  
  291.             canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mPaint);  
  292.         }  
  293.   
  294.         if (isAnimEnd()) {  
  295.             reset();  
  296.         } else {  
  297.             invalidateDelayed();  
  298.         }  
  299.     }  
  300.   
  301.     /** 
  302.      * 发送重绘消息 
  303.      */  
  304.     private void invalidateDelayed() {  
  305.         this.postDelayed(new Runnable() {  
  306.   
  307.             @Override  
  308.             public void run() {  
  309.                 invalidate();  
  310.             }  
  311.         }, mFrameRate);  
  312.     }  
  313.   
  314.     /** 
  315.      * 判断是否找到被点击的子视图 
  316.      *  
  317.      * @return 
  318.      */  
  319.     private boolean isFoundTouchedSubView() {  
  320.         return mCenterPoint != null && mTargetView != null;  
  321.     }  
  322.   
  323.     private void reset() {  
  324.         mCenterPoint = null;  
  325.         mTargetRectf = null;  
  326.         mRadius = DEFAULT_RADIUS;  
  327.         mColorAlpha = mBackupAlpha;  
  328.         mTargetView = null;  
  329.         invalidate();  
  330.     }  
  331.   
  332. }  


自定义的属性, attrs.xml

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <resources>  
  3.   
  4.     <declare-styleable name="MaterialLayout">  
  5.         <attr name="alpha" format="integer" />  
  6.         <attr name="alpha_step" format="integer" />  
  7.         <attr name="framerate" format="integer" />  
  8.         <attr name="duration" format="integer" />  
  9.         <attr name="color" format="color" />  
  10.         <attr name="scale" format="float" />  
  11.     </declare-styleable>  
  12.   
  13. </resources>  


使用示例

引用MaterialLayout工程或者将代码和attrs.xml拷贝到你的工程中,然后在你的布局xml中添加MaterialLayout布局,注意,不要忘了引用MaterialLayout自定义属性的命名空间,即下面的xmlns:ml这句。把com.example.materialdemo替换成你的包名就OK了。
[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. <org.simple.MaterialLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:ml="http://schemas.android.com/apk/res/com.example.materialdemo"  
  3.     android:id="@+id/layout"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="match_parent"  
  6.     android:layout_margin="5dp"  
  7.     android:background="#f0f0f0"  
  8.     android:gravity="center"  
  9.     ml:duration="200"  
  10.     ml:alpha="200"  
  11.     ml:scale="1.2"  
  12.     ml:color="#FFD306" >  
  13.   
  14.     <Button  
  15.         android:id="@+id/my_button"  
  16.         android:layout_width="wrap_content"  
  17.         android:layout_height="wrap_content"  
  18.         android:background="#33CC99"  
  19.         android:padding="10dp"  
  20.         android:text="@string/click"  
  21.         android:textSize="20sp" />  
  22.   
  23.     <ImageView  
  24.         android:id="@+id/my_imageview1"  
  25.         android:layout_width="100dp"  
  26.         android:layout_height="100dp"  
  27.         android:layout_below="@id/my_button"  
  28.         android:layout_marginTop="30dp"  
  29.         android:background="#33CC99"  
  30.         android:contentDescription="@string/app_name"  
  31.         android:padding="10dp"  
  32.         android:src="@drawable/ic_launcher" />  
  33.   
  34. </org.simple.MaterialLayout>  


效果图

这个gif录得有点卡,真机上看起来还是不错的。大家可以到github上clone一份运行看看效果,如果觉得不行也别喷,给出你的github地址,本人也愿意学习您的优秀实现。在这里也期待刚哥早日开源出更好的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值