安卓自定義控制元件-實現IOS版UC瀏覽器三點載入動畫效果
阿新 • • 發佈:2019-02-04
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();
}