最近項(xiàng)目中要實(shí)現(xiàn)加速球效果。
是時(shí)候該學(xué)習(xí)一波了绪商,好了廢話不多說,記筆記后德,還是從自己發(fā)憷的自定義view開始部宿。
先來研究Path類。
官方定義:
Path類封裝了由直線段瓢湃,二次曲線和三次曲線組成的復(fù)合(多輪廓)幾何路徑理张。它可以用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
指定封閉的形狀(例如落蝙,直角织狐,橢圓形)在添加到路徑時(shí)的方向
Path.Direction.CCW : 繪制封閉path時(shí)的繪制方向?yàn)槟鏁r(shí)針閉合 。
Path.Direction.CW : 繪制封閉path時(shí)的繪制方向?yàn)轫槙r(shí)針 閉合筏勒。
猛的一看移迫,貌似有點(diǎn)懂(理解為畫路徑時(shí)的方向)。細(xì)想想有點(diǎn)蒙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);
}
效果圖:
接下來研究下一知識點(diǎn)(通過Direction可以幫助我們理解奇偶原則和非零環(huán)繞數(shù)原則)
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
我們先來科普一下奇偶原則和非零環(huán)繞數(shù)原則。摘自非零環(huán)繞數(shù)規(guī)則和奇-偶規(guī)則(Non-Zero Winding Number Rule&&Odd-even Rule)
在圖形學(xué)中判斷一個(gè)點(diǎn)是否在多邊形內(nèi)捐顷,若多邊形不是自相交的荡陷,那么可以簡單的判斷這個(gè)點(diǎn)在多邊形內(nèi)部還是外部雨效;若多邊形是自相交的,那么就需要根據(jù)非零環(huán)繞數(shù)規(guī)則和奇-偶規(guī)則判斷废赞。
判斷多邊形是否是自相交的:多邊形在平面內(nèi)除頂點(diǎn)外還有其他公共點(diǎn)徽龟。
不自交的多邊形:多邊形僅在頂點(diǎn)處連接,而在平面內(nèi)沒有其他公共點(diǎn)唉地,此時(shí)可以直接劃分內(nèi)-外部分据悔。 自相交的多邊形:多邊形在平面內(nèi)除頂點(diǎn)外還有其他公共點(diǎn),此時(shí)劃分內(nèi)-外部分需要采用以下的方法渣蜗。
(1)奇-偶規(guī)則(Odd-even Rule):奇數(shù)表示在多邊形內(nèi)屠尊,偶數(shù)表示在多邊形外。
從任意位置p作一條射線耕拷,若與該射線相交的多邊形邊的數(shù)目為奇數(shù),則p是多邊形內(nèi)部點(diǎn)托享,否則是外部點(diǎn)骚烧。
(2)非零環(huán)繞數(shù)規(guī)則(Nonzero Winding Number Rule):若環(huán)繞數(shù)為0表示在多邊形外,非零表示在多邊形內(nèi)闰围。
首先使多邊形(通俗講多邊形沿著某個(gè)方向一筆畫下來的)的邊變?yōu)槭噶吭甙怼h(huán)繞數(shù)初始化為零。再從任意位置p作一條射線羡榴。當(dāng)從p點(diǎn)沿射線方向移動(dòng)時(shí)碧查,對在每個(gè)方向上穿過射線的邊計(jì)數(shù),每當(dāng)多邊形的邊從右到左穿過射線時(shí)(順時(shí)針)校仑,環(huán)繞數(shù)加1忠售,從左到右時(shí)(或者逆時(shí)針),環(huán)繞數(shù)減1迄沫。處理完多邊形的所有相關(guān)邊之后稻扬,若環(huán)繞數(shù)為非零,則p為內(nèi)部點(diǎn)羊瘩,否則泰佳,p是外部點(diǎn)。
參考[1]中例子如下尘吗,
判斷點(diǎn)p是否在多邊形內(nèi)逝她,從點(diǎn)p向外做一條射線(可以任意方向),多邊形的邊從左到右經(jīng)過射線時(shí)環(huán)數(shù)減1睬捶,多邊形的邊從右往左經(jīng)過射線時(shí)環(huán)數(shù)加1黔宛,最后環(huán)數(shù)不為0,即表示在多邊形內(nèi)部侧戴。
當(dāng)然宁昭,非零繞數(shù)規(guī)則和奇偶規(guī)則會判斷出現(xiàn)矛盾的情況跌宛,如下圖所示,左側(cè)表示用 奇偶規(guī)則判斷繞環(huán)數(shù)為2 积仗,表示在多邊形外疆拘,所以沒有填充。右側(cè)圖用非零繞環(huán)規(guī)則判斷出繞數(shù)為2寂曹,非0表示在多邊形內(nèi)部哎迄,所以填充。
另外一個(gè)例子隆圆,如下
好吧漱挚,上代碼驗(yàn)證。
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);
}
效果圖:
左側(cè)所有圓閉合方向?yàn)轫槙r(shí)針方向渺氧。右側(cè)兩側(cè)閉合圓為順時(shí)針方向旨涝,中間圓為逆時(shí)針方向。對于EVEN_ODD模式來說侣背,與閉合方向沒關(guān)系白华。對于WINDING模式來說,閉合方向不同穿過任意射線的方向不同贩耐。故出現(xiàn)上述現(xiàn)象弧腥。
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);
}
效果圖:
這里強(qiáng)調(diào)一點(diǎn):因?yàn)閜ath.op是Api19新增的,所以想要在Api19以下實(shí)現(xiàn)同樣功能需要 采用畫布的clip實(shí)現(xiàn)
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的各個(gè)方法:
[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.
上述兩對方法有兩個(gè)在5.0后新增的重載方法潮太,下面我們來詳細(xì)介紹這兩對方法作用與區(qū)別管搪。
第一對,以Add方式:
/**
* Add the specified arc to the path as a new contour.
* 將指定的圓弧添加到路徑中作為一個(gè)新的輪廓铡买。
* @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);
}
上過學(xué)的應(yīng)該都知道橢圓公式(不知道的自己科普)更鲁,第一個(gè)參數(shù)的由來。第二個(gè)參數(shù)為橢圓的起始度數(shù)寻狂,第二個(gè)參數(shù)為橢圓的結(jié)束度數(shù)岁经。從橢圓右側(cè)定點(diǎn)開始為0度基準(zhǔn)點(diǎn),順時(shí)針方向增加蛇券。
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);
}
我們發(fā)現(xiàn),arcTo比addArc 多以個(gè)參數(shù)纠亚,干啥用的呢塘慕?看注釋 ** If true, always begin a new contour with the arc** 如果為forceMoveTo==true ,會與addArc方式相同蒂胞,以一個(gè)新的輪廓添加到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.
將指定的弧線附加到路徑作為一個(gè)新的輪廓。(forceMoveTo false 的情況下)如果圓弧的起始點(diǎn)與上次path的結(jié)束點(diǎn)不相同蛤织,則在上次結(jié)束點(diǎn)的基礎(chǔ)上調(diào)用lineTo() 連接到圓弧的其實(shí)點(diǎn)赴叹。如果Path 重置或者調(diào)用new Path()方法,則首先會調(diào)用moveTo() 到圓弧的其實(shí)點(diǎn)指蚜。
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);
}
暫時(shí)解釋到這里吧乞巧,用的過程中慢慢領(lǐng)會!
[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)
計(jì)算path中控制的點(diǎn)的邊界摊鸡,將結(jié)果寫入bounds中绽媒,如果Path中只有0或者1個(gè)點(diǎn),那么bounds會返回(0,0,0,0)的值免猾,exact這個(gè)變量暫時(shí)沒用,其實(shí)就是path的最邊界點(diǎn)到X 軸 Y軸垂線與XY軸的交點(diǎn)是辕。通俗的講就>是一個(gè)矩形正好將這個(gè)path包裹起來。
上述大家應(yīng)該比較容易理解吧猎提,在此就不做贅述获三!
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).
二階、 三階貝賽爾曲線自己暫時(shí)只會簡單的使用锨苏,后續(xù)會補(bǔ)上Demo 加速球石窑。
詳細(xì)了解貝賽爾曲線,有個(gè)大神博客已經(jīng)寫得非常詳細(xì).
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.
返回由路徑的內(nèi)容定義的路徑的凸度蚓炬。如果路徑具有單個(gè)輪廓,則路徑是凸的躺屁,并且僅在單個(gè)方向上曲線肯夏。
該函數(shù)將從其控制點(diǎn)計(jì)算路徑的凸度,并緩存結(jié)果犀暑。
哪個(gè)大神用過上邊這個(gè)方法驯击?不知道這個(gè)具體的應(yīng)用場景!
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
如果填充類型是經(jīng)過反向變換的,返回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 點(diǎn)直線連接到(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).
設(shè)置下一個(gè)直線 曲線 閉合回路的起始點(diǎn)
[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)
兩個(gè)方法見名知意,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 進(jìn)行 Path.Op op)
Set this path to the result of applying the Op to the two specified paths.運(yùn)算择吊,結(jié)果保存在當(dāng)前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)
當(dāng)前path與參數(shù)中path進(jìn)行[op](https://developer.android.google.cn/reference/android/graphics/Path.html#op(android.graphics.Path, android.graphics.Path.Op))運(yùn)算。
[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的方法是基于原點(diǎn)的坐標(biāo)系(偏移量), rXxx方法是基于當(dāng)前點(diǎn)坐標(biāo)系(偏移量)
reset rewind 區(qū)別
清除Path中的內(nèi)容,“FillType”影響的是顯示效果囱持,“數(shù)據(jù)結(jié)構(gòu)”影響的是重建速度锌杀。
reset不保留內(nèi)部數(shù)據(jù)結(jié)構(gòu)婚脱,但會保留FillType.
rewind會保留內(nèi)部的數(shù)據(jù)結(jié)構(gòu),但不保留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)
接下來把工作中實(shí)現(xiàn)加速球效果的demo給大家分享一下,有什么寫的不拖地兒袭灯,歡迎指導(dǎo),大家相互學(xué)習(xí),圖片比例是按照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"; //程序崩潰時(shí)回復(fù)數(shù)據(jù)
private static final String KEY_BUNDLE_PROGRESS = "key_bundle_progress"; //回復(fù)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;//內(nèi)圓環(huán)寬度比例
public static final float DEFAULT_OUTSIDE_CIRCLE_STROKE_WIDTH_RATIO = 0.015f;//外圓環(huán)寬度比例
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;//一個(gè)view能容納的波長個(gè)數(shù)
private int mWaveLength; //波長長度
private int mWaveSwing; // 振幅
private int mOffsetX; //偏移量
private int mSecondOffsetX; //第二個(gè)波長偏移量
private int mProgress; //進(jìn)度
private int mMaxProgress = DEFAULT_MAX_PROGRESS; //進(jìn)度最大值
// 暫時(shí)不考慮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;
//內(nèi)圓半徑及strokewidth
mInsideCircleStrokeWidth = maxWidth * DEFAULT_INSIDE_CIRCLE_STROKE_WIDTH_RATIO;
mInsideCircleRadius = maxWidth * DEFAULT_INSIDE_CIRCLE_RADIUS_RATIO
- mInsideCircleStrokeWidth * 0.5f;
//內(nèi)圓外圓的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();
//移動(dòng)初始位置為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 運(yùn)算
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>
轉(zhuǎn)載請標(biāo)明出處:http://www.reibang.com/p/04c2c7046519