1. 程式人生 > >安卓自定義控制元件-實現IOS版UC瀏覽器三點載入動畫效果

安卓自定義控制元件-實現IOS版UC瀏覽器三點載入動畫效果

1.實現分析

廢話不多說,看下IOS版UC瀏覽器的載入效果

這裡寫圖片描述

簡單畫個圖看下整個過程

這裡寫圖片描述
1.B圓的圓心移動的座標為:A圓和B圓的圓心的距離L的中點為圓心O1的下半圓的運動軌跡經過的座標,就有一個由B位置到A位置圓周運動的軌跡。
2.C圓的圓心移動的座標為:B圓和C圓的圓心的距離L的中點為圓心02的上半圓的運動軌跡經過的座標,就有一個由C位置到B位置圓周運動的軌跡。
3.A圓就特別一些,我分為兩個過程:一個是起點P0為A圓心,控制點P1為(L/2,L/2),終點P2為B圓心的二階貝塞爾曲線;一個是起點P0為B圓心,控制點P1為(L*3/2,-L/2),終點P2為C圓心的二階貝塞爾曲線
4.A圓的透明度為255,B圓為255*0.8,C圓為255*0.6

4.1 A移動到C,透明度變化255->255*0.6
4.2 B移動到A,透明度變化255*0.8->255
4.3 C移動到B,透明度變化255*0.6->255*0.8

2.程式碼實現

2.1 需要的變數

 public class ThreePointLoadingView extends View {
    // 畫筆
    private Paint mBallPaint;
    // 寬度
    private int mWidth;
    // 高度
    private int mHeight;
    // 圓之間的距離
    private
float mSpace; // 圓的半徑 private float mBallRadius; // 三個圓合起來的距離(包括間距) private float mTotalLength; // A圓心的x座標 private float mABallX; // A圓心的y座標 private float mABallY; // B圓心的x座標 private float mBBallX; // B圓心的y座標 private float mBBallY; // C圓心的x座標 private float mCBallX; // C圓心的y座標
private float mCBallY; // 圓心移動的距離 private float mMoveLength; // A圓心做二階貝塞爾曲線的起點、控制點、終點 private PointF mABallP0; private PointF mABallP1; private PointF mABallP2; // A圓心貝塞爾曲線運動時的座標 private float mABallazierX; private float mABallazierY; // 值動畫 private ValueAnimator mAnimator; // 值動畫產生的x方向的偏移量 private float mOffsetX = 0; // 根據mOffsetX算得的y方向的偏移量 private float mOffsetY; // A圓的起始透明度 private int mABallAlpha = 255; // B圓的起始透明度 private int mBBallAlpha = (int) (255 * 0.8); // C圓的起始透明度 private int mCBallAlpha = (int) (255 * 0.6);

2.2 構造時初始化畫筆和A圓的三個點

    public ThreePointLoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {

        mBallPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);

        mBallPaint.setColor(ContextCompat.getColor(getContext(), R.color.material_deep_orange_a200));

        mBallPaint.setStyle(Paint.Style.FILL);

        mABallP0 = new PointF();
        mABallP1 = new PointF();
        mABallP2 = new PointF();

    }

2.3 測量時初始化圓半徑、間距等資訊

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

        // 考慮padding值
        mWidth = measureSize(widthMeasureSpec, MeasureUtil.dip2px(getContext(), 180)) + getPaddingLeft() + getPaddingRight();
        mHeight = measureSize(heightMeasureSpec, MeasureUtil.dip2px(getContext(), 180)) + getPaddingTop() + getPaddingBottom();

        setMeasuredDimension(mWidth, mHeight);

        // 間距為寬度10分之一
        mSpace = mWidth * 1.0f / 20;

        // 半徑為寬度50分之一
        mBallRadius = mWidth * 1.0f / 50;

        // 總的長度為三個圓直徑加上之間的間距
        mTotalLength = mBallRadius * 6 + mSpace * 2;

        // 兩個圓圓心的距離
        mMoveLength = mSpace + mBallRadius * 2;

        // A圓心起始座標,同時貝塞爾曲線的起始座標也是這個
        mABallazierX = mABallX = (mWidth - mTotalLength) / 2 + mBallRadius;
        mABallazierY = mABallY = mHeight / 2;

        // A圓心起始點,控制點,終點
        mABallP0.set(mABallX, mABallY);
        mABallP1.set(mABallX + mMoveLength / 2, mABallY - mMoveLength / 2);
        mABallP2.set(mBBallX, mBBallY);

        // B圓心的起始座標
        mBBallX = (mWidth - mTotalLength) / 2 + mBallRadius * 3 + mSpace;
        mBBallY = mHeight / 2;

        // C圓心的起始座標
        mCBallX = (mWidth - mTotalLength) / 2 + mBallRadius * 5 + mSpace * 2;
        mCBallY = mHeight / 2;

    }

2.4 繪製三個圓並且開啟值動畫

    @Override
    protected void onDraw(Canvas canvas) {

        // 根據x方向偏移量求出y方向偏移量
        mOffsetY = (float) Math.sqrt(mMoveLength / 2 * mMoveLength / 2 - (mMoveLength / 2 - mOffsetX) * (mMoveLength / 2 - mOffsetX));

        // 繪製B圓
        mBallPaint.setAlpha(mBBallAlpha);
        canvas.drawCircle(mBBallX - mOffsetX,
                (float) (mBBallY + mOffsetY),
                mBallRadius,
                mBallPaint);

        // 繪製C圓
        mBallPaint.setAlpha(mCBallAlpha);
        canvas.drawCircle(mCBallX - mOffsetX,
                (float) (mCBallY - mOffsetY),
                mBallRadius,
                mBallPaint);

        // 繪製A圓
        mBallPaint.setAlpha(mABallAlpha);
        canvas.drawCircle(mABallazierX, mABallazierY, mBallRadius, mBallPaint);

        if (mAnimator == null) {
            // 啟動值動畫
            startLoading();
        }

    }

BC圓的移動依賴於:mOffsetY = (float) Math.sqrt(mMoveLength / 2 * mMoveLength / 2 - (mMoveLength / 2 - mOffsetX) * (mMoveLength / 2 - mOffsetX))對應的計算,mMoveLength / 2為半徑r,mOffsetX為offset,看草圖即可理解,第三象限的情況其實跟第四象限一樣的,因為(mMoveLength / 2 - mOffsetX)的平方總是為正
這裡寫圖片描述
A圓的移動則是在值動畫中算出座標點(mABallazierX, mABallazierY),首先看下二階貝塞爾曲線:
二階貝塞爾曲線(拋物線):

這裡寫圖片描述

這裡寫圖片描述

原理:由 P0 至 P1 的連續點 Q0,描述一條線段。
由 P1 至 P2 的連續點 Q1,描述一條線段。
由 Q0 至 Q1 的連續點 B(t),描述一條二次貝塞爾曲線。

2.5 值動畫的邏輯處理

    // 開啟值動畫
    private void startLoading() {

        // 範圍在0到圓心移動的距離,這個是以B圓到A圓位置為基準的
        mAnimator = ValueAnimator.ofFloat(0, mMoveLength);

        // 設定監聽
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                // B圓和C圓對應的X的偏移量
                mOffsetX = (float) animation.getAnimatedValue();

                float fraction = animation.getAnimatedFraction();

                // B移動到A,透明度變化255*0.8->255
                mBBallAlpha = (int) (255 * 0.8 + 255 * fraction * 0.2);
                // C移動到B,透明度變化255*0.6->255*0.8
                mCBallAlpha = (int) (255 * 0.6 + 255 * fraction * 0.2);
                // A移動到C,透明度變化255->255*0.6
                mABallAlpha = (int) (255 - 255 * fraction * 0.4);

                // A圓的分段二階貝塞爾曲線的處理
                if (fraction < 0.5) {
                    // fraction小於0.5時,為A到B過程的情況

                    // 乘以2是因為貝塞爾公式的t範圍在0到1
                    fraction *= 2;

                    // 設定當前情況的起始點、控制點、終點
                    mABallP0.set(mABallX, mABallY);
                    mABallP1.set(mABallX + mMoveLength / 2, mABallY - mMoveLength / 2);
                    mABallP2.set(mBBallX, mBBallY);

                    // 代入貝塞爾公式得到貝塞爾曲線過程的x,y座標
                    mABallazierX = getBazierValue(fraction, mABallP0.x, mABallP1.x, mABallP2.x);
                    mABallazierY = getBazierValue(fraction, mABallP0.y, mABallP1.y, mABallP2.y);
                } else {
                    // fraction大於等於0.5時,為A到B過程之後,再從B到C過程的情況

                    // 減0.5是因為t要從0開始變化
                    fraction -= 0.5;
                    // 乘以2是因為貝塞爾公式的t範圍在0到1
                    fraction *= 2;

                    // 設定當前情況的起始點、控制點、終點
                    mABallP0.set(mBBallX, mBBallY);
                    mABallP1.set(mBBallX + mMoveLength / 2, mBBallY + mMoveLength / 2);
                    mABallP2.set(mCBallX, mCBallY);

                    // 代入貝塞爾公式得到貝塞爾曲線過程的x,y座標
                    mABallazierX = getBazierValue(fraction, mABallP0.x, mABallP1.x, mABallP2.x);
                    mABallazierY = getBazierValue(fraction, mABallP0.y, mABallP1.y, mABallP2.y);
                }

                // 強制重新整理
                postInvalidate();

            }
        });
        // 動畫無限模式
        mAnimator.setRepeatCount(ValueAnimator.INFINITE);
        // 時長1秒
        mAnimator.setDuration(1000);
        // 延遲0.5秒執行
        mAnimator.setStartDelay(500);
        // 開啟動畫
        mAnimator.start();

    }

    /**
     * 二階貝塞爾公式:B(t)=(1-t)^2*P0+2*t*(1-t)*P1+t^2*P2,(t∈[0,1])
     */
    private float getBazierValue(float fraction, float p0, float p1, float p2) {
        return (1 - fraction) * (1 - fraction) * p0 + 2 * fraction * (1 - fraction) * p1 + fraction * fraction * p2;
    }

2.7 View銷燬時的處理

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        // 銷燬view時取消動畫,避免記憶體洩露
        mAnimator.cancel();
    }

3.實現效果

這裡寫圖片描述