美文网首页ITBOXAndroidAndroid
Android水波纹特效的简单实现

Android水波纹特效的简单实现

作者: hackware | 来源:发表于2016-06-13 12:44 被阅读16751次

    我的开源页面指示器框架 MagicIndicator,各位一定不要错过哦。

    水波纹特效,想必大家或多或少见过,在我的印象中,大致有如下几种:

    • 支付宝 "咻咻咻" 式
    • 流量球 "荡漾" 式
    • 真实的水波纹效果,基于Bitmap处理式

    今天我们主要讲一讲如何通过自定义View(以下简称WaveView)实现 "咻咻咻" 式的水波纹扩散效果,少废话,先看东西:

    填充式水波纹,间距相等 非填充式水波纹,间距相等 非填充式水波纹,间距不断变大 填充式水波纹,间距不断变小

    额,想必大家已经知道基本的原理了,就是用Canvas来画嘛,但可不是简单的画哦,请往下看。

    分析


    这种类型的水波纹,其实无非就是画圆而已,在给定的矩形中,一个个圆由最小半径扩大到最大半径,伴随着透明度从1.0变为0.0。我们假定这种扩散是匀速的,则一个圆从创建(透明度为1.0)到消失(透明度为0.0)的时长就是定值,那么某一时刻某一个圆的半径以及透明度完全可以由扩散时间(当前时间 - 创建时间)决定。

    实现


    按照上面的分析,我们写出以下Circle类来表示一个圆:

    private class Circle {
        private long mCreateTime;
    
        public Circle() {
            this.mCreateTime = System.currentTimeMillis();
        }
    
        public int getAlpha() {
            float percent = (System.currentTimeMillis() - mCreateTime) * 1.0f / mDuration;
            return (int) ((1.0f - percent) * 255);
        }
    
        public float getCurrentRadius() {
            float percent = (System.currentTimeMillis() - mCreateTime) * 1.0f / mDuration;
            return mInitialRadius + percent * (mMaxRadius - mInitialRadius);
        }
    }
    

    自然而然,在WaveView中,要有一个List来保存当前正在显示的圆:

    private List<Circle> mCircleList = new ArrayList<Circle>();
    

    我们定义一个start方法,用来启动扩散:

    public void start() {
        if (!mIsRunning) {
            mIsRunning = true;
            mCreateCircle.run();
        }
    }
    
    private Runnable mCreateCircle = new Runnable() {
        @Override
        public void run() {
            if (mIsRunning) {
                newCircle();
                postDelayed(mCreateCircle, mSpeed); // 每隔mSpeed毫秒创建一个圆
            }
        }
    };
    
    private void newCircle() {
        long currentTime = System.currentTimeMillis();
        if (currentTime - mLastCreateTime < mSpeed) {
            return;
        }
        Circle circle = new Circle();
        mCircleList.add(circle);
        invalidate();
        mLastCreateTime = currentTime;
    }
    

    start方法只是简单的创建了一个圆并添加到了mCircleList中,同时开启了循环创建圆的Runnable,然后通知界面刷新,我们再看看onDraw方法:

    protected void onDraw(Canvas canvas) {
        Iterator<Circle> iterator = mCircleList.iterator();
        while (iterator.hasNext()) {
            Circle circle = iterator.next();
            if (System.currentTimeMillis() - circle.mCreateTime < mDuration) {
                mPaint.setAlpha(circle.getAlpha());
                canvas.drawCircle(getWidth() / 2, getHeight() / 2, circle.getCurrentRadius(), mPaint);
            } else {
                iterator.remove();
            }
        }
        if (mCircleList.size() > 0) {
            postInvalidateDelayed(10);
        }
    }
    

    onDraw方法遍历了每一个Circle,判断Circle的扩散时间是否超过了设定的扩散时间,如果是则移除,如果不是,则计算Circle当前的透明度和半径并绘制出来。我们添加了一个延时刷新来不断重绘界面,以达到连续的波纹扩散效果。

    现在运行程序,应该能看到图2中的效果了,不过有点别扭,按常识,水波的间距是越来越大的,如何做到呢?

    技巧


    要让水波纹的半径非匀速变大,我们只能去修改Circle.getCurrentRadius()方法了。我们再次看看这个方法:

    public float getCurrentRadius() {
        float percent = (System.currentTimeMillis() - mCreateTime) * 1.0f / mDuration;
        return mInitialRadius + percent * (mMaxRadius - mInitialRadius);
    }
    

    percent表示Circle当前扩散时间和总扩散时间的一个百分比,考虑到当前扩散时间超过总扩散时间时Circle会被移除,因此percent的实际区间为[0, 1],看到[0, 1],我不知道大家想到的是什么,我首先想到的就是差值器(Interpolator),我们可以通过定义差值器来实现对Circle半径变化的控制!

    我们修改代码:

    private Interpolator mInterpolator = new LinearInterpolator();
    
    public void setInterpolator(Interpolator interpolator) {
        mInterpolator = interpolator;
        if (mInterpolator == null) {
            mInterpolator = new LinearInterpolator();
        }
    }
    
    private class Circle {
        private long mCreateTime;
    
        public Circle() {
            this.mCreateTime = System.currentTimeMillis();
        }
    
        public int getAlpha() {
            float percent = (System.currentTimeMillis() - mCreateTime) * 1.0f / mDuration;
            return (int) ((1.0f - mInterpolator.getInterpolation(percent)) * 255);
        }
    
        public float getCurrentRadius() {
            float percent = (System.currentTimeMillis() - mCreateTime) * 1.0f / mDuration;
            return mInitialRadius + mInterpolator.getInterpolation(percent) * (mMaxRadius - mInitialRadius);
        }
    }
    

    这样,外部使用WaveView时,只需调用setInterpolator()来定义不同的插值器即可实现不同的效果。

    图3效果的代码:

    mWaveView = (WaveView) findViewById(R.id.wave_view);
    mWaveView.setDuration(5000);
    mWaveView.setStyle(Paint.Style.STROKE);
    mWaveView.setSpeed(400);
    mWaveView.setColor(Color.parseColor("#ff0000"));
    mWaveView.setInterpolator(new AccelerateInterpolator(1.2f));
    mWaveView.start();
    

    图4效果的代码:

    mWaveView = (WaveView) findViewById(R.id.wave_view);
    mWaveView.setDuration(5000);
    mWaveView.setStyle(Paint.Style.FILL);
    mWaveView.setColor(Color.parseColor("#ff0000"));
    mWaveView.setInterpolator(new LinearOutSlowInInterpolator());
    mWaveView.start();
    

    附上WaveView的所有代码:

    /**
     * 水波纹特效
     * Created by hackware on 2016/6/17.
     */
    public class WaveView extends View {
        private float mInitialRadius;   // 初始波纹半径
        private float mMaxRadiusRate = 0.85f;   // 如果没有设置mMaxRadius,可mMaxRadius = 最小长度 * mMaxRadiusRate;
        private float mMaxRadius;   // 最大波纹半径
        private long mDuration = 2000; // 一个波纹从创建到消失的持续时间
        private int mSpeed = 500;   // 波纹的创建速度,每500ms创建一个
        private Interpolator mInterpolator = new LinearInterpolator();
    
        private List<Circle> mCircleList = new ArrayList<Circle>();
        private boolean mIsRunning;
    
        private boolean mMaxRadiusSet;
    
        private Paint mPaint;
        private long mLastCreateTime;
    
        private Runnable mCreateCircle = new Runnable() {
            @Override
            public void run() {
                if (mIsRunning) {
                    newCircle();
                    postDelayed(mCreateCircle, mSpeed);
                }
            }
        };
    
        public WaveView(Context context) {
            this(context, null);
        }
    
        public WaveView(Context context, AttributeSet attrs) {
            super(context, attrs);
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            setStyle(Paint.Style.FILL);
        }
    
        public void setStyle(Paint.Style style) {
            mPaint.setStyle(style);
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            if (!mMaxRadiusSet) {
                mMaxRadius = Math.min(w, h) * mMaxRadiusRate / 2.0f;
            }
        }
    
        public void setMaxRadiusRate(float maxRadiusRate) {
            this.mMaxRadiusRate = maxRadiusRate;
        }
    
        public void setColor(int color) {
            mPaint.setColor(color);
        }
    
        /**
         * 开始
         */
        public void start() {
            if (!mIsRunning) {
                mIsRunning = true;
                mCreateCircle.run();
            }
        }
    
        /**
         * 停止
         */
        public void stop() {
            mIsRunning = false;
        }
    
        protected void onDraw(Canvas canvas) {
            Iterator<Circle> iterator = mCircleList.iterator();
            while (iterator.hasNext()) {
                Circle circle = iterator.next();
                if (System.currentTimeMillis() - circle.mCreateTime < mDuration) {
                    mPaint.setAlpha(circle.getAlpha());
                    canvas.drawCircle(getWidth() / 2, getHeight() / 2, circle.getCurrentRadius(), mPaint);
                } else {
                    iterator.remove();
                }
            }
            if (mCircleList.size() > 0) {
                postInvalidateDelayed(10);
            }
        }
    
        public void setInitialRadius(float radius) {
            mInitialRadius = radius;
        }
    
        public void setDuration(long duration) {
            this.mDuration = duration;
        }
    
        public void setMaxRadius(float maxRadius) {
            this.mMaxRadius = maxRadius;
            mMaxRadiusSet = true;
        }
    
        public void setSpeed(int speed) {
            mSpeed = speed;
        }
    
        private void newCircle() {
            long currentTime = System.currentTimeMillis();
            if (currentTime - mLastCreateTime < mSpeed) {
                return;
            }
            Circle circle = new Circle();
            mCircleList.add(circle);
            invalidate();
            mLastCreateTime = currentTime;
        }
    
        private class Circle {
            private long mCreateTime;
    
            public Circle() {
                this.mCreateTime = System.currentTimeMillis();
            }
    
            public int getAlpha() {
                float percent = (System.currentTimeMillis() - mCreateTime) * 1.0f / mDuration;
                return (int) ((1.0f - mInterpolator.getInterpolation(percent)) * 255);
            }
    
            public float getCurrentRadius() {
                float percent = (System.currentTimeMillis() - mCreateTime) * 1.0f / mDuration;
                return mInitialRadius + mInterpolator.getInterpolation(percent) * (mMaxRadius - mInitialRadius);
            }
        }
    
        public void setInterpolator(Interpolator interpolator) {
            mInterpolator = interpolator;
            if (mInterpolator == null) {
                mInterpolator = new LinearInterpolator();
            }
        }
    }
    

    完整 demo 请访问我的 GitHub

    总结


    想必大家看完这篇文章会觉得原来插值器还可以这么用。其实,有些时候我们使用系统提供的API,往往过于局限其中,有时候换个思路,说不定会得到奇妙的效果。周末愉快~~~。

    相关文章

      网友评论

      • 小小程序员jh:真的不错,正好需要!
      • 小猪向天飞:作者你好,请问有研究过ReactNative的水波纹动画实现吗?谢谢。
      • 风筝FLY:楼主,怎么修改初始线条宽度呢??
      • 2f15eef9dd68:功能是很好,不过我觉得可以控制对Circle进行缓存,要不然重复创建与销毁还是有点浪费
      • luo2016:请问下非填充式水波纹怎么设置水波纹线的粗细,默认的太细了,效果不明显
      • wo883721:思路很清晰。我刚刚做这个需求的时候,思路有点蒙,看到有几个圆环,想着控制每个圆环半径从最小到最大,有点傻逼了。就应该想你这样,控制每个圆从最小扩散到最大,因为同时存在多个圆,就可以有圆环效果了。
      • starryCaptain:楼主也是Smartisan用户啊,握手
      • Alex_Cin:今天逛 github 发现了楼主,居然是合肥的。。。 我之前在合肥上大学的,好有缘分
        hackware: @Alex_Cin 哎哟,好巧哈
      • 68237ecb8356:亲们 诺诺的问一下 光有源码了 如何 和xml 配合 才能将效果显示出来啊 :mask:
      • 68237ecb8356:有问题 请教一下
      • 68237ecb8356:楼主 我很崇拜你 啊 加 我 qq 吧 1131216061
      • hubsul_nvtag:请问这个能一下子停止波纹吗?好像是渐渐停止的,还有这个波纹的初始波纹半径设置好像没起作用呢,楼主
        hackware: @nvtag 你首先得把mIsRunning置为false,避免再创建新的圆,再把mCircleList清掉,invalidavate即可,我在github的代码里有一个stopImmediately的方法
        hubsul_nvtag:@hackware 那个一下子停住能给我讲讲吗?我有点懵了
        hackware:@nvtag 一下子停止需要把circle都移除,再invalidate,初始半径有作用啊
      • 3423305a7b38:我也在研究六边形波纹效果 但是 时间 透明度 半径的问题没解决 楼主结合时间 把三个问题很巧妙的解决 给个赞

        hackware: @熊熊熊熊能 哈哈哈哈…
      • sxt_0215:求源码 sxt_0215@163.com 感谢 :joy:
        hackware:@宿希婷 源码都贴了啊 :joy:
      • f5d840cd19b1:LinearOutSlowInInterpolator() 方法我没有?
        hackware:@f5d840cd19b1 系统自带的,难道有兼容性问题
      • 小菜_charry:能否提供下源码方便研究
        hackware:@小菜_charry 不是都全贴了么?
      • ffc04f48f698:为什么在一个circle被移除前,onDraw会被一直掉用呢?invalidate()只执行了一次啊。。不理解。
        ffc04f48f698:@hackware 懂了。。。 :smile:
        hackware:@ffc04f48f698 因为有postInvalidate(10)啊,只要还有circle的话,每隔10ms就调用一下invalidate
      • 9a0a59e34476: 不错哦, 写的很清楚 赞一个,期待更多作品
      • xi宝:在stop()方法中应该加上:getHandler().removeCallbacks(mCreateCircle);
        否则线程会一直跑的。
        xi宝:@hackware 楼主是对的,因为此句话mIsRunning = false;决定了是否继续创建。是我想多了,嘻嘻。
        1bcab1154c88:@hackware 为什么不回复我呢
        hackware:@mqclucky 不会的
      • 1bcab1154c88:mainActivity 和xml文件可否一起发一下 或则直接给个源码链接 或发到邮箱zwill2014@163.com
        hackware:@android_张波 你要调用start啊
        1bcab1154c88:@hackware 我写了 可是现实的还是空白的界面,像我这样的新手,有时候有点依赖源码
        hackware:@android_张波 源码都贴了,你复制WaveView过去就行 自己写一个空Activity,在xml加上WaveView即可
      • 8e51a15a3fd9:有没有源码求源码,给个地址或者发到邮箱谢谢,920651004@qq.com
        hackware:@Dsnack 源码不是贴了么
      • one_cup:关于插值器的使用,确实提供了一种新的思路
      • NKming:写的很不错,赞一个
      • 5317811311d5:湖广会馆

      本文标题:Android水波纹特效的简单实现

      本文链接:https://www.haomeiwen.com/subject/pttsdttx.html