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); }