Android MaterialDesign之水波点击效果的几种实现方法

什么是水波点击的效果? 下面是几种不同的实现方法的效果图以及实现方法

Video_2016-08-31_003846

如何实现?

方法一 使用官方提供的RippleDrawable类

优点:使用方便,非常漂亮。

缺点:Android5.0以下版本无法使用

步骤:

  1. 添加一个普通的 ripple_bg_drawable.xml 背景文件

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <solid android:color="#8cc476" />
        <corners android:radius="0dp" />
    </shape>
    
  2. 添加带波纹效果的背景文件 ripple_bg.xml

    <?xml version="1.0" encoding="utf-8"?>
    <ripple xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="#FF21272B">
        <item android:drawable="@drawable/ripple_bg_drawable" />
    </ripple>
    

    这里使用了上面的xml文件作为背景,然后给组件设置背景时,选 ripple_bg.xml 就可以了。如

    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="@drawable/ripple_bg"
        android:padding="10dp"
        android:text="使用RippleDrawable类实现" />
    

    效果:

    Video_2016-08-30_231558

    很简单的录制了下gif ,质量不好。实际效果很好看。

    注意事项:如果你的api最低版本低于21,则ripple这里其实是有错误提醒的,低于这个版本会报错。

方法二 使用代码实现

优点:低版本兼容、使用简单

缺点:效果不如官方的好

步骤:

  1. 在values下添加 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="mycolor" format="color" />
            <attr name="scale" format="float" />
        </declare-styleable>
    </resources>
    
  2. 添加一个自定义的布局类 MaterialLayout.class

    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_mycolor, 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();
        }
    }
    
  3. 在布局文件中引用

    <com.liangddyy.rippledemo.MaterialLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <Button
                    android:layout_marginTop="10dp"
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:padding="10dp"
                    android:text="自定义布局实现1" />
                <Button
                    android:layout_marginTop="10dp"
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:padding="10dp"
                    android:text="自定义布局实现2" />
            </LinearLayout>
        </com.liangddyy.rippledemo.MaterialLayout>
    

    效果:

    Video_2016-08-30_235950

    其实可以看到,只要在 MaterialLayout 布局中的控件都有这个效果,所以使用其他是很方便的。

    这部分代码见原作者博客 http://blog.csdn.net/bboyfeiyu/article/details/42587799

方法三 第三方库实现

github上的一个叫 RippleEffec 项目

优点:美观、使用简单、最低兼容API 9

缺点:不如官方的好看啦

步骤:

  1. 导入库

    dependencies {
        compile 'com.github.traex.rippleeffect:library:1.3'
    }
    
  2. 使用类似于方法二,比较灵活和方便

    <com.andexert.library.RippleView
        android:layout_marginTop="10dp"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        rv_centered="true">
        <Button
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="#4ce65e"
            android:text="第三方库实现"/>
    </com.andexert.library.RippleView>
    

如果直接导入使用报错,那可以复制如下源码到工程使用。

  1. 定义个颜色

    <color name="rippelColor">#FFFFFF</color>

  2. 添加attrs.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="RippleView">
            <attr name="rv_alpha" format="integer" />
            <attr name="rv_framerate" format="integer" />
            <attr name="rv_rippleDuration" format="integer" />
            <attr name="rv_zoomDuration" format="integer" />
            <attr name="rv_color" format="color" />
            <attr name="rv_centered" format="boolean" />
            <attr name="rv_type" format="enum">
                <enum name="simpleRipple" value="0" />
                <enum name="doubleRipple" value="1" />
                <enum name="rectangle" value="2" />
            </attr>
            <attr name="rv_ripplePadding" format="dimension" />
            <attr name="rv_zoom" format="boolean" />
            <attr name="rv_zoomScale" format="float" />
        </declare-styleable>
    </resources>
    
  3. 添加RippleView.java文件

    代码太多,不复制了。参见末尾处的项目源码吧。

    效果:

    Video_2016-08-31_003808

最后附上整个示例工程代码:

https://github.com/liangddyy/RippleDemo

原文:http://539go.com/2016/08/31/Android-MaterialDesign%E4%B9%8B%E6%B0%B4%E6%B3%A2%E7%82%B9%E5%87%BB%E6%95%88%E6%9E%9C%E7%9A%84%E5%87%A0%E7%A7%8D%E5%AE%9E%E7%8E%B0%E6%96%B9%E6%B3%95/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容