Android水波紋特效的簡單實(shí)現(xiàn)

我的開源頁面指示器框架 MagicIndicator,各位一定不要錯(cuò)過哦。

水波紋特效,想必大家或多或少見過,在我的印象中,大致有如下幾種:

  • 支付寶 "咻咻咻" 式
  • 流量球 "蕩漾" 式
  • 真實(shí)的水波紋效果,基于Bitmap處理式

今天我們主要講一講如何通過自定義View(以下簡稱WaveView)實(shí)現(xiàn) "咻咻咻" 式的水波紋擴(kuò)散效果,少廢話,先看東西:

填充式水波紋,間距相等
非填充式水波紋,間距相等
非填充式水波紋,間距不斷變大
填充式水波紋,間距不斷變小

額,想必大家已經(jīng)知道基本的原理了,就是用Canvas來畫嘛,但可不是簡單的畫哦,請往下看。

分析


這種類型的水波紋,其實(shí)無非就是畫圓而已,在給定的矩形中,一個(gè)個(gè)圓由最小半徑擴(kuò)大到最大半徑,伴隨著透明度從1.0變?yōu)?.0。我們假定這種擴(kuò)散是勻速的,則一個(gè)圓從創(chuàng)建(透明度為1.0)到消失(透明度為0.0)的時(shí)長就是定值,那么某一時(shí)刻某一個(gè)圓的半徑以及透明度完全可以由擴(kuò)散時(shí)間(當(dāng)前時(shí)間 - 創(chuàng)建時(shí)間)決定。

實(shí)現(xiàn)


按照上面的分析,我們寫出以下Circle類來表示一個(gè)圓:

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中,要有一個(gè)List來保存當(dāng)前正在顯示的圓:

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

我們定義一個(gè)start方法,用來啟動(dòng)擴(kuò)散:

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毫秒創(chuàng)建一個(gè)圓
        }
    }
};

private void newCircle() {
    long currentTime = System.currentTimeMillis();
    if (currentTime - mLastCreateTime < mSpeed) {
        return;
    }
    Circle circle = new Circle();
    mCircleList.add(circle);
    invalidate();
    mLastCreateTime = currentTime;
}

start方法只是簡單的創(chuàng)建了一個(gè)圓并添加到了mCircleList中,同時(shí)開啟了循環(huán)創(chuàng)建圓的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方法遍歷了每一個(gè)Circle,判斷Circle的擴(kuò)散時(shí)間是否超過了設(shè)定的擴(kuò)散時(shí)間,如果是則移除,如果不是,則計(jì)算Circle當(dāng)前的透明度和半徑并繪制出來。我們添加了一個(gè)延時(shí)刷新來不斷重繪界面,以達(dá)到連續(xù)的波紋擴(kuò)散效果。

現(xiàn)在運(yùn)行程序,應(yīng)該能看到圖2中的效果了,不過有點(diǎn)別扭,按常識,水波的間距是越來越大的,如何做到呢?

技巧


要讓水波紋的半徑非勻速變大,我們只能去修改Circle.getCurrentRadius()方法了。我們再次看看這個(gè)方法:

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

percent表示Circle當(dāng)前擴(kuò)散時(shí)間和總擴(kuò)散時(shí)間的一個(gè)百分比,考慮到當(dāng)前擴(kuò)散時(shí)間超過總擴(kuò)散時(shí)間時(shí)Circle會被移除,因此percent的實(shí)際區(qū)間為[0, 1],看到[0, 1],我不知道大家想到的是什么,我首先想到的就是差值器(Interpolator),我們可以通過定義差值器來實(shí)現(xiàn)對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時(shí),只需調(diào)用setInterpolator()來定義不同的插值器即可實(shí)現(xiàn)不同的效果。

圖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;   // 如果沒有設(shè)置mMaxRadius,可mMaxRadius = 最小長度 * mMaxRadiusRate;
    private float mMaxRadius;   // 最大波紋半徑
    private long mDuration = 2000; // 一個(gè)波紋從創(chuàng)建到消失的持續(xù)時(shí)間
    private int mSpeed = 500;   // 波紋的創(chuàng)建速度,每500ms創(chuàng)建一個(gè)
    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 請?jiān)L問我的 GitHub

總結(jié)


想必大家看完這篇文章會覺得原來插值器還可以這么用。其實(shí),有些時(shí)候我們使用系統(tǒng)提供的API,往往過于局限其中,有時(shí)候換個(gè)思路,說不定會得到奇妙的效果。周末愉快~~~。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容