美文网首页android动画Android知识Android开发
LoadingDrawable之CircleJump系列一

LoadingDrawable之CircleJump系列一

作者: dinus_developer | 来源:发表于2016-08-24 00:41 被阅读1639次

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

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

概述

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

CollisionLoadingRenderer的思路

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

  2. 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的思路

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

  2. 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

用法

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

关于我

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

源码地址:传送门

相关文章

网友评论

本文标题:LoadingDrawable之CircleJump系列一

本文链接:https://www.haomeiwen.com/subject/wbwysttx.html