LoadingDrawable之CircleJump系列一

2017-01-13 15:17:46来源:http://www.jianshu.com/p/fa85bc5e77d1作者:dinus_developer人点击

本文主要讲述LoadingDrawable之CircleJump系列的两个动画的实现原理。建议在看此博文之前先阅读LoadingDrawable前言, LoadingDrawable涉及的一些基类此篇博文将不再赘述。


Note: 这个项目正处于更新阶段, 将会不断有新的加载动画加入,欢迎关注我的Github, 获取LoadingDrawable的最新动态。


概述

LoadingDrawable所实现的动画深受大家的青睐,但是它是如何实现的呢? 这篇博文将带领大家领悟我的内心世界。这篇博文主要讲述CollisionLoadingRendererSwapLoadingRenderer这两个渲染器的实现原理。首先预览一下这两个LoadingRenderer的效果图(左上方为CollisionLoadingRenderer,右上方为SwapLoadingRenderer),看到效果图不要太快的往下看,可以先思考一下实现方式。先思考再借鉴最后实践对于一个程序员的迅速提升是必不可少的。




CollisionLoadingRenderer的思路

CollisionLoadingRenderer的原理
(1)首先需要调用PaintsetShader(Shader shader)方法, 通过LinearGradient设置渐变区域。 Note:渐变的距离是从第二个球到倒数第二个球之间的距离。
(2)绘制第二个至倒数第二个之间的圆球和圆球下面的椭圆。
(3)左右两边的球运动的曲线是y=ax^2. a > 0 所以第一个球的运动轨迹就是抛物线y=ax^2位于y轴左边的曲线, 最后一个球的运动轨迹就是抛物线y=ax^2位于y轴右边的曲线。 附二次函数
(4)根据运动曲线绘制运动的球。



CollisionLoadingRenderer的实现细节
LoadingRenderer的动画实现主要通过draw(Canvas canvas)(负责动画的绘制)和computeRender(float)(负责计算绘制需要的参数)。 此动画的主要分为三步
设置渐变区域 --> 绘制渐变区域的图像和两边的球 --> 动起来



(1)设置渐变区域


private void adjustParams() {    
mBallCenterY = mHeight / 2.0f;
//mWidth是drawable的宽度
//mBallRadius 是球的半径, 乘2表示直径
//mBallCount - 2是因为渐变区域是从第二个到倒数第二个, 减去两边的两个
mBallSideOffsets = (mWidth - mBallRadius * 2.0f * (mBallCount - 2)) / 2;
}
private void setupPaint() {
mPaint.setStyle(Paint.Style.FILL);
mPaint.setShader(new LinearGradient(mBallSideOffsets, 0, mWidth - mBallSideOffsets, 0,
mColors, mPositions, Shader.TileMode.CLAMP));
}

其中mBallSideOffsets是线性渐变在x方向上的开始位置, mWidth - mBallSideOffsets是线性渐变在x方向上的结束位置,在y方向上开始和结束值要一样, 因为我们是水平方向渐变。


(2)绘制渐变区域的图像和两边的球


@Override
protected void draw(Canvas canvas) {
//保存图层
int saveCount = canvas.save();
//绘制第二个到倒数第二个之间的球和球下面的椭圆
for (int i = 1; i < mBallCount - 1; i++) {
//绘制球
mPaint.setAlpha(MAX_ALPHA);
canvas.drawCircle(mBallRadius * (i * 2 - 1) + mBallSideOffsets,
mBallCenterY, mBallRadius, mPaint);
//绘制椭圆
mOvalRect.set(mBallRadius * (i * 2 - 2) + mBallSideOffsets,
mHeight - mOvalVerticalRadius * 2,
mBallRadius * (i * 2) + mBallSideOffsets,
mHeight);
mPaint.setAlpha(OVAL_ALPHA);
canvas.drawOval(mOvalRect, mPaint);
}
//绘制第一个球
mPaint.setAlpha(MAX_ALPHA);
canvas.drawCircle(mBallSideOffsets - mBallRadius - mLeftBallMoveXOffsets,
mBallCenterY - mLeftBallMoveYOffsets, mBallRadius, mPaint);
//绘制第一个椭圆
mOvalRect.set(mBallSideOffsets - mBallRadius - mBallRadius * mLeftOvalShapeRate - mLeftBallMoveXOffsets,
mHeight - mOvalVerticalRadius - mOvalVerticalRadius * mLeftOvalShapeRate,
mBallSideOffsets - mBallRadius + mBallRadius * mLeftOvalShapeRate - mLeftBallMoveXOffsets,
mHeight - mOvalVerticalRadius + mOvalVerticalRadius * mLeftOvalShapeRate);
mPaint.setAlpha(OVAL_ALPHA);
canvas.drawOval(mOvalRect, mPaint);
//绘制最后一个球
mPaint.setAlpha(MAX_ALPHA);
canvas.drawCircle(mBallRadius * (mBallCount * 2 - 3) + mBallSideOffsets + mRightBallMoveXOffsets,
mBallCenterY - mRightBallMoveYOffsets, mBallRadius, mPaint);
//绘制最后一个椭圆
mOvalRect.set(mBallRadius * (mBallCount * 2 - 3) - mBallRadius * mRightOvalShapeRate + mBallSideOffsets + mRightBallMoveXOffsets,
mHeight - mOvalVerticalRadius - mOvalVerticalRadius * mRightOvalShapeRate,
mBallRadius * (mBallCount * 2 - 3) + mBallRadius * mRightOvalShapeRate + mBallSideOffsets + mRightBallMoveXOffsets,
mHeight - mOvalVerticalRadius + mOvalVerticalRadius * mRightOvalShapeRate);
mPaint.setAlpha(OVAL_ALPHA);
canvas.drawOval(mOvalRect, mPaint);
//恢复图层
canvas.restoreToCount(saveCount);
}

draw(Canvas canvas)函数的代码的难点主要是计算球心,与椭圆的归一化位置。
[1]首先给出渐变区域的球的球心和椭圆归一化位置的计算方式
第i个球的球心X坐标 = 第1个球的最左边坐标(线性渐变的开始位置mBallSideOffsets) + 第i 个球心距第1个球的最左边的偏移量。【公式Ball】。
第i个椭圆的left = 第i个球的球心坐标 - 球的半径(mBallRadius)【公式Oval_Left】。
第i个椭圆的right = 第i个球的球心坐标 + 球的半径(mBallRadius)【公式Oval_Right】。
第i个椭圆的bottom = Drawable的底部(mHeight)【公式Oval_Bottom】。
第i个椭圆的top = 第i个椭圆的bottom - 椭圆的高度(mOvalVerticalRadius * 2)【公式Oval_Top】。
[2]然后给出第1个球的球心和椭圆归一化位置的计算方式
第1个球的球心X坐标 = 【公式Ball】i置0 - 当前第一个球的偏移量(mLeftBallMoveXOffsets)。
第1个椭圆的left = 第1个球的球心坐标 - 球的半径(mBallRadius) * 椭圆的缩小比例(mLeftOvalShapeRate)。
第1个椭圆的right = 第1个球的球心坐标 + 球的半径(mBallRadius)* 椭圆的缩小比例(mLeftOvalShapeRate)。
第1个椭圆的bottom = Drawable的底部(mHeight)+ 球的半径(mOvalVerticalRadius)* 椭圆的缩小比例(mLeftOvalShapeRate)。
第1个椭圆的top = 第1个椭圆的bottom - 椭圆垂直方向的半径(mOvalVerticalRadius) * 椭圆的缩小比例(mLeftOvalShapeRate)。
[3]最后给出最后1个球的球心和椭圆归一化位置的计算方式
同[2]。


(3)动起来


@Override
protected void computeRender(float renderProgress) {
// 在进度的前25%将第一个球移动到最左边
if (renderProgress <= START_LEFT_DURATION_OFFSET) {
float startLeftOffsetProgress = renderProgress / START_LEFT_DURATION_OFFSET;
computeLeftBallMoveOffsets(DECELERATE_INTERPOLATOR.getInterpolation(startLeftOffsetProgress));
return;
}
// 在进度的25%~50%将第一个球移动到原始位置
if (renderProgress <= START_RIGHT_DURATION_OFFSET) {
float startRightOffsetProgress = (renderProgress - START_LEFT_DURATION_OFFSET) / (START_RIGHT_DURATION_OFFSET - START_LEFT_DURATION_OFFSET);
computeLeftBallMoveOffsets(ACCELERATE_INTERPOLATOR.getInterpolation(1.0f - startRightOffsetProgress));
return;
}
// 在进度的50%~75%将最后一个球移动到最右边位置
if (renderProgress <= END_RIGHT_DURATION_OFFSET) {
float endRightOffsetProgress = (renderProgress - START_RIGHT_DURATION_OFFSET) / (END_RIGHT_DURATION_OFFSET - START_RIGHT_DURATION_OFFSET);
computeRightBallMoveOffsets(DECELERATE_INTERPOLATOR.getInterpolation(endRightOffsetProgress));
return;
}
// 在进度的75%~100%将最后一个球移动到原始位置
if (renderProgress <= END_LEFT_DURATION_OFFSET) {
float endRightOffsetProgress = (renderProgress - END_RIGHT_DURATION_OFFSET) / (END_LEFT_DURATION_OFFSET - END_RIGHT_DURATION_OFFSET);
computeRightBallMoveOffsets(ACCELERATE_INTERPOLATOR.getInterpolation(1 - endRightOffsetProgress));
return;
}
}
private void computeLeftBallMoveOffsets(float progress) {
mRightBallMoveXOffsets = 0.0f;
mRightBallMoveYOffsets = 0.0f;
mLeftOvalShapeRate = 1.0f - progress;
mLeftBallMoveXOffsets = mBallMoveXOffsets * progress;
//y = ax^2
mLeftBallMoveYOffsets = (float) (Math.pow(mLeftBallMoveXOffsets, 2) * mBallQuadCoefficient);
}
private void computeRightBallMoveOffsets(float progress) {
mLeftBallMoveXOffsets = 0.0f;
mLeftBallMoveYOffsets = 0.0f;
mRightOvalShapeRate = 1.0f - progress;
mRightBallMoveXOffsets = mBallMoveXOffsets * progress;
//y = ax^2
mRightBallMoveYOffsets = (float) (Math.pow(mRightBallMoveXOffsets, 2) * mBallQuadCoefficient);
}

computeRender(float renderProgress)主要是通过对应的Interpolator计算当前球(第一个或者最后一个球)的进度, 并通过computeLeftBallMoveOffsets(float progress)或者computeRightBallMoveOffsets(float progress)计算当前球的x, y坐标。 computeLeftBallMoveOffsets(float progress)中的代码主要做两件事情, 一、将右边的球复位, 二、通过当前进度,计算第一个球的x坐标值,并通过y = ax^2计算对应的y坐标值。computeRightBallMoveOffsets(float progress)同理。


SwapLoadingRenderer的思路

SwapLoadingRenderer的原理
(1)绘制静止的圆环。
(2)球和圆环的运动曲线是x^2 + y^2 = r^2. 球和圆环分别交替做上半圆和下半圆的弧线运动。附圆的标准方程
(3)绘制正在交换(绕圆弧运动)的球和圆环, 这球和圆环都做顺时针运动,球的交换规则是: 上半圆 -->下半圆 -->循环... -->最后一个总是下半圆,与其交换的圆环的交换规则是:下半圆 -->上半圆 -->循环... -->最后一个总是上半圆



SwapLoadingRenderer的实现细节
SwapLoadingRenderer的实现相对简单一点。此动画主要分为
绘制静止的圆环和交换中的球和圆环 --> 动起来
(1)绘制静止的圆环和交换中的球和圆环



@Override
protected void draw(Canvas canvas) {
//保存图层
int saveCount = canvas.save();
for (int i = 0; i < mBallCount; i++) {
if (i == mSwapIndex) {
//绘制交换中的球
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(mBallSideOffsets + mBallRadius * (i * 2 + 1) + i * mBallInterval + mSwapBallOffsetX
, mBallCenterY - mSwapBallOffsetY, mBallRadius, mPaint);
} else if (i == (mSwapIndex + 1) % mBallCount) {
/绘制交换中的圆环
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(mBallSideOffsets + mBallRadius * (i * 2 + 1) + i * mBallInterval - mSwapBallOffsetX
, mBallCenterY + mSwapBallOffsetY, mBallRadius - mStrokeWidth / 2, mPaint);
} else {
//绘制静止的圆环
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(mBallSideOffsets + mBallRadius * (i * 2 + 1) + i * mBallInterval,
mBallCenterY, mBallRadius - mStrokeWidth / 2, mPaint);
}
}
//恢复图层
canvas.restoreToCount(saveCount);
}

CollisionLoadingRenderer类似。 draw(Canvas canvas)函数的代码的难点主要是计算球心和圆环的圆心。
[1]首先给出静止圆环的圆心的计算方式
第i个圆环的圆心X坐标 = 第1个圆环的最左边坐标(圆环偏移量mBallSideOffsets) + 第i 个圆心距第1个圆环的最左边的偏移量(mBallRadius * (i * 2 + 1) + i * mBallInterval其中mBallRadius是圆环的半径,mBallInterval是圆环之间的间距)。【公式Circle】。
[2]然后给出运动中的球的球心的计算方式
第i个运动中球的球心X坐标 = 将i带入【公式Circle】+ 当前第i个球的球心X坐标的偏移量(mSwapBallOffsetX)。
第i个运动中球的球心Y坐标 = 球心Y坐标+ 当前第i个球的球心Y坐标的偏移量(mSwapBallOffsetY)。
[3]然后给出运动中的圆环的圆心的计算方式
同[2]。


(2)动起来


@Override
protected void computeRender(float renderProgress) {
mSwapIndex = (int) (renderProgress / mASwapThreshold);
//交换的轨迹 : x^2 + y^2 = r ^ 2
float swapTraceProgress = ACCELERATE_DECELERATE_INTERPOLATOR.getInterpolation(
(renderProgress - mSwapIndex * mASwapThreshold) / mASwapThreshold);
float swapTraceRadius = mSwapIndex == mBallCount - 1
? (mBallRadius * 2 * (mBallCount - 1) + mBallInterval * (mBallCount - 1)) / 2
: (mBallRadius * 2 + mBallInterval) / 2;
// 计算当前交换的球的球心的X偏移量
mSwapBallOffsetX = mSwapIndex == mBallCount - 1
? -swapTraceProgress * swapTraceRadius * 2
: swapTraceProgress * swapTraceRadius * 2;
// 如果 mSwapIndex == mBallCount - 1 则 (swapTraceRadius, swapTraceRadius) 作为运动轨迹的初始圆心
// 否则 (-swapTraceRadius, -swapTraceRadius) 作为运动轨迹的初始圆心
float xCoordinate = mSwapIndex == mBallCount - 1
? mSwapBallOffsetX + swapTraceRadius
: mSwapBallOffsetX - swapTraceRadius;
// 计算当前交换的球的球心的Y偏移量
mSwapBallOffsetY = (float) (mSwapIndex % 2 == 0 && mSwapIndex != mBallCount - 1
? Math.sqrt(Math.pow(swapTraceRadius, 2.0f) - Math.pow(xCoordinate, 2.0f))
: -Math.sqrt(Math.pow(swapTraceRadius, 2.0f) - Math.pow(xCoordinate, 2.0f)));
}

computeRender(float renderProgress)主要是通过对应的Interpolator计算当前处于交换的球的交换进度swapTraceProgress, 并根据当前处于交换的球的位置计算其与其交换的圆环之间球心距swapTraceRadius * 2。 其中当前交换的球的球心X偏移量mSwapBallOffsetX =swapTraceProgress * swapTraceRadius * 2 如果是最后一个则为相反数,因为是从左往右移动了。其中xCoordinate是当前运动轨迹的相对球心位置。 当前交换的球的球心Y偏移量是通过球的方程 mSwapBallOffsetY ^2 + xCoordinate^2 = swapTraceRadius ^2xCoordinate带入方程即可计算得出mSwapBallOffsetY


用法
了解LoadingDrawable的基本用法可以参考Wiki主页
了解CollisionLoadingRenderer的用法
了解SwapLoadingRenderer的用法

关于我

我喜欢Android, 喜欢开源, 喜欢做动画, 会不定期开源一些有用的项目。


源码地址:传送门




最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台