实现Android 5 0 Material Design的点击任意View的水波效果

本文介绍了如何实现Android 5.0 Material Design中的点击任意View时的水波效果。作者提供了自定义布局的实现思路,通过在用户触摸布局时找到对应子视图并绘制波纹效果,使背景图层半径增大、透明度减小,从而在任何视图上产生动态效果。文章包含代码示例和实际效果展示。
摘要由CSDN通过智能技术生成
               

    转载请注明出处,本文来自【 Mr.Simple的博客 】

 我正在参加博客之星,点击这里投我一票吧,谢谢~   

前言

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

        

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

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

 

代码实现

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

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

 直接上代码吧。

/* * The MIT License (MIT) * * Copyright (c) 2015 bboyfeiyu@gmail.com ( mr.simple ) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */package org.simple;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Point;import android.graphics.RectF;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.view.ViewGroup;import android.widget.RelativeLayout;import org.simple.materiallayout.R;/** * MaterialLayout是模拟Android 5.0中View被点击的波纹效果的布局,与其他的模拟Material * Desigin效果的View不同,所有在MaterialLayout布局下的子视图被点击时都会产生波纹效果,而不是某个特定的View才会有这样的效果. *  * @author mrsimple */public class MaterialLayout extends RelativeLayout {    private static final int DEFAULT_RADIUS = 10;    private static final int DEFAULT_FRAME_RATE = 10;    private static final int DEFAULT_DURATION = 200;    private static final int DEFAULT_ALPHA = 255;    private static final float DEFAULT_SCALE = 0.8f;    private static final int DEFAULT_ALPHA_STEP = 5;    /**     * 动画帧率     */    private int mFrameRate = DEFAULT_FRAME_RATE;    /**     * 渐变动画持续时间     */    private int mDuration = DEFAULT_DURATION;    /**     *      */    private Paint mPaint = new Paint();    /**     * 被点击的视图的中心点     */    private Point mCenterPoint = null;    /**     * 视图的Rect     */    private RectF mTargetRectf;    /**     * 起始的圆形背景半径     */    private int mRadius = DEFAULT_RADIUS;    /**     * 最大的半径     */    private int mMaxRadius = DEFAULT_RADIUS;    /**     * 渐变的背景色     */    private int mCirclelColor = Color.LTGRAY;    /**     * 每次重绘时半径的增幅     */    private int mRadiusStep = 1;    /**     * 保存用户设置的alpha值     */    private int mBackupAlpha;    /**     * 圆形半径针对于被点击视图的缩放比例,默认为0.8     */    private float mCircleScale = DEFAULT_SCALE;    /**     * 颜色的alpha值, (0, 255)     */    private int mColorAlpha = DEFAULT_ALPHA;    /**     * 每次动画Alpha的渐变递减值     */    private int mAlphaStep = DEFAULT_ALPHA_STEP;    private View mTargetView;    /**     * @param context     */    public MaterialLayout(Context context) {        this(context, null);    }    public MaterialLayout(Context context, AttributeSet attrs) {        super(context, attrs);        init(context, attrs);    }    public MaterialLayout(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        init(context, attrs);    }    private void init(Context context, AttributeSet attrs) {        if (isInEditMode()) {            return;        }        if (attrs != null) {            initTypedArray(context, attrs);        }        initPaint();        this.setWillNotDraw(false);        this.setDrawingCacheEnabled(true);    }    private void initTypedArray(Context context, AttributeSet attrs) {        final TypedArray typedArray = context.obtainStyledAttributes(attrs,                R.styleable.MaterialLayout);        mCirclelColor = typedArray.getColor(R.styleable.MaterialLayout_color, Color.LTGRAY);        mDuration = typedArray.getInteger(R.styleable.MaterialLayout_duration,                DEFAULT_DURATION);        mFrameRate = typedArray                .getInteger(R.styleable.MaterialLayout_framerate, DEFAULT_FRAME_RATE);        mColorAlpha = typedArray.getInteger(R.styleable.MaterialLayout_alpha, DEFAULT_ALPHA);        mCircleScale = typedArray.getFloat(R.styleable.MaterialLayout_scale, DEFAULT_SCALE);        typedArray.recycle();    }    private void initPaint() {        mPaint.setAntiAlias(true);        mPaint.setStyle(Paint.Style.FILL);        mPaint.setColor(mCirclelColor);        mPaint.setAlpha(mColorAlpha);        // 备份alpha属性用于动画完成时重置        mBackupAlpha = mColorAlpha;    }    /**     * 点击的某个坐标点是否在View的内部     *      * @param touchView     * @param x 被点击的x坐标     * @param y 被点击的y坐标     * @return 如果点击的坐标在该view内则返回true,否则返回false     */    private boolean isInFrame(View touchView, float x, float y) {        initViewRect(touchView);        return mTargetRectf.contains(x, y);    }    /**     * 获取点中的区域,屏幕绝对坐标值,这个高度值也包含了状态栏和标题栏高度     *      * @param touchView     */    private void initViewRect(View touchView) {        int[] location = new int[2];        touchView.getLocationOnScreen(location);        // 视图的区域        mTargetRectf = new RectF(location[0], location[1], location[0]                + touchView.getWidth(), location[1] + touchView.getHeight());    }    /**     * 减去状态栏和标题栏的高度     */    private void removeExtraHeight() {        int[] location = new int[2];        this.getLocationOnScreen(location);        // 减去两个该布局的top,这个top值就是状态栏的高度        mTargetRectf.top -= location[1];        mTargetRectf.bottom -= location[1];        // 计算中心点坐标        int centerHorizontal = (int) (mTargetRectf.left + mTargetRectf.right) / 2;        int centerVertical = (int) ((mTargetRectf.top + mTargetRectf.bottom) / 2);        // 获取中心点        mCenterPoint = new Point(centerHorizontal, centerVertical);    }    private View findTargetView(ViewGroup viewGroup, float x, float y) {        int childCount = viewGroup.getChildCount();        // 迭代查找被点击的目标视图        for (int i = 0; i < childCount; i++) {            View childView = viewGroup.getChildAt(i);            if (childView instanceof ViewGroup) {                return findTargetView((ViewGroup) childView, x, y);            } else if (isInFrame(childView, x, y)) { // 否则判断该点是否在该View的frame内                return childView;            }        }        return null;    }    private boolean isAnimEnd() {        return mRadius >= mMaxRadius;    }    private void calculateMaxRadius(View view) {        // 取视图的最长边        int maxLength = Math.max(view.getWidth(), view.getHeight());        // 计算Ripple圆形的半径        mMaxRadius = (int) ((maxLength / 2) * mCircleScale);        int redrawCount = mDuration / mFrameRate;        // 计算每次动画半径的增值        mRadiusStep = (mMaxRadius - DEFAULT_RADIUS) / redrawCount;        // 计算每次alpha递减的值        mAlphaStep = (mColorAlpha - 100) / redrawCount;    }    /**     * 处理ACTION_DOWN触摸事件, 注意这里获取的是Raw x, y,     * 即屏幕的绝对坐标,但是这个当屏幕中有状态栏和标题栏时就需要去掉这些高度,因此得到mTargetRectf后其高度需要减去该布局的top起点     * ,也就是标题栏和状态栏的总高度.     *      * @param event     */    private void deliveryTouchDownEvent(MotionEvent event) {        if (event.getAction() == MotionEvent.ACTION_DOWN) {            mTargetView = findTargetView(this, event.getRawX(), event.getRawY());            if (mTargetView != null) {                removeExtraHeight();                // 计算相关数据                calculateMaxRadius(mTargetView);                // 重绘视图                invalidate();            }        }    }    @Override    public boolean onInterceptTouchEvent(MotionEvent event) {        deliveryTouchDownEvent(event);        return super.onInterceptTouchEvent(event);    }    @Override    protected void dispatchDraw(Canvas canvas) {        super.dispatchDraw(canvas);        // 绘制Circle        drawRippleIfNecessary(canvas);    }    private void drawRippleIfNecessary(Canvas canvas) {        if (isFoundTouchedSubView()) {            // 计算新的半径和alpha值            mRadius += mRadiusStep;            mColorAlpha -= mAlphaStep;            // 裁剪一块区域,这块区域就是被点击的View的区域.通过clipRect来获取这块区域,使得绘制操作只能在这个区域范围内的进行,            // 即使绘制的内容大于这块区域,那么大于这块区域的绘制内容将不可见. 这样保证了背景层只能绘制在被点击的视图的区域            canvas.clipRect(mTargetRectf);            mPaint.setAlpha(mColorAlpha);            // 绘制背景圆形,也就是            canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, mRadius, mPaint);        }        if (isAnimEnd()) {            reset();        } else {            invalidateDelayed();        }    }    /**     * 发送重绘消息     */    private void invalidateDelayed() {        this.postDelayed(new Runnable() {            @Override            public void run() {                invalidate();            }        }, mFrameRate);    }    /**     * 判断是否找到被点击的子视图     *      * @return     */    private boolean isFoundTouchedSubView() {        return mCenterPoint != null && mTargetView != null;    }    private void reset() {        mCenterPoint = null;        mTargetRectf = null;        mRadius = DEFAULT_RADIUS;        mColorAlpha = mBackupAlpha;        mTargetView = null;        invalidate();    }}


自定义的属性, attrs.xml

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="MaterialLayout">        <attr name="alpha" format="integer" />        <attr name="alpha_step" format="integer" />        <attr name="framerate" format="integer" />        <attr name="duration" format="integer" />        <attr name="color" format="color" />        <attr name="scale" format="float" />    </declare-styleable></resources>


使用示例

  引用MaterialLayout工程或者将代码和attrs.xml拷贝到你的工程中,然后在你的布局xml中添加MaterialLayout布局,注意,不要忘了引用MaterialLayout自定义属性的命名空间,即下面的xmlns:ml这句。把com.example.materialdemo替换成你的包名就OK了。
<org.simple.MaterialLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:ml="http://schemas.android.com/apk/res/com.example.materialdemo"    android:id="@+id/layout"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:layout_margin="5dp"    android:background="#f0f0f0"    android:gravity="center"    ml:duration="200"    ml:alpha="200"    ml:scale="1.2"    ml:color="#FFD306" >    <Button        android:id="@+id/my_button"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:background="#33CC99"        android:padding="10dp"        android:text="@string/click"        android:textSize="20sp" />    <ImageView        android:id="@+id/my_imageview1"        android:layout_width="100dp"        android:layout_height="100dp"        android:layout_below="@id/my_button"        android:layout_marginTop="30dp"        android:background="#33CC99"        android:contentDescription="@string/app_name"        android:padding="10dp"        android:src="@drawable/ic_launcher" /></org.simple.MaterialLayout>


效果图

          

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

 

github仓库

 猛击此去fork吧


 我正在参加博客之星,既然你已经看到这里,点击这里投我一票吧,谢谢~    

           

再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值