Android 自定義View之咖啡杯動畫
效果
CoffeeView

CoffeeView
大概思路
- 自定義view,直接繼承view
- 複寫onSizeChanged()方法,在此計算杯墊,杯子,煙霧效果的path
- 在onDraw()方法中,描繪杯墊,杯子
- 處理煙霧動畫效果
畫杯子
這裡需要畫兩部分內容,第一部分是杯子,第二部分是杯耳(提手的地方)
我們可以使用addRoundRect來描繪圓角矩形,並且可指定每個圓角的半徑即圓角的程度
/** * Add a closed round-rectangle contour to the path. Each corner receives * two radius values [X, Y]. The corners are ordered top-left, top-right, * bottom-right, bottom-left * * @param rect The bounds of a round-rectangle to add to the path * @param radii Array of 8 values, 4 pairs of [X,Y] radii * @param dirThe direction to wind the round-rectangle's contour */ public void addRoundRect(RectF rect, float[] radii, Direction dir) { if (rect == null) { throw new NullPointerException("need rect parameter"); } addRoundRect(rect.left, rect.top, rect.right, rect.bottom, radii, dir); }
以下程式碼註釋:
//計算view的中心點座標 mCenterX = w / 2; mCenterY = h / 2; //杯子的寬,為view的寬度的2/3 float cupWidth= w * 2 / 3f; //杯子的高,為view的高度3/8 float cupHeight = (h / 2) * 3 / 4f; //計算出杯子的中心點座標 float cupCenterX = mCenterX; float cupCenterY = mCenterY + cupHeight / 2; //計算杯子矩形的左上右上的圓角半徑 float cupTopRoundRadius = Math.min(cupWidth, cupHeight) / 20f; //計算杯子矩形的左下右下的圓角半徑 float cupBottomRoundRadius = cupTopRoundRadius * 10; //重置杯子path mCupPath.reset(); //添加杯子(杯身)軌跡 mCupPath.addRoundRect(new RectF(cupCenterX - cupWidth / 2, cupCenterY - cupHeight / 2 - cupHeight / 10, cupCenterX + cupWidth / 2, cupCenterY + cupHeight / 2), new float[]{cupTopRoundRadius, cupTopRoundRadius, cupTopRoundRadius, cupTopRoundRadius,cupBottomRoundRadius, cupBottomRoundRadius, cupBottomRoundRadius, cupBottomRoundRadius}, Path.Direction.CW); //計算杯耳寬度 float cupEarWidth= (w - cupWidth) * 3 / 4f; //計算杯耳高度 float cupEarHeight = cupHeight / 3; //計算杯耳的中心點座標 float cupEarCenterX = mCenterX + cupWidth / 2; float cupEarCenterY = mCenterY + cupHeight / 2; //計算杯耳的圓角半徑 float cupEarRoundRadius = Math.min(cupEarWidth, cupEarHeight) / 2f; //設定杯耳畫筆的描邊寬度 mCupEarPaint.setStrokeWidth(Math.min(cupEarWidth, cupEarHeight) / 3f); //重置杯耳path mCupEarPath.reset(); //添加杯耳軌跡 mCupEarPath.addRoundRect(new RectF(cupEarCenterX - cupEarWidth / 2, cupEarCenterY - cupEarHeight / 2 - cupHeight / 10, cupEarCenterX + cupEarWidth / 2, cupEarCenterY + cupEarHeight / 2), new float[]{cupEarRoundRadius, cupEarRoundRadius, cupEarRoundRadius, cupEarRoundRadius, cupEarRoundRadius, cupEarRoundRadius, cupEarRoundRadius, cupEarRoundRadius}, Path.Direction.CW);
在onDraw方法中
canvas.drawPath(mCupEarPath, mCupEarPaint); canvas.drawPath(mCupPath, mCupPaint);
畫杯墊
首先計算杯墊path軌跡
//計算杯墊寬度 float coasterWidth = cupWidth; //計算杯墊高度 float coasterHeight = (h / 2 - cupHeight) * 1 / 3f; //計算杯墊中心點座標 float coasterCenterX = mCenterX; float coasterCenterY = mCenterY + cupHeight + (h / 2 - cupHeight) / 2f; //計算杯墊圓角半徑 float coasterRoundRadius = Math.min(coasterWidth, coasterHeight) / 2f; //重置杯墊path mCoasterPath.reset(); //添加杯墊軌跡 mCoasterPath.addRoundRect(new RectF(coasterCenterX - coasterWidth / 2, coasterCenterY - coasterHeight / 2, coasterCenterX + coasterWidth / 2, coasterCenterY + coasterHeight / 2), coasterRoundRadius, coasterRoundRadius, Path.Direction.CW);
在onDraw方法中
canvas.drawPath(mCoasterPath, mCoasterPaint);
畫煙霧
煙霧原理
- 根據貝塞爾曲線新增波浪軌跡
- 根據LinearGradient實現顏色漸變效果
每條煙霧大概如下效果

CoffeeView
當移動至煙霧底部的時候,重新將其移動至頭部,這樣迴圈動畫,就會顯示無線的滾動效果
程式碼註釋如下:
//計算煙霧的寬度 float vaporsStrokeWidth = cupWidth / 15f; //計算煙霧相隔距離大小 float vaporsGapWidth = (cupWidth - VAPOR_COUNT * vaporsStrokeWidth) / 4f; mVaporsHeight= cupHeight * 4 / 5f; //設定煙霧畫筆描邊大小 mVaporPaint.setStrokeWidth(vaporsStrokeWidth); float startX, startY, stopX, stopY; //設定漸變效果 LinearGradient linearGradient = new LinearGradient(mCenterX, mCenterY, mCenterX, mCenterY - mVaporsHeight, new int[]{mVaporColor, Color.TRANSPARENT}, null, Shader.TileMode.CLAMP); //煙霧畫筆增加漸變效果的渲染器 mVaporPaint.setShader(linearGradient); //增加每條煙霧的path的貝塞爾波浪 for (int i = 0; i < VAPOR_COUNT; i++) { mVaporsPath[i].reset(); startX = (mCenterX - cupWidth / 2) + vaporsStrokeWidth / 2 + i * vaporsStrokeWidth + (i + 1) * vaporsGapWidth; startY = mCenterY + mVaporsHeight; stopX = startX; stopY = mCenterY - mVaporsHeight; mVaporsPath[i].moveTo(startX, startY); mVaporsPath[i].quadTo(startX - vaporsGapWidth / 2, startY - mVaporsHeight / 4, startX, startY - mVaporsHeight / 2); mVaporsPath[i].quadTo(startX + vaporsGapWidth / 2, startY - mVaporsHeight * 3 / 4, startX, mCenterY); mVaporsPath[i].quadTo(startX - vaporsGapWidth / 2, mCenterY - mVaporsHeight / 4, startX, mCenterY - mVaporsHeight / 2); mVaporsPath[i].quadTo(startX + vaporsGapWidth / 2, mCenterY - mVaporsHeight * 3 / 4, stopX, stopY); //add twice the bezier curve mVaporsPath[i].quadTo(startX - vaporsGapWidth / 2, stopY - mVaporsHeight / 4, startX, stopY - mVaporsHeight / 2); mVaporsPath[i].quadTo(startX + vaporsGapWidth / 2, stopY - mVaporsHeight * 3 / 4, startX, stopY - mVaporsHeight); mVaporsPath[i].quadTo(startX - vaporsGapWidth / 2, stopY - mVaporsHeight - mVaporsHeight / 4, startX, stopY - mVaporsHeight - mVaporsHeight / 2); mVaporsPath[i].quadTo(startX + vaporsGapWidth / 2, stopY - mVaporsHeight - mVaporsHeight * 3 / 4, stopX, stopY - 2 * mVaporsHeight); }
煙霧動畫處理:
每條煙霧都有一個path記錄其軌跡,利用path的transform方法可移動path
/** * Transform the points in this path by matrix, and write the answer * into dst. If dst is null, then the the original path is modified. * * @param matrix The matrix to apply to the path * @param dstThe transformed path is written here. If dst is null, *then the the original path is modified */ public void transform(Matrix matrix, Path dst) { long dstNative = 0; if (dst != null) { dst.isSimplePath = false; dstNative = dst.mNativePath; } nTransform(mNativePath, matrix.native_instance, dstNative); }
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, -vaporHeight); final int finalI = i; valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { mAnimatorValues[finalI] = (float) valueAnimator.getAnimatedValue(); invalidate(); } }); valueAnimator.setDuration(1000); valueAnimator.setInterpolator(new LinearInterpolator()); valueAnimator.setRepeatCount(ValueAnimator.INFINITE); valueAnimator.setRepeatMode(ValueAnimator.RESTART); mValueAnimators.add(valueAnimator);
在onDraw方法中,描繪煙霧
private void drawVapors(Canvas canvas){ for (int i = 0; i < VAPOR_COUNT; i++){ mCalculateMatrix.reset(); mCalculatePath.reset(); float animatedValue = mAnimatorValues[i]; mCalculateMatrix.postTranslate(0, animatedValue); mVaporsPath[i].transform(mCalculateMatrix, mCalculatePath); canvas.drawPath(mCalculatePath, mVaporPaint); } }
我這裡設定了每條煙霧的移動速度是一樣的,你們可以下載原始碼來修改,看看不同的移動速度的效果