Android水波纹特效的简单实现

我的开源页面指示器框架 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,往往过于局限其中,有时候换个思路,说不定会得到奇妙的效果。周末愉快~~~。

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

推荐阅读更多精彩内容