Path學(xué)習(xí)

最近項(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);
    }

效果圖:

cw ccw.png

接下來研究下一知識點(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)部侧戴。

五角星.gif

當(dāng)然宁昭,非零繞數(shù)規(guī)則和奇偶規(guī)則會判斷出現(xiàn)矛盾的情況跌宛,如下圖所示,左側(cè)表示用 奇偶規(guī)則判斷繞環(huán)數(shù)為2 积仗,表示在多邊形外疆拘,所以沒有填充。右側(cè)圖用非零繞環(huán)規(guī)則判斷出繞數(shù)為2寂曹,非0表示在多邊形內(nèi)部哎迄,所以填充。

異常.png

另外一個(gè)例子隆圆,如下

不同效果.png

好吧漱挚,上代碼驗(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);
    }

效果圖:

filltype.png

左側(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.XORINTERSECT剛好相反 ;
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.png

這里強(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);
    }
addArc效果圖.png
addArc分析.png

第二對缀壤,以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);
    }
arcTo forceMoveTo = false.png
arcTo forceMoveTo = true.png

暫時(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圖的比例畫的
先看效果

20170603_132424.gif

代碼:

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末染乌,一起剝皮案震驚了整個(gè)濱河市俺榆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌粒蜈,老刑警劉巖顺献,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異枯怖,居然都是意外死亡注整,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門度硝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肿轨,“玉大人,你說我怎么就攤上這事蕊程〗放郏” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵藻茂,是天一觀的道長驹暑。 經(jīng)常有香客問我,道長辨赐,這世上最難降的妖魔是什么优俘? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮掀序,結(jié)果婚禮上帆焕,老公的妹妹穿的比我還像新娘。我一直安慰自己不恭,他們只是感情好叶雹,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著县袱,像睡著了一般浑娜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上式散,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天筋遭,我揣著相機(jī)與錄音,去河邊找鬼。 笑死漓滔,一個(gè)胖子當(dāng)著我的面吹牛编饺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播响驴,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼透且,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了豁鲤?” 一聲冷哼從身側(cè)響起秽誊,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎琳骡,沒想到半個(gè)月后锅论,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡楣号,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年最易,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炫狱。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡藻懒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出视译,到底是詐尸還是另有隱情嬉荆,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布酷含,位于F島的核電站员寇,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏第美。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一陆爽、第九天 我趴在偏房一處隱蔽的房頂上張望什往。 院中可真熱鬧,春花似錦慌闭、人聲如沸别威。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽省古。三九已至,卻和暖如春丧失,著一層夾襖步出監(jiān)牢的瞬間豺妓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留琳拭,地道東北人训堆。 一個(gè)月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像白嘁,于是被迫代替她去往敵國和親坑鱼。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 簡單說說 ??Path是android中用來封裝幾何學(xué)路徑的一個(gè)類絮缅,因?yàn)镻ath在圖形繪制上占的比重還是相當(dāng)大的鲁沥,...
    前世小書童閱讀 6,291評論 5 44
  • UIBezierPath Class Reference 譯:UIBezierPath類封裝了Core Graph...
    鋼鉄俠閱讀 1,733評論 0 3
  • 版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載耕魄。系列教程:Android開發(fā)之從零開始系列大家要是看到有錯(cuò)誤的...
    Anlia閱讀 20,694評論 1 24
  • 今天吃晚飯的時(shí)候画恰,老公跟我說今天接了三個(gè)電話,都是請喝喜酒的屎开。我一聽腦袋頓時(shí)大了起來阐枣,飯都沒心思吃了。氣得罵了句粗...
    灬北風(fēng)江上寒閱讀 653評論 30 31
  • 8月4號奄抽,沉寂了很久的大西北混合旅群收到嚴(yán)總發(fā)來的消息蔼两,“小伙伴們 ,國慶要不要再來一次暢游哇”逞度,“先預(yù)留我”额划,“...
    kungewb閱讀 658評論 0 1