Android 自定义View之烧瓶loading动画

我们首先看下效果

FlaskView
FlaskView

画瓶子

首先,创建一个自定义view,我们知道,在view的大小发生改变后,会回调接口

/**
* This is called during layout when the size of this view has changed. If
* you were just added to the view hierarchy, you're called with the old
* values of 0.
*
* @param w Current width of this view.
* @param h Current height of this view.
* @param oldw Old width of this view.
* @param oldh Old height of this view.
*/
protected void onSizeChanged(int w, int h, int oldw, int oldh) {

}

因此,我们可以在该方法里面,取得view的宽高后进行瓶子的初始化,大概的思路是:

  • 计算瓶底圆半径大小
  • 计算瓶颈高度大小
  • 计算瓶盖高度大小
  • path添加瓶底圆轨迹
  • path添加瓶颈轨迹
  • path添加瓶盖轨迹

在Android中,默认的0°为数学上圆的90°,这里不明白的请百度

关于瓶底扇形圆弧,这里测试得出取-70° 到 250°,即瓶底圆和屏颈相交的两个点,为比较美观的,因此这里取了这个角度。

关于path.addArc,首先这里的参数代表

  • oval 圆弧形状边界,可以当做是一个矩形边界
  • startAngle 起始角度
  • sweepAngle 旋转角度(注意,这里不是最终的角度,而是要旋转的角度)
/**
* Add the specified arc to the path as a new contour.
*
* @param oval The bounds of oval defining the shape and size of the arc
* @param startAngle Starting angle (in degrees) where the arc begins
* @param sweepAngle Sweep angle (in degrees) measured clockwise
*/
public void addArc(RectF oval, float startAngle, float sweepAngle) {
    addArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle);
}

详细可查看以下代码注释

//获取view的中心点
float centerX = w / 2;
float centerY = h / 2;

//瓶底圆半径为view的宽度的1/5
float flaskBottomCircleRadius = w / 5f;

//瓶颈高度为半径的2/3
float neckHeight = flaskBottomCircleRadius * 2f / 3;

//瓶盖高度为瓶颈高度的3/10
float headHeight = 0.3f * neckHeight;

//重置path
mFlaskPath.reset();

//计算瓶子在view中的中心点y坐标
float flaskCenterY = centerY + (neckHeight + headHeight) / 2;

//**********************************************************瓶底部分******************************************************
//瓶底和瓶颈的左边和右边的相交的两个点的坐标
float[] leftEndPos = new float[2];
float[] rightEndPos = new float[2];

//瓶底圆底部点的坐标
float[] bottomPos = new float[2];

//计算三个点的坐标
leftEndPos[0] = (float) (flaskBottomCircleRadius * Math.cos(250 * Math.PI / 180f) + centerX);
leftEndPos[1] = (float) (flaskBottomCircleRadius * Math.sin(250 * Math.PI / 180f) + flaskCenterY);

rightEndPos[0] = (float) (flaskBottomCircleRadius * Math.cos(-70 * Math.PI / 180f) + centerX);
rightEndPos[1] = (float) (flaskBottomCircleRadius * Math.sin(-70 * Math.PI / 180f) + flaskCenterY);

bottomPos[0] = (float) (flaskBottomCircleRadius * Math.cos(90 * Math.PI / 180f) + centerX);
bottomPos[1] = (float) (flaskBottomCircleRadius * Math.sin(90 * Math.PI / 180f) + flaskCenterY);

//计算出圆弧所在的区域
RectF flaskArcRect = new RectF(centerX - flaskBottomCircleRadius, flaskCenterY - flaskBottomCircleRadius,
centerX + flaskBottomCircleRadius, flaskCenterY + flaskBottomCircleRadius);

//添加底部圆弧轨迹
mFlaskPath.addArc(flaskArcRect, -70, 320);
//***********************************************************************************************************************

//首先将path移至左边相交点
mFlaskPath.moveTo(leftEndPos[0], leftEndPos[1]);

//添加左边的瓶颈线
mFlaskPath.lineTo(leftEndPos[0], leftEndPos[1] - neckHeight);

//通过贝塞尔曲线添加左边瓶盖轨迹
mFlaskPath.quadTo(leftEndPos[0] - flaskBottomCircleRadius / 8, leftEndPos[1] - neckHeight - headHeight / 2,
    leftEndPos[0], leftEndPos[1] - neckHeight - headHeight);

//移动至右边瓶盖定点
mFlaskPath.lineTo(rightEndPos[0],rightEndPos[1] - neckHeight - headHeight);

//通过贝塞尔曲线添加右边瓶盖轨迹
mFlaskPath.quadTo(rightEndPos[0] + flaskBottomCircleRadius / 8, rightEndPos[1] - neckHeight - headHeight / 2,
    rightEndPos[0], rightEndPos[1] - neckHeight);

//添加右边的瓶颈线
mFlaskPath.lineTo(rightEndPos[0], rightEndPos[1]);

View的onDraw中描绘瓶子

canvas.drawPath(mFlaskPath, mStrokePaint);

画水位

根据以上代码,我们已经计算获得了整个瓶子的path,那么我们如何去计算和画水位呢?

  • 计算瓶子path所占的区域
  • 对整个瓶子的path进行canvas裁剪

我们可以通过path.computeBounds()计算出瓶子所占的整个区域

mFlaskPath.computeBounds(mFlaskBoundRect, false);
mFlaskBoundRect.bottom -= (mFlaskBoundRect.bottom - bottomPos[1]);

但是我们这里为什么还要减去一个差值呢?

这是因为,path.addArc()后,如果圆被截断即addArc的并不是一个完整的圆(我们这里瓶底就是一个弧度圆,瓶底与瓶颈之间的交点使瓶底圆截断),会导致path.computeBounds()计算出来的区域多出来一定的空间,这里贴两张示例图:

以下为不减去该差值的效果:


FlaskView

以下为减去该差值的效果:


FlaskView

计算出瓶子的区域后,我们就可以获取水位的区域了

mWaterRect.set(mFlaskBoundRect.left, mFlaskBoundRect.bottom - mFlaskBoundRect.height() * mWaterHeightPercent,mFlaskBoundRect.right, mFlaskBoundRect.bottom);

利用canvas的裁剪功能,进行水位的绘制

//裁剪整个瓶子的画布
canvas.clipPath(mFlaskPath);

//画水位
canvas.drawRect(mWaterRect, mWaterPaint);

画水泡

水泡生成和描绘的思路

  • 根据水位区域,在水位底部,随机产生水泡
  • 产生水泡后,将该水泡记录下来,并且根据一个speed进行位移
  • 当水泡离开水位区域,将其在记录中移除
private void createBubble() {
    //若水泡数量达到上限或者水位区域为空的时候,不产生水泡
    if (mBubbles.size() >= mBubbleMaxNumber
        || mWaterRect.isEmpty()) {
        return;
    }
    
    //根据时间间隔,判断是否已到达水泡产生的时间
    long current = System.currentTimeMillis();
    if ((current - mBubbleCreationTime) < mBubbleCreationInterval){
        return;
    }
    
    mBubbleCreationTime = current;
    
    //以下代码为随机计算水泡坐标 + 半径+ 速度
    Bubble bubble = obtainBubble();
    int radius = mBubbleMinRadius + mOnlyRandom.nextInt(mBubbleMaxRadius - mBubbleMinRadius);
    
    bubble.radius = radius;
    bubble.speed = mBubbleMinSpeed + mOnlyRandom.nextFloat() * mBubbleMaxSpeed;
    
    bubble.x = mWaterRect.left + mOnlyRandom.nextInt((int) mWaterRect.width()); //random x coordinate
    bubble.y = mWaterRect.bottom - radius - mStrokeWidth / 2; //the fixed y coordinate
    
    mBubbles.add(bubble);
}

利用canvas的裁剪功能,进行水泡的绘制

//裁剪水位画布
canvas.clipRect(mWaterRect);

//描绘水泡
drawBubbles(canvas);

优化

我们知道,在进行频繁的创建水泡的时候,如果每次都创建新对象的话, 可能会增加不必要的内存使用,而且很容易引起频繁的gc,甚至是内存抖动。

因此这里我增加了一个回收功能

//首先判断栈中是否存在回收的对象,若存在,则直接复用,若不存在,则创建一个新的对象
private Bubble obtainBubble(){
    if (mRecycler.isEmpty()){
         return new Bubble();
    }
    
    return mRecycler.pop();
}

//回收到一个栈里面,若这个栈数量超过最大可显示数量,则pop
private void recycle(Bubble bubble){
    if (bubble == null){
        return;
    }
    
    if (mRecycler.size() >= mBubbleMaxNumber){
        mRecycler.pop();
    }
    
    mRecycler.push(bubble);
}

Github

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

推荐阅读更多精彩内容