最近項目中要實現加速球效果。
是時候該學習一波了,好了廢話不多說,記筆記,還是從自己發憷的自定義view開始。
先來研究Path類。
官方定義:
Path類封裝了由直線段,二次曲線和三次曲線組成的復合(多輪廓)幾何路徑。它可以用canvas.drawPath(路徑,畫筆)繪制,填充或描邊(基于畫筆的樣式),或者可以用于剪裁或在路徑上繪制文本。
The Path class encapsulates compound (multiple contour) geometric paths consisting of straight line segments, quadratic curves, and cubic curves.It can be drawn with canvas.drawPath(path, paint), either filled or stroked (based on the paint's Style), or it can be used for clipping or to draw text on a path.
Path.Direction
Path.Direction
指定封閉的形狀(例如,直角,橢圓形)在添加到路徑時的方向
Path.Direction.CCW : 繪制封閉path時的繪制方向為逆時針閉合 。
Path.Direction.CW : 繪制封閉path時的繪制方向為順時針 閉合。
猛的一看,貌似有點懂(理解為畫路徑時的方向)。細想想有點蒙B。好了代碼演示一下就一目了然。
private void initPaint() {
//初始化畫筆
mPaint = new Paint();
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
}
private void initPath() {
//初始化path
mPath = new Path();
}
private void drawDirectionPath(Canvas canvas){
mPath.addCircle(200, 100, 100, Path.Direction.CCW);
canvas.drawPath(mPath,mPaint);
canvas.drawTextOnPath("11111122222222222222222233333333333333333344444",mPath,0,0,mPaint);
canvas.save();
canvas.translate(0,200);
mPath.rewind();
mPath.addCircle(200, 100, 100, Path.Direction.CW);
canvas.drawPath(mPath,mPaint);
canvas.drawTextOnPath("11111122222222222222222233333333333333333344444",mPath,0,0,mPaint);
canvas.restore();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawDirectionPath(canvas);
}
效果圖:
接下來研究下一知識點(通過Direction可以幫助我們理解奇偶原則和非零環繞數原則)
Path.FillType
- EVEN_ODD :Specifies that "inside" is computed by an odd number of edge crossings.
- WINDING:Specifies that "inside" is computed by a non-zero sum of signed edge crossings.
- INVERSE_EVEN_ODD:Same as EVEN_ODD, but draws outside of the path, rather than inside.
- INVERSE_WINDING:Same as WINDING, but draws outside of the path, rather than inside
我們先來科普一下奇偶原則和非零環繞數原則。摘自非零環繞數規則和奇-偶規則(Non-Zero Winding Number Rule&&Odd-even Rule)
在圖形學中判斷一個點是否在多邊形內,若多邊形不是自相交的,那么可以簡單的判斷這個點在多邊形內部還是外部;若多邊形是自相交的,那么就需要根據非零環繞數規則和奇-偶規則判斷。
判斷多邊形是否是自相交的:多邊形在平面內除頂點外還有其他公共點。
不自交的多邊形:多邊形僅在頂點處連接,而在平面內沒有其他公共點,此時可以直接劃分內-外部分。 自相交的多邊形:多邊形在平面內除頂點外還有其他公共點,此時劃分內-外部分需要采用以下的方法。
(1)奇-偶規則(Odd-even Rule):奇數表示在多邊形內,偶數表示在多邊形外。
從任意位置p作一條射線,若與該射線相交的多邊形邊的數目為奇數,則p是多邊形內部點,否則是外部點。
(2)非零環繞數規則(Nonzero Winding Number Rule):若環繞數為0表示在多邊形外,非零表示在多邊形內。
首先使多邊形(通俗講多邊形沿著某個方向一筆畫下來的)的邊變為矢量。將環繞數初始化為零。再從任意位置p作一條射線。當從p點沿射線方向移動時,對在每個方向上穿過射線的邊計數,每當多邊形的邊從右到左穿過射線時(順時針),環繞數加1,從左到右時(或者逆時針),環繞數減1。處理完多邊形的所有相關邊之后,若環繞數為非零,則p為內部點,否則,p是外部點。
參考[1]中例子如下,
判斷點p是否在多邊形內,從點p向外做一條射線(可以任意方向),多邊形的邊從左到右經過射線時環數減1,多邊形的邊從右往左經過射線時環數加1,最后環數不為0,即表示在多邊形內部。
當然,非零繞數規則和奇偶規則會判斷出現矛盾的情況,如下圖所示,左側表示用 奇偶規則判斷繞環數為2 ,表示在多邊形外,所以沒有填充。右側圖用非零繞環規則判斷出繞數為2,非0表示在多邊形內部,所以填充。
另外一個例子,如下
好吧,上代碼驗證。
public void showPathWithFillType(Canvas canvas,int offsetX,int offsetY,Path.FillType type, Path.Direction centerCircleDirection){
canvas.save();
canvas.translate(offsetX,offsetY);
mPath.reset();
mPath.addCircle(50,50,50,Path.Direction.CW );
mPath.addCircle(100,100,50,centerCircleDirection);
mPath.addCircle(150,150,50,Path.Direction.CW );
mPath.setFillType(type);
canvas.drawPath(mPath,mPaint);
canvas.restore();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
showPathWithFillType(canvas ,0,0,Path.FillType.EVEN_ODD, Path.Direction.CW);
showPathWithFillType(canvas,210,0,Path.FillType.EVEN_ODD, Path.Direction.CCW);
showPathWithFillType(canvas,0,210,Path.FillType.WINDING, Path.Direction.CW);
showPathWithFillType(canvas,210,210,Path.FillType.WINDING, Path.Direction.CCW);
showPathWithFillType(canvas,0,420,Path.FillType.INVERSE_EVEN_ODD, Path.Direction.CW);
showPathWithFillType(canvas,210,420,Path.FillType.INVERSE_EVEN_ODD, Path.Direction.CCW);
showPathWithFillType(canvas,0,630,Path.FillType.INVERSE_WINDING, Path.Direction.CW);
showPathWithFillType(canvas,210,630,Path.FillType.INVERSE_WINDING, Path.Direction.CCW);
}
效果圖:
左側所有圓閉合方向為順時針方向。右側兩側閉合圓為順時針方向,中間圓為逆時針方向。對于EVEN_ODD模式來說,與閉合方向沒關系。對于WINDING模式來說,閉合方向不同穿過任意射線的方向不同。故出現上述現象。
Path.Op
Path.Op.DIFFERENCE 減去path1中path1與path2都存在的部分;
path1 = (path1 - path1 ∩ path2)
Path.Op.INTERSECT 保留path1與path2共同的部分;
path1 = path1 ∩ path2
Path.Op.UNION 取path1與path2的并集;
path1 = path1 ∪ path2
Path.Op.REVERSE_DIFFERENCE 與DIFFERENCE剛好相反;
path1 = path2 - (path1 ∩ path2)
Path.Op.XOR 與INTERSECT剛好相反 ;
path1 = (path1 ∪ path2) - (path1 ∩ path2)
private void drawOpPath(Canvas canvas) {
resetOp(canvas, 0, 0, Path.Op.DIFFERENCE);
resetOp(canvas, 100, 0, Path.Op.REVERSE_DIFFERENCE);
resetOp(canvas, 0, 100, Path.Op.INTERSECT);
resetOp(canvas, 100, 100, Path.Op.UNION);
resetOp(canvas, 0, 200, Path.Op.XOR);
}
public void resetOp(Canvas canvas, int offsetX, int offsetY, Path.Op op) {
mPath.rewind();
mOpPath.rewind();
canvas.save();
canvas.translate(offsetX, offsetY);
mPath.addCircle(25, 25, 25, Path.Direction.CW);
mOpPath.addCircle(50, 50, 25, Path.Direction.CCW);
mPath.op(mOpPath, op);
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawOpPath(canvas);
}
效果圖:
這里強調一點:因為path.op是Api19新增的,所以想要在Api19以下實現同樣功能需要 采用畫布的clip實現
private void resetOp(Canvas canvas, int offsetX, int offsetY, Region.Op op) {
mPath.rewind();
mOpPath.rewind();
canvas.save();
canvas.translate(offsetX, offsetY);
mPath.addCircle(25, 25, 25, Path.Direction.CW);
mOpPath.addCircle(50, 50, 25, Path.Direction.CW);
canvas.clipPath(mPath);
canvas.clipPath(mOpPath, op);
canvas.drawColor(Color.parseColor("#ffaa66cc"));
canvas.restore();
}
接下來研究Path的各個方法:
[addArc](https://developer.android.google.cn/reference/android/graphics/Path.html#addArc(float, float, float, float, float, float))(float left, float top, float right, float bottom, float startAngle, float sweepAngle)
Add the specified arc to the path as a new contour.(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP)
[addArc](https://developer.android.google.cn/reference/android/graphics/Path.html#addArc(android.graphics.RectF, float, float))(RectF oval, float startAngle, float sweepAngle)
Add the specified arc to the path as a new contour.
[arcTo](https://developer.android.google.cn/reference/android/graphics/Path.html#arcTo(android.graphics.RectF, float, float, boolean))(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
Append the specified arc to the path as a new contour.
[arcTo](https://developer.android.google.cn/reference/android/graphics/Path.html#arcTo(float, float, float, float, float, float, boolean))(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP)
Append the specified arc to the path as a new contour.
上述兩對方法有兩個在5.0后新增的重載方法,下面我們來詳細介紹這兩對方法作用與區別。
第一對,以Add方式:
/**
* 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);
}
上過學的應該都知道橢圓公式(不知道的自己科普),第一個參數的由來。第二個參數為橢圓的起始度數,第二個參數為橢圓的結束度數。從橢圓右側定點開始為0度基準點,順時針方向增加。
public void addArc(Canvas canvas){
if( Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
mPath.addArc(10,10,200,180,10,70);
}else{
mRect.set(10,10,200,180);
mPath.addArc(mRect,10,70);
}
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawPath(mPath,mPaint);
}
第二對,以arcTo方式:
/**
* Append the specified arc to the path as a new contour. If the start of
* the path is different from the path's current last point, then an
* automatic lineTo() is added to connect the current contour to the
* start of the arc. However, if the path is empty, then we call moveTo()
* with the first point of the are.
*
* @param oval The bounds of oval defining shape and size of the arc
* @param startAngle Starting angle (in degrees) where the arc begins
* @param sweepAngle Sweep angle (in degrees) measured clockwise, treated
* mod 360.
* @param forceMoveTo If true, always begin a new contour with the arc
*/
public void arcTo(RectF oval, float startAngle, float sweepAngle,
boolean forceMoveTo) {
arcTo(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, forceMoveTo);
}
我們發現,arcTo比addArc 多以個參數,干啥用的呢?看注釋 ** If true, always begin a new contour with the arc** 如果為forceMoveTo==true ,會與addArc方式相同,以一個新的輪廓添加到path中;如果為false呢?接著看方法注釋:
Append the specified arc to the path as a new contour. If the start of the path is different from the path's >current last point, then an automatic lineTo() is added to connect the current contour to the start of the arc.However, if the path is empty, then we call moveTo() with the first point of the are.
將指定的弧線附加到路徑作為一個新的輪廓。(forceMoveTo false 的情況下)如果圓弧的起始點與上次path的結束點不相同,則在上次結束點的基礎上調用lineTo() 連接到圓弧的其實點。如果Path 重置或者調用new Path()方法,則首先會調用moveTo() 到圓弧的其實點。
public void addArc(Canvas canvas){
mPath.lineTo(20,20);
if( Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
mPath.arcTo(10,10,200,180,10,70,false);
}else{
mRect.set(10,10,200,180);
mPath.arcTo(mRect,10,70,false);
}
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawPath(mPath,mPaint);
}
暫時解釋到這里吧,用的過程中慢慢領會!
[addCircle](https://developer.android.google.cn/reference/android/graphics/Path.html#addCircle(float, float, float, android.graphics.Path.Direction))(float x, float y, float radius, Path.Direction dir)
Add a closed circle contour to the path
[addOval](https://developer.android.google.cn/reference/android/graphics/Path.html#addOval(android.graphics.RectF, android.graphics.Path.Direction))(RectF oval, Path.Direction dir)
Add a closed oval contour to the path
[addOval](https://developer.android.google.cn/reference/android/graphics/Path.html#addOval(float, float, float, float, android.graphics.Path.Direction))(float left, float top, float right, float bottom, Path.Direction dir)
Add a closed oval contour to the path
addPath(Path src)
Add a copy of src to the path
void addRoundRect (RectF rect, float[] radii, Path.Direction dir)
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
public void computeBounds ([RectF](file:///D:/android/android-sdk-windows/docs/reference/android/graphics/RectF.html) bounds, boolean exact)
計算path中控制的點的邊界,將結果寫入bounds中,如果Path中只有0或者1個點,那么bounds會返回(0,0,0,0)的值,exact這個變量暫時沒用,其實就是path的最邊界點到X 軸 Y軸垂線與XY軸的交點。通俗的講就>是一個矩形正好將這個path包裹起來。
上述大家應該比較容易理解吧,在此就不做贅述!
quadTo
void quadTo (float x1, float y1, float x2, float y2)
Add a quadratic bezier from the last point, approaching control point (x1,y1), and ending at (x2,y2). If no >moveTo() call has been made for this contour, the first point is automatically set to (0,0).
cubicTo
void cubicTo (float x1, float y1, float x2, float y2, float x3, float y3)
Add a cubic bezier from the last point, approaching control points (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been made for this contour, the first point is automatically set to (0,0).
二階、 三階貝賽爾曲線自己暫時只會簡單的使用,后續會補上Demo 加速球。
詳細了解貝賽爾曲線,有個大神博客已經寫得非常詳細.
isConvex
boolean isConvex ()
Returns the path's convexity, as defined by the content of the path.
A path is convex if it has a single contour, and only ever curves in a single direction.
This function will calculate the convexity of the path from its control points, and cache the result.
返回由路徑的內容定義的路徑的凸度。如果路徑具有單個輪廓,則路徑是凸的,并且僅在單個方向上曲線。
該函數將從其控制點計算路徑的凸度,并緩存結果。
哪個大神用過上邊這個方法?不知道這個具體的應用場景!
Path.FillType getFillType ()
Return the path's fill type. This defines how "inside" is computed. The default value is WINDING.
獲取path的FillType,前面已做解釋。
isEmpty()
Returns true if the path is empty (contains no lines or curves)
如果path不包含任何直線或者曲線 返回 true
isInverseFillType()
Returns true if the filltype is one of the INVERSE variants
如果填充類型是經過反向變換的,返回true。
isRect(RectF rect)
Returns true if the path specifies a rectangle.
判斷是否是rect
[lineTo](https://developer.android.google.cn/reference/android/graphics/Path.html#lineTo(float, float))(float x, float y)
Add a line from the last point to the specified point (x,y).
從lastPoint 點直線連接到(x,y)
[moveTo](https://developer.android.google.cn/reference/android/graphics/Path.html#moveTo(float, float))(float x, float y)
Set the beginning of the next contour to the point (x,y).
設置下一個直線 曲線 閉合回路的起始點
[offset](https://developer.android.google.cn/reference/android/graphics/Path.html#offset(float, float, android.graphics.Path))(float dx, float dy, Path dst)
[offset](https://developer.android.google.cn/reference/android/graphics/Path.html#offset(float, float))(float dx, float dy)
Offset the path by (dx,dy)
兩個方法見名知意,path偏移多少多少
[op](https://developer.android.google.cn/reference/android/graphics/Path.html#op(android.graphics.Path, android.graphics.Path, android.graphics.Path.Op))(Path path1, Path path2, Path.Op op)
Set this path to the result of applying the Op to the two specified paths.
path1 與path2 進行 Path.Op op)
Set this path to the result of applying the Op to the two specified paths.運算,結果保存在當前path中。
[op](https://developer.android.google.cn/reference/android/graphics/Path.html#op(android.graphics.Path, android.graphics.Path.Op))(Path path, Path.Op op)
當前path與參數中path進行[op](https://developer.android.google.cn/reference/android/graphics/Path.html#op(android.graphics.Path, android.graphics.Path.Op))運算。
[rCubicTo](https://developer.android.google.cn/reference/android/graphics/Path.html#rCubicTo(float, float, float, float, float, float))(float x1, float y1, float x2, float y2, float x3, float y3)、[rLineTo](https://developer.android.google.cn/reference/android/graphics/Path.html#rLineTo(float, float))(float dx, float dy)、[rMoveTo](https://developer.android.google.cn/reference/android/graphics/Path.html#rMoveTo(float, float))(float dx, float dy)、[rQuadTo](https://developer.android.google.cn/reference/android/graphics/Path.html#rQuadTo(float, float, float, float))(float dx1, float dy1, float dx2, float dy2)
不帶r的方法是基于原點的坐標系(偏移量), rXxx方法是基于當前點坐標系(偏移量)
reset rewind 區別
清除Path中的內容,“FillType”影響的是顯示效果,“數據結構”影響的是重建速度。
reset不保留內部數據結構,但會保留FillType.
rewind會保留內部的數據結構,但不保留FillType
reset()
/**
* Clear any lines and curves from the path, making it empty.
* This does NOT change the fill-type setting.
*/
public void reset() {
isSimplePath = true;
mLastDirection = null;
if (rects != null) rects.setEmpty();
// We promised not to change this, so preserve it around the native
// call, which does now reset fill type.
final FillType fillType = getFillType();
native_reset(mNativePath);
setFillType(fillType);
}
final FillType fillType = getFillType();
setFillType(fillType);
rewind()
/**
* Rewinds the path: clears any lines and curves from the path but
* keeps the internal data structure for faster reuse.
*/
public void rewind() {
isSimplePath = true;
mLastDirection = null;
if (rects != null) rects.setEmpty();
native_rewind(mNativePath);
}
矩陣變換
[transform](https://developer.android.google.cn/reference/android/graphics/Path.html#transform(android.graphics.Matrix, android.graphics.Path))(Matrix matrix, Path dst)
transform(Matrix matrix)
接下來把工作中實現加速球效果的demo給大家分享一下,有什么寫的不拖地兒,歡迎指導,大家相互學習,圖片比例是按照UI圖的比例畫的
先看效果
代碼:
package cn.laifrog;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Shader;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;
/**
* author: laifrog
* time:2017/6/3.
* description:
*/
public class SpeedBallView extends View {
private static final String TAG = "SpeedBallView";
private static final String KEY_BUNDLE_SUPER_DATA = "key_bundle_super_data"; //程序崩潰時回復數據
private static final String KEY_BUNDLE_PROGRESS = "key_bundle_progress"; //回復progress
public static final int DEFAULT_WAVE_COUNT = 2;
public static final int DEFAULT_MAX_PROGRESS = 100;
public static final float DEFAULT_MAX_SWING_RATIO = 0.08f; //振幅占用圓球的的比例
public static final float DEFAULT_MIN_SWING_RATIO = 0.025f; //振幅占用圓球的的比例
public static final float DEFAULT_INSIDE_CIRCLE_STROKE_WIDTH_RATIO = 0.015f;//內圓環寬度比例
public static final float DEFAULT_OUTSIDE_CIRCLE_STROKE_WIDTH_RATIO = 0.015f;//外圓環寬度比例
public static final float DEFAULT_INSIDE_CIRCLE_RADIUS_RATIO = 0.45f;
private Paint mWavePaint;
private Paint mCirclePaint;
private Path mForwardWavePath;
private Path mBackWavePath;
private Path mCircleClipPath;
private Path mOutsideCirclePath;
private Path mInsideCirclePath;
private LinearGradient mWaveShader;
private LinearGradient mLowWaveShader;
private LinearGradient mMiddleWaveShader;
private LinearGradient mHighWaveShader;
private ColorMatrixColorFilter mColorMatrixColorFilter;
private int mWaveCount = DEFAULT_WAVE_COUNT;//一個view能容納的波長個數
private int mWaveLength; //波長長度
private int mWaveSwing; // 振幅
private int mOffsetX; //偏移量
private int mSecondOffsetX; //第二個波長偏移量
private int mProgress; //進度
private int mMaxProgress = DEFAULT_MAX_PROGRESS; //進度最大值
// 暫時不考慮padding
private int mWidth;
private int mHeight;
private float mInsideCircleStrokeWidth;
private float mOutsideCircleStrokeWidth;
private float mInsideCircleRadius;
private float mOutsideCircleRadius;
private int mInsideCircleColor;
private int mOutsideCircleColor;
//不同百分比的漸變色
private int[] mGreenColor;
private int[] mOrangeColor;
private int[] mRedColor;
private boolean isStopWave;
private ValueAnimator mWaveAnimator;
private ValueAnimator mSecondAnimator;
private ValueAnimator mWaveSwingAnimator;
public SpeedBallView(Context context) {
this(context, null);
}
public SpeedBallView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SpeedBallView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
initPaint();
initPath();
initData();
}
private void initData() {
mWaveLength = mWidth / mWaveCount;
mWaveSwing = (int) (mHeight * DEFAULT_MIN_SWING_RATIO);
int maxWidth = Math.min(mWidth, mHeight);
//外圓半徑及strokewidth
mOutsideCircleStrokeWidth = maxWidth * DEFAULT_OUTSIDE_CIRCLE_STROKE_WIDTH_RATIO;
mOutsideCircleRadius = maxWidth * 0.5f - mOutsideCircleStrokeWidth * 0.5f;
//內圓半徑及strokewidth
mInsideCircleStrokeWidth = maxWidth * DEFAULT_INSIDE_CIRCLE_STROKE_WIDTH_RATIO;
mInsideCircleRadius = maxWidth * DEFAULT_INSIDE_CIRCLE_RADIUS_RATIO
- mInsideCircleStrokeWidth * 0.5f;
//內圓外圓的path
mOutsideCirclePath.addCircle(mWidth * 0.5f, mHeight * 0.5f, mOutsideCircleRadius,
Path.Direction.CCW);
mInsideCirclePath.addCircle(mWidth * 0.5f, mHeight * 0.5f, mInsideCircleRadius,
Path.Direction.CCW);
//op 的圓
mCircleClipPath.addCircle(mWidth * 0.5f, mHeight * 0.5f,
mInsideCircleRadius - mInsideCircleStrokeWidth
* 0.5f, Path.Direction.CCW);
mForwardWavePath = calculateWavePath(mForwardWavePath, 0);
mBackWavePath = calculateWavePath(mBackWavePath, 0);
}
private void init() {
mInsideCircleColor = Color.argb(0xcc, 0xff, 0xff, 0xff);
mOutsideCircleColor = Color.argb(0x33, 0xff, 0xff, 0xff);
mGreenColor = new int[2];
mOrangeColor = new int[2];
mRedColor = new int[2];
Resources resource = getContext().getResources();
mGreenColor[0] = resource.getColor(R.color.color_wave_green_up);
mGreenColor[1] = resource.getColor(R.color.color_wave_green_down);
mOrangeColor[0] = resource.getColor(R.color.color_wave_orange_up);
mOrangeColor[1] = resource.getColor(R.color.color_wave_orange_down);
mRedColor[0] = resource.getColor(R.color.color_wave_red_up);
mRedColor[1] = resource.getColor(R.color.color_wave_red_down);
mColorMatrixColorFilter = new ColorMatrixColorFilter(new ColorMatrix(new float[]{
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 0.5f, 0,
}));
}
private void initPath() {
mForwardWavePath = new Path();
mBackWavePath = new Path();
mOutsideCirclePath = new Path();
mInsideCirclePath = new Path();
mCircleClipPath = new Path();
}
private void initPaint() {
mWavePaint = new Paint();
mWavePaint.setAntiAlias(true);
mWavePaint.setDither(true);
mWavePaint.setStyle(Paint.Style.FILL);
mCirclePaint = new Paint();
mCirclePaint.setDither(true);
mCirclePaint.setAntiAlias(true);
mCirclePaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
initData();
//初始化加速球漸變色
mLowWaveShader = new LinearGradient(0, 0, 0, mHeight, mGreenColor, null,
Shader.TileMode.CLAMP);
mMiddleWaveShader = new LinearGradient(0, 0, 0, mHeight, mOrangeColor, null,
Shader.TileMode.CLAMP);
mHighWaveShader = new LinearGradient(0, 0, 0, mHeight, mRedColor, null,
Shader.TileMode.CLAMP);
updateWaveShader();
}
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
Parcelable superState = super.onSaveInstanceState();
bundle.putParcelable(KEY_BUNDLE_SUPER_DATA, superState);
bundle.putInt(KEY_BUNDLE_PROGRESS, mProgress);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
Bundle restoreData = (Bundle) state;
Parcelable superData = (Parcelable) restoreData.get(KEY_BUNDLE_SUPER_DATA);
super.onRestoreInstanceState(superData);
mProgress = restoreData.getInt(KEY_BUNDLE_PROGRESS);
updateWaveShader();
}
/**
* 更改shader的色值.
*/
public void updateWaveShader() {
if (mProgress < 30) {
mWaveShader = mLowWaveShader;
} else if (mProgress >= 30 && mProgress < 80) {
mWaveShader = mMiddleWaveShader;
} else {
mWaveShader = mHighWaveShader;
}
}
private void drawCircle(Canvas canvas, Path circlePath, Paint circlePaint) {
canvas.drawPath(circlePath, circlePaint);
}
private Path calculateWavePath(Path wavePath, float offsetX) {
wavePath.reset();
//移動初始位置為width
wavePath.moveTo(-mWidth + offsetX, calculateWaveHeight());
//水波浪線
for (int i = 0; i < mWaveCount * 2; i++) {
wavePath.quadTo(
-(mWaveCount * mWaveLength) + (0.25f * mWaveLength) + (i * mWaveLength) + offsetX,
calculateWaveHeight() + mWaveSwing,
-(mWaveCount * mWaveLength) + (0.5f * mWaveLength) + (i * mWaveLength) + offsetX,
calculateWaveHeight());
wavePath.quadTo(
-(mWaveCount * mWaveLength) + (0.75f * mWaveLength) + (i * mWaveLength) + offsetX,
calculateWaveHeight() - mWaveSwing,
-(mWaveCount * mWaveLength) + mWaveLength + (i * mWaveLength) + offsetX,
calculateWaveHeight());
}
wavePath.lineTo(mWidth, mHeight);
wavePath.lineTo(-mWaveCount * mWaveLength + offsetX, mHeight);
wavePath.close();
//path 運算
wavePath.op(mCircleClipPath, Path.Op.INTERSECT);
return wavePath;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int size = 0;
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (MeasureSpec.EXACTLY == widthSpecMode || MeasureSpec.EXACTLY == heightSpecMode) {
size = Math.min(widthSize, heightSize);
} else {
// TODO: 2017/5/12
}
setMeasuredDimension(size, size);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//draw white background
mCirclePaint.setARGB(0x33, 0xff, 0xff, 0xff);
mCirclePaint.setStyle(Paint.Style.FILL);
drawCircle(canvas, mCircleClipPath, mCirclePaint);
//draw forward wave
mWavePaint.setColorFilter(null);
mWavePaint.setShader(mWaveShader);
canvas.drawPath(mForwardWavePath, mWavePaint);
//draw back wave
mWavePaint.setShader(mWaveShader);
mWavePaint.setColorFilter(mColorMatrixColorFilter);
canvas.drawPath(mBackWavePath, mWavePaint);
//draw inside circle
mCirclePaint.setColor(mInsideCircleColor);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setStrokeWidth(mInsideCircleStrokeWidth);
drawCircle(canvas, mInsideCirclePath, mCirclePaint);
//draw outside circle
mCirclePaint.setColor(mOutsideCircleColor);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setStrokeWidth(mOutsideCircleStrokeWidth);
drawCircle(canvas, mOutsideCirclePath, mCirclePaint);
}
public float calculateWaveHeight() {
float clipCircleRadius = mInsideCircleRadius - mInsideCircleStrokeWidth * 0.5f;
float waveHeight = (mHeight * 0.5f - clipCircleRadius) + (2 * clipCircleRadius)
- (2 * clipCircleRadius) * mProgress / mMaxProgress;
if (mProgress >= mMaxProgress) {
waveHeight = -mWaveSwing;
} else if (mProgress <= 0) {
waveHeight = mHeight + mWaveSwing;
}
return waveHeight;
}
public void startWave() {
isStopWave = false;
if (mWaveAnimator != null) {
mWaveAnimator.cancel();
}
if (mSecondAnimator != null) {
mSecondAnimator.cancel();
}
if (mWaveSwingAnimator != null) {
mWaveSwingAnimator.cancel();
}
mWaveAnimator = ValueAnimator.ofInt(0, mWidth);
mWaveAnimator.setDuration(1500);
mWaveAnimator.setRepeatCount(ValueAnimator.INFINITE);
mWaveAnimator.setInterpolator(new LinearInterpolator());
mWaveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (isStopWave && Math.abs(
(float) mWaveSwingAnimator.getAnimatedValue() - DEFAULT_MIN_SWING_RATIO)
<= 0.002f) {
mWaveAnimator.cancel();
}
mOffsetX = (int) animation.getAnimatedValue();
mForwardWavePath = calculateWavePath(mForwardWavePath, mOffsetX);
invalidate();
}
});
mSecondAnimator = ValueAnimator.ofInt(0, mWidth);
mSecondAnimator.setDuration(2000);
mSecondAnimator.setRepeatCount(ValueAnimator.INFINITE);
mSecondAnimator.setInterpolator(new LinearInterpolator());
mSecondAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (isStopWave && Math.abs(
(float) mWaveSwingAnimator.getAnimatedValue() - DEFAULT_MIN_SWING_RATIO)
<= 0.002f) {
mSecondAnimator.cancel();
}
mSecondOffsetX = (int) animation.getAnimatedValue();
mBackWavePath = calculateWavePath(mBackWavePath, mSecondOffsetX);
invalidate();
}
});
mWaveSwingAnimator = ValueAnimator.ofFloat(DEFAULT_MIN_SWING_RATIO, DEFAULT_MAX_SWING_RATIO,
DEFAULT_MIN_SWING_RATIO);
mWaveSwingAnimator.setDuration(5000);
mWaveSwingAnimator.setInterpolator(new LinearInterpolator());
mWaveSwingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float swing = (float) animation.getAnimatedValue();
if (isStopWave && Math.abs(swing - DEFAULT_MIN_SWING_RATIO) <= 0.002f) {
mWaveAnimator.cancel();
}
mWaveSwing = (int) (mHeight * swing);
invalidate();
}
});
mSecondAnimator.start();
mWaveAnimator.start();
mWaveSwingAnimator.start();
}
public void stopWave() {
isStopWave = true;
}
public void setProgress(final int progress) {
if (progress == mProgress) {
return;
}
mProgress = progress;
updateWaveShader();
postInvalidate();
}
public int getProgress() {
return mProgress;
}
public void setMaxProgress(int maxProgress) {
mMaxProgress = maxProgress;
postInvalidate();
}
public int getMaxProgress() {
return mMaxProgress;
}
}
public class MainActivity extends AppCompatActivity {
private SpeedBallView mSpeedBallView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
mSpeedBallView = (SpeedBallView) findViewById(R.id.main_speed_ball);
mSpeedBallView.setProgress(50);
mSpeedBallView.post(new Runnable() {
@Override
public void run() {
mSpeedBallView.startWave();
}
});
}
}
<!--加速球漸變色-->
<color name="color_wave_green_down">#4e961c</color>
<color name="color_wave_green_up">#87c552</color>
<color name="color_wave_orange_down">#ae6c18</color>
<color name="color_wave_orange_up">#ecd25a</color>
<color name="color_wave_red_down">#b7250e</color>
<color name="color_wave_red_up">#ec4a25</color>