PathMeasure之直播間送愛(ài)心

前言

像我這樣迷茫的人菊霜,像我這樣尋找的人,像我這樣碌碌無(wú)為的人脊另,你還見(jiàn)過(guò)多少人 ... 

詮釋了我的內(nèi)心真實(shí)想法

PathMeasure之直播間送愛(ài)心

Path(路徑)导狡,在繪制自定義控件中占著舉足輕重的地位。稍微復(fù)雜點(diǎn)的控件都會(huì)有著它的身影偎痛。是不是有時(shí)候還在為求控件某點(diǎn)的坐標(biāo)而犯愁呢旱捧?反正當(dāng)時(shí)我還在傻傻的計(jì)算控件路徑的公式去求坐標(biāo)。那么踩麦,如何來(lái)定位任意一個(gè)給定Path的任意一個(gè)點(diǎn)的坐標(biāo)呢枚赡?

Android SDK提供了一個(gè)非常有用的API來(lái)幫助開發(fā)者解決以上的難題,這個(gè)類就是 PathMeasure 谓谦,它的中文意思路徑測(cè)量贫橙,英文學(xué)得不好,只能翻譯這個(gè)淺顯的含義茁计。

先來(lái)揪一揪最終實(shí)現(xiàn)的效果圖:

heartview

簡(jiǎn)單控制了愛(ài)心的縮放料皇,速率以及透明度。源碼也非常的簡(jiǎn)單易懂星压,若有什么疑問(wèn)請(qǐng)留言践剂。

文章結(jié)尾會(huì)附上源碼,如果對(duì)你有所幫助娜膘,還望動(dòng)手點(diǎn)一點(diǎn)star

那下面重點(diǎn)來(lái)看一看 PathMeasure

PathMeasure的那些事

首先來(lái)看下 PathMeasure 的 API:

api

PathMeasure 的 API 非常簡(jiǎn)單逊脯,基本都是望文生義,紅框圈住是比較常用的方法竣贪。接著我們挨著來(lái)揪一揪各個(gè)方法的用法以及含義军洼。

初始化

PathMeasure 的初始化可以直接 new 一個(gè) PathMeasure

pathMeasure = new PathMeasure();

初始化 PathMeasure 后,可以通過(guò)以下的方式將 Path 和 PathMeasure 進(jìn)行綁定:

pathMeasure.setPath(path, false);

當(dāng)然還可以將上面兩步結(jié)合在一起完成有參的構(gòu)造方法來(lái)進(jìn)行初始化:

PathMeasure(Path path, boolean forceClosed)

參數(shù) path 就是需要計(jì)算演怎,測(cè)量的路徑匕争;那么 forceClosed 又代表什么含義呢,字面上強(qiáng)制關(guān)閉爷耀,接下來(lái)通過(guò)一個(gè)案例來(lái)加深對(duì)它的理解甘桑。

forceClosed參數(shù)

先看下面一段代碼,繪制了兩條線段,分別改變 forceClosed 的值來(lái)獲取 PathMeasure.getLength() 的值:

        mPathMeasure = new PathMeasure();
        mPath = new Path();

        //水平繪制長(zhǎng)為600的線段
        mPath.moveTo(800, 200);
        mPath.lineTo(200, 200);
        //繪制長(zhǎng)為800的字段
        mPath.lineTo(200, 1000);
         
        mPathMeasure.setPath(mPath, false);

forceClosed 為 false跑杭,true 的效果圖如下:

line

可以得出以下的結(jié)論铆帽,forceClosed 為 false 為兩條線段的長(zhǎng)度(600+800),forceClosed 為 ture 為路徑閉合的總長(zhǎng)度(600+800+1000)德谅。簡(jiǎn)單的說(shuō)爹橱,forceClosed 就是 Path 最終是否需要閉合,如果為 ture 的話窄做,則不管關(guān)聯(lián)的 Path 是否是閉合的愧驱,計(jì)算的時(shí)候都會(huì)按閉合來(lái)計(jì)算。

但是這個(gè)參數(shù)對(duì) Path 和 PathMeasure 的影響是需要解釋下的:

  • forceClosed 參數(shù)對(duì)綁定的 Path 不會(huì)產(chǎn)生任何影響浸策,例如一個(gè)折線段的 Path冯键,本身是沒(méi)有閉合的,forceClosed 設(shè)置為 ture 的時(shí)候庸汗, PathMeasure 計(jì)算的 Path 是閉合的惫确,但 Path 本身繪制出來(lái)是不會(huì)閉合的。

  • forceClosed 參數(shù)對(duì) PathMeasure 的測(cè)量結(jié)果有影響蚯舱,還是例如前面說(shuō)的一個(gè)折線段的 Path改化,本身沒(méi)有閉合,forceClosed 設(shè)置為 ture 枉昏, PathMeasure 的計(jì)算就會(huì)包含最后一段閉合的路徑陈肛,與原來(lái)的 Path 不同。

還有一點(diǎn)需要注意一下兄裂,如果你繪制的路徑是直線路徑句旱,則設(shè)置 forceClosed 失效。

getLength

PathMeasure.getLength() 的使用非常廣泛晰奖,其作用就是獲取計(jì)算的路徑長(zhǎng)度谈撒。

getSegment

getSegment 用于獲取 Path 的一個(gè)片段,方法如下:

getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
參數(shù) 作用 備注
返回值(boolean) 判斷截取是否成功 true 表示截取成功匾南,結(jié)果存入dst中啃匿,false 截取失敗,不會(huì)改變dst中內(nèi)容
startD 開始截取位置距離 Path 起點(diǎn)的長(zhǎng)度 取值范圍: 0~getLength
stopD 結(jié)束截取位置距離 Path 起點(diǎn)的長(zhǎng)度 取值范圍: 0~getLength
dst 截取的 Path 將會(huì)添加到 dst 中 注意: 是添加蛆楞,而不是替換
startWithMoveTo 起始點(diǎn)是否使用 moveTo 用于保證截取的 Path 第一個(gè)點(diǎn)位置不變

  • 4.4或者之前的版本溯乒,在默認(rèn)開啟硬件加速的情況下,更改 dst 的內(nèi)容后可能繪制會(huì)出現(xiàn)問(wèn)題豹爹,請(qǐng)關(guān)閉硬件加速或者給 dst 添加一個(gè)單個(gè)操作裆悄,例如:

       dst.lineTo(0, 0)
    

通過(guò)以下案例來(lái)加深對(duì) getSegment 的理解,效果圖如下:

fivestar

其原理就是通過(guò) getSegment 來(lái)不斷截取 Path 片段臂聋,從而不斷繪制完整的路徑灯帮,代碼如下:

public class CircleView extends View {

    Paint mPaint;

    Path mPath;

    Path mDstPath;

    PathMeasure mPathMeasure;

    float mPathLength;

    float mAnimatedValue;

    public CircleView(Context context) {
        this(context, null);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(2);
        mPaint.setColor(Color.RED);

        mPathMeasure = new PathMeasure();
        mPath = new Path();
        mDstPath = new Path();

        //圓弧路徑
        mPath.addArc(new RectF(200, 200, 600, 600), 0, 359.6f);

        //繪制五角星
        for (int i = 1; i < 6; i++) {
            Point p = getPoint(200, -144 * i);
            mPath.lineTo(400 + p.x, 400 + p.y);
        }

        mPath.close();

        mPathMeasure.setPath(mPath, false);

        mPathLength = mPathMeasure.getLength();

        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.setDuration(3000);
        animator.setRepeatCount(-1);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAnimatedValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        animator.start();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mDstPath.reset();
        // 硬件加速的BUG
        mDstPath.lineTo(0, 0);

        //獲取到路徑片段
        mPathMeasure.getSegment(0, mAnimatedValue * mPathLength, mDstPath, true); //注意startWithMoveTo一般使用true,如果使用false所有的軌跡會(huì)連接上原點(diǎn)崖技。
        canvas.drawPath(mDstPath, mPaint);
    }

    private Point getPoint(float radius, float angle) {
        float x = (float) ((radius) * Math.cos(angle * Math.PI / 180f));
        float y = (float) ((radius) * Math.sin(angle * Math.PI / 180f));
        Point p = new Point(x, y);
        return p;
    }

    private class Point {
        private float x;
        private float y;

        private Point(float x, float y) {
            this.x = x;
            this.y = y;
        }
    }
}

代碼中有相應(yīng)的注釋加以說(shuō)明

nextContour

nextContour() 方法用的比較少,比較大部分情況下都只會(huì)有一個(gè) Path 而不是多個(gè)钟哥,畢竟這樣會(huì)增加 Path 的復(fù)雜度,但是如果真有一個(gè) Path瞎访,包含了多個(gè) Path腻贰,那么通過(guò) nextContour 這個(gè)方法,就可以進(jìn)行切換扒秸,同時(shí)播演,默認(rèn)的 API,例如 getLength伴奥,獲取的也是當(dāng)前的這段 Path 所對(duì)應(yīng)的長(zhǎng)度写烤,而不是所有的 Path 的長(zhǎng)度,同時(shí)拾徙,nextContou r獲取 Path 的順序洲炊,與 Path 的添加順序是相同的,這里就不再舉例說(shuō)明了尼啡。

getPosTan

getPosTan(float distance, float[] pos, float[] tan)

這個(gè) API 非常強(qiáng)大暂衡,直播愛(ài)心的實(shí)現(xiàn)就用到了該方法。意思就是獲取路徑上某點(diǎn)的坐標(biāo)及其切線的坐標(biāo)

各個(gè)參數(shù)的含義如下表:

參數(shù) 作用 備注
返回值(boolean) 判斷截取是否成功 數(shù)據(jù)會(huì)存入 pos 和 tan 中崖瞭,false 表示失敗狂巢,pos 和 tan 不會(huì)改變
distance 距離 Path 起點(diǎn)的長(zhǎng)度 取值范圍: 0~getLength
pos 保存該點(diǎn)的坐標(biāo)值 坐標(biāo)值: (x, y)
tan 保存該點(diǎn)的正切值 正切值: (x, y)

接著來(lái)看以下案例,效果圖如下:

arrow

通常我們按照以下的方式來(lái)轉(zhuǎn)換切線的角度:

float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);

以下是模擬圓上箭頭圍繞圓的運(yùn)動(dòng)趨勢(shì)代碼:

public class ArrowView extends View {

    Paint mPaint;

    Path mPath;

    PathMeasure mPathMeasure;

    float mPathLength;

    float mAnimatedValue;

    float[] pos = new float[2];

    float[] tan = new float[2];

    public ArrowView(Context context) {
        this(context, null);
    }

    public ArrowView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ArrowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(6);
        mPaint.setColor(Color.RED);

        mPathMeasure = new PathMeasure();
        mPath = new Path();
        mPath.addCircle(0, 0, 200, Path.Direction.CW);

        mPathMeasure.setPath(mPath, false);

        mPathLength = mPathMeasure.getLength();

        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.setDuration(3000);
        animator.setRepeatCount(-1);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAnimatedValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        animator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mPathMeasure.getPosTan(mAnimatedValue * mPathLength, pos, tan);
        float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);

        canvas.save();
        canvas.translate(400, 400);
        mPaint.setColor(Color.RED);
        canvas.drawPath(mPath, mPaint);

        //繪制箭頭
        Path path = new Path(); //不建議在onDraw中直接new一個(gè)對(duì)象
        path.moveTo(40 * (float) Math.cos(-30 * Math.PI / 180f), 200 + 40 * (float) Math.sin(-30 * Math.PI / 180f));
        path.lineTo(0, 200);
        path.lineTo(40 * (float) Math.cos(30 * Math.PI / 180f), 200 + 40 * (float) Math.sin(30 * Math.PI / 180f));

        canvas.rotate(degrees);
        mPaint.setColor(Color.GREEN);
        canvas.drawPath(path, mPaint);

        canvas.restore();
    }
}

接著我們來(lái)看一看后一個(gè) API

getMatrix

getMatrix 方法用于獲取路徑上某一長(zhǎng)度的位置以及位置的正切值的矩陣书聚,方法體如下:

getMatrix(float distance, Matrix matrix, int flags)
參數(shù) 作用 備注
返回值(boolean) 判斷獲取是否成功 true表示成功 數(shù)據(jù)會(huì)存入matrix中唧领,false 失敗,matrix內(nèi)容不會(huì)改變
distance 距離 Path 起點(diǎn)的長(zhǎng)度 取值范圍: 0~getLength
matrix 根據(jù) falgs 標(biāo)記轉(zhuǎn)換成matrix 會(huì)根據(jù) flags 的設(shè)置而存入不同的矩陣
flags 規(guī)定哪些內(nèi)容會(huì)存入到matrix中 POSITION_MATRIX_FLAG(位置) TANGENT_MATRIX_FLAG(正切)

參數(shù)的含義:

參數(shù) 作用 備注
返回值(boolean) 判斷獲取是否成功 true表示成功 數(shù)據(jù)會(huì)存入matrix中雌续,false 失敗斩个,matrix內(nèi)容不會(huì)改變
distance 距離 Path 起點(diǎn)的長(zhǎng)度 取值范圍: 0~getLength
matrix 根據(jù) falgs 標(biāo)記轉(zhuǎn)換成matrix 會(huì)根據(jù) flags 的設(shè)置而存入不同的矩陣
flags 規(guī)定哪些內(nèi)容會(huì)存入到matrix中 POSITION_MATRIX_FLAG(位置) TANGENT_MATRIX_FLAG(正切)

flags 選項(xiàng)可以選擇 位置 或者 正切 ,如果我們兩個(gè)選項(xiàng)都想選擇怎么辦?

可以將兩個(gè)選項(xiàng)之間用 | 連接起來(lái)西雀,如下:

pathMeasure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG| PathMeasure.POSITION_MATRIX_FLAG);

以下是內(nèi)切圓的案例萨驶,效果圖如下:

matrix

代碼如下:

public class MatrixView extends View {

    Paint mPaint;

    Path mPath;

    PathMeasure mPathMeasure;

    Matrix mMatrix;

    float mPathLength;

    float mAnimatedValue;

    Bitmap mBitmap;

    public MatrixView(Context context) {
        this(context, null);
    }

    public MatrixView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MatrixView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(2);
        mPaint.setColor(Color.RED);

        mPathMeasure = new PathMeasure();
        mPath = new Path();
        mMatrix = new Matrix();
        mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_matrix);

        mPath.addCircle(0, 0, 200, Path.Direction.CW);

        mPathMeasure.setPath(mPath, false);

        mPathLength = mPathMeasure.getLength();

        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.setDuration(3000);
        animator.setRepeatCount(-1);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAnimatedValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        animator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mMatrix.reset();
        mPathMeasure.getMatrix(mAnimatedValue * mPathLength, mMatrix,
                PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);
        canvas.translate(400, 400);
        canvas.drawBitmap(mBitmap, mMatrix, mPaint);
        canvas.drawPath(mPath, mPaint);
    }
}

相信 PathMeasure 的強(qiáng)大會(huì)讓你愛(ài)不釋手。

PathMeasure送愛(ài)心

熟悉了 PathMeasure 相關(guān)的 API 艇肴,那么理解以下的代碼就非常容易了腔呜。原理非常簡(jiǎn)單繪制三階貝塞爾曲線根據(jù) getPosTan 獲取曲線上坐標(biāo),最后根據(jù)坐標(biāo)繪制愛(ài)心 Bitmap再悼,具體代碼如下:

public class HeartView extends View {

    private SparseArray<Bitmap> mBitmapSparseArray = new SparseArray<>();

    private SparseArray<Heart> mHeartSparseArray;

    private Paint mPaint;

    private int mWidth;

    private int mHeight;

    private Matrix mMatrix;

    //動(dòng)畫時(shí)長(zhǎng)
    private int mDuration;

    //最大速率
    private int mMaxRate;

    //是否控制速率
    private boolean mRateEnable;

    //是否控制透明度速率
    private boolean mAlphaEnable;

    //是否控制縮放
    private boolean mScaleEnable;

    //動(dòng)畫時(shí)長(zhǎng)
    private final static int DURATION_TIME = 3000;

    //最大速率
    private final static int MAX_RATE = 2000;

    public HeartView(Context context) {
        this(context, null);
    }

    public HeartView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init(context);
    }

    private void init(Context context) {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mHeartSparseArray = new SparseArray<>();
        mMatrix = new Matrix();

        mDuration = DURATION_TIME;
        mMaxRate = MAX_RATE;
        mRateEnable = true;
        mAlphaEnable = true;
        mScaleEnable = true;

        initBitmap(context);
    }

    private void initBitmap(Context context) {
        Bitmap bitmap1 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart1);
        Bitmap bitmap2 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart2);
        Bitmap bitmap3 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart3);
        Bitmap bitmap4 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart4);
        Bitmap bitmap5 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart5);
        Bitmap bitmap6 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart6);
        Bitmap bitmap7 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart7);
        mBitmapSparseArray.put(HeartType.BLUE, bitmap1);
        mBitmapSparseArray.put(HeartType.GREEN, bitmap2);
        mBitmapSparseArray.put(HeartType.YELLOW, bitmap3);
        mBitmapSparseArray.put(HeartType.PINK, bitmap4);
        mBitmapSparseArray.put(HeartType.BROWN, bitmap5);
        mBitmapSparseArray.put(HeartType.PURPLE, bitmap6);
        mBitmapSparseArray.put(HeartType.RED, bitmap7);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        //wrap_content情況-默認(rèn)高度為200寬度100
        int defaultWidth = (int) dp2px(100);

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(defaultWidth, defaultWidth * 3);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(defaultWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, defaultWidth * 3);
        } else {
            setMeasuredDimension(widthSpecSize, heightSpecSize);
        }
    }

    public void addHeart(int arrayIndex) {
        if (arrayIndex < 0 || arrayIndex > (mBitmapSparseArray.size() - 1)) return;
        Path path = new Path();
        final PathMeasure pathMeasure = new PathMeasure();
        final float pathLength;
        final Heart heart = new Heart();
        final int bitmapIndex = arrayIndex;

        //繪制三階貝塞爾曲線
        PointF start = new PointF();//起點(diǎn)位置
        PointF control1 = new PointF(); //貝塞爾控制點(diǎn)
        PointF control2 = new PointF(); //貝塞爾控制點(diǎn)
        PointF end = new PointF(); //貝塞爾結(jié)束點(diǎn)

        initStartAndEnd(start, end);
        initControl(control1, control2);

        path.moveTo(start.x, start.y);
        path.cubicTo(control1.x, control1.y, control2.x, control2.y, end.x, end.y);

        pathMeasure.setPath(path, false);

        pathLength = pathMeasure.getLength();

        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f);
        //先加速后減速
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        //動(dòng)畫的長(zhǎng)短來(lái)控制速率
        animator.setDuration(mDuration + (mRateEnable ? (int) (Math.random() * mMaxRate) : 0));
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                float fraction = valueAnimator.getAnimatedFraction();

                float[] pos = new float[2];
                pathMeasure.getPosTan(fraction * pathLength, pos, new float[2]);
                heart.setX(pos[0]);
                heart.setY(pos[1]);
                heart.setProgress(fraction);

                invalidate();
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                mHeartSparseArray.remove(heart.hashCode());
            }

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                mHeartSparseArray.put(heart.hashCode(), heart);
                heart.setIndex(bitmapIndex);
            }
        });
        animator.start();
    }

    public void addHeart() {
        addHeart(new Random().nextInt(mBitmapSparseArray.size() - 1));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mHeartSparseArray == null || mHeartSparseArray.size() == 0) return;

        canvasHeart(canvas);
    }

    private void canvasHeart(Canvas canvas) {
        for (int i = 0; i < mHeartSparseArray.size(); i++) {

            Heart heart = mHeartSparseArray.valueAt(i);

            //設(shè)置畫筆透明度
            mPaint.setAlpha(mAlphaEnable ? (int) (255 * (1.0f - heart.getProgress())) : 255);

            //會(huì)覆蓋掉之前的x,y數(shù)值
            mMatrix.setTranslate(0, 0);
            //位移到x,y
            mMatrix.postTranslate(heart.getX(), heart.getY());

            mMatrix.postScale(dealToScale(heart.getProgress()), dealToScale(heart.getProgress()),
                    mWidth / 2, mHeight);

            if (heart != null) {
                canvas.drawBitmap(mBitmapSparseArray.get(heart.getIndex()), mMatrix, mPaint);
            }

        }
    }

    private float dealToScale(float fraction) {
        if (fraction < 0.1f && mScaleEnable) {
            return 0.5f + fraction / 0.1f * 0.5f;
        }
        return 1.0f;
    }

    public void initControl(PointF control1, PointF control2) {
        control1.x = (float) (Math.random() * mWidth);
        control1.y = (float) (Math.random() * mHeight);

        control2.x = (float) (Math.random() * mWidth);
        control2.y = (float) (Math.random() * mHeight);

        if (control1.x == control2.x && control1.y == control2.y) {
            initControl(control1, control2);
        }
    }

    public void initStartAndEnd(PointF start, PointF end) {
        start.x = mWidth / 2;
        start.y = mHeight;

        end.x = mWidth / 2;
        end.y = 0;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
    }

    @Override
    protected void onDetachedFromWindow() {
        cancel();
        super.onDetachedFromWindow();
    }


    /**
     * 取消已有動(dòng)畫核畴,釋放資源
     */
    public void cancel() {
        //回收bitmap
        for (int i = 0; i < mBitmapSparseArray.size(); i++) {
            if (mBitmapSparseArray.valueAt(i) != null) {
                mBitmapSparseArray.valueAt(i).recycle();
            }
        }
    }

    public int getDuration() {
        return mDuration;
    }

    public void setDuration(int duration) {
        mDuration = duration;
    }

    public int getMaxRate() {
        return mMaxRate;
    }

    public void setMaxRate(int maxRate) {
        mMaxRate = maxRate;
    }

    public boolean getRateEnable() {
        return mRateEnable;
    }

    public void setRateEnable(boolean rateEnable) {
        mRateEnable = rateEnable;
    }

    public boolean getAlphaEnable() {
        return mAlphaEnable;
    }

    public void setAlphaEnable(boolean alphaEnable) {
        mAlphaEnable = alphaEnable;
    }

    public boolean getScaleEnable() {
        return mScaleEnable;
    }

    public void setScaleEnable(boolean scaleEnable) {
        mScaleEnable = scaleEnable;
    }

    private float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }

}

源代碼

源碼已上傳到Github

https://github.com/HpWens/HeartViewDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市冲九,隨后出現(xiàn)的幾起案子谤草,更是在濱河造成了極大的恐慌跟束,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丑孩,死亡現(xiàn)場(chǎng)離奇詭異冀宴,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)温学,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門略贮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人仗岖,你說(shuō)我怎么就攤上這事逃延。” “怎么了轧拄?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵揽祥,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我檩电,道長(zhǎng)拄丰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任是嗜,我火速辦了婚禮愈案,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鹅搪。我一直安慰自己站绪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布丽柿。 她就那樣靜靜地躺著恢准,像睡著了一般。 火紅的嫁衣襯著肌膚如雪甫题。 梳的紋絲不亂的頭發(fā)上馁筐,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音坠非,去河邊找鬼敏沉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛炎码,可吹牛的內(nèi)容都是我干的盟迟。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼潦闲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼攒菠!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起歉闰,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤辖众,失蹤者是張志新(化名)和其女友劉穎卓起,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凹炸,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡戏阅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了啤它。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饲握。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蚕键,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情衰粹,我是刑警寧澤锣光,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站铝耻,受9級(jí)特大地震影響誊爹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瓢捉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一频丘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泡态,春花似錦搂漠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至靶壮,卻和暖如春怔毛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背腾降。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工拣度, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人螃壤。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓抗果,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親映穗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子窖张,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容