LoadingDrawable之CircleJump系列一

本文主要讲述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, 喜欢开源, 喜欢做动画, 会不定期开源一些有用的项目。

源码地址:传送门

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,567评论 25 707
  • 《ilua》速成开发手册3.0 官方用户交流:iApp开发交流(1) 239547050iApp开发交流(2) 1...
    叶染柒丶阅读 9,733评论 0 11
  • 刀,十二柄挂着雨水的长刀 刀锋沁着杀气,夹杂着风声雨声以及划破空气的“呲呲”声,已经对马车形成合围之势 眼看就要刺...
    我是玄猫阅读 255评论 0 2
  • 遵照老师的要求,今天找到了两本电子书,一本是萨提亚的《新家庭如何塑造人》,一本是美国人劳伦斯.科恩著作《游戏力》,...
    冠世墨玉yanzi阅读 244评论 0 0
  • 本来,我们像平行的雨滴,互不干扰,是大地的引力使我们融为一块;本来,我们像散漫的江河,各奔东西,是潮汐的魅力使我们...
    孤独乞丐阅读 103评论 0 0