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;
private float mABallX;
private float mABallY;
private float mBBallX;
private float mBBallY;
private float mCBallX;
private float mCBallY;
private float mMoveLength;
private PointF mABallP0;
private PointF mABallP1;
private PointF mABallP2;
private float mABallazierX;
private float mABallazierY;
private ValueAnimator mAnimator;
private float mOffsetX = 0;
private float mOffsetY;
private int mABallAlpha = 255;
private int mBBallAlpha = (int) (255 * 0.8);
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) {
mWidth = measureSize(widthMeasureSpec, MeasureUtil.dip2px(getContext(), 180)) + getPaddingLeft() + getPaddingRight();
mHeight = measureSize(heightMeasureSpec, MeasureUtil.dip2px(getContext(), 180)) + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(mWidth, mHeight);
mSpace = mWidth * 1.0f / 20;
mBallRadius = mWidth * 1.0f / 50;
mTotalLength = mBallRadius * 6 + mSpace * 2;
mMoveLength = mSpace + mBallRadius * 2;
mABallazierX = mABallX = (mWidth - mTotalLength) / 2 + mBallRadius;
mABallazierY = mABallY = mHeight / 2;
mABallP0.set(mABallX, mABallY);
mABallP1.set(mABallX + mMoveLength / 2, mABallY - mMoveLength / 2);
mABallP2.set(mBBallX, mBBallY);
mBBallX = (mWidth - mTotalLength) / 2 + mBallRadius * 3 + mSpace;
mBBallY = mHeight / 2;
mCBallX = (mWidth - mTotalLength) / 2 + mBallRadius * 5 + mSpace * 2;
mCBallY = mHeight / 2;
}
2.4 绘制三个圆并且开启值动画
@Override
protected void onDraw(Canvas canvas) {
mOffsetY = (float) Math.sqrt(mMoveLength / 2 * mMoveLength / 2 - (mMoveLength / 2 - mOffsetX) * (mMoveLength / 2 - mOffsetX));
mBallPaint.setAlpha(mBBallAlpha);
canvas.drawCircle(mBBallX - mOffsetX,
(float) (mBBallY + mOffsetY),
mBallRadius,
mBallPaint);
mBallPaint.setAlpha(mCBallAlpha);
canvas.drawCircle(mCBallX - mOffsetX,
(float) (mCBallY - mOffsetY),
mBallRadius,
mBallPaint);
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() {
mAnimator = ValueAnimator.ofFloat(0, mMoveLength);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mOffsetX = (float) animation.getAnimatedValue();
float fraction = animation.getAnimatedFraction();
mBBallAlpha = (int) (255 * 0.8 + 255 * fraction * 0.2);
mCBallAlpha = (int) (255 * 0.6 + 255 * fraction * 0.2);
mABallAlpha = (int) (255 - 255 * fraction * 0.4);
if (fraction <0.5) {
fraction *= 2;
mABallP0.set(mABallX, mABallY);
mABallP1.set(mABallX + mMoveLength / 2, mABallY - mMoveLength / 2);
mABallP2.set(mBBallX, mBBallY);
mABallazierX = getBazierValue(fraction, mABallP0.x, mABallP1.x, mABallP2.x);
mABallazierY = getBazierValue(fraction, mABallP0.y, mABallP1.y, mABallP2.y);
} else {
fraction -= 0.5;
fraction *= 2;
mABallP0.set(mBBallX, mBBallY);
mABallP1.set(mBBallX + mMoveLength / 2, mBBallY + mMoveLength / 2);
mABallP2.set(mCBallX, mCBallY);
mABallazierX = getBazierValue(fraction, mABallP0.x, mABallP1.x, mABallP2.x);
mABallazierY = getBazierValue(fraction, mABallP0.y, mABallP1.y, mABallP2.y);
}
postInvalidate();
}
});
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAnimator.setDuration(1000);
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();
mAnimator.cancel();
}
3.实现效果
Demo下载:防IOS-UC浏览器三点加载动画