Path学习

最近项目中要实现加速球效果。
是时候该学习一波了,好了废话不多说,记笔记,还是从自己发憷的自定义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);
    }

效果图:

cw ccw.png

接下来研究下一知识点(通过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,即表示在多边形内部。

五角星.gif

当然,非零绕数规则和奇偶规则会判断出现矛盾的情况,如下图所示,左侧表示用 奇偶规则判断绕环数为2 ,表示在多边形外,所以没有填充。右侧图用非零绕环规则判断出绕数为2,非0表示在多边形内部,所以填充。

异常.png

另外一个例子,如下

不同效果.png

好吧,上代码验证。

    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

左侧所有圆闭合方向为顺时针方向。右侧两侧闭合圆为顺时针方向,中间圆为逆时针方向。对于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.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

这里强调一点:因为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);
    }
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);
    }

我们发现,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);
    }
arcTo forceMoveTo = false.png
arcTo forceMoveTo = true.png

暂时解释到这里吧,用的过程中慢慢领会!

[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图的比例画的
先看效果

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"; //程序崩溃时回复数据
    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>

转载请标明出处:http://www.jianshu.com/p/04c2c7046519

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 简单说说   Path是android中用来封装几何学路径的一个类,因为Path在图形绘制上占的比重还是相当大的,...
    前世小书童阅读 6,281评论 5 44
  • UIBezierPath Class Reference 译:UIBezierPath类封装了Core Graph...
    鋼鉄侠阅读 1,715评论 0 3
  • 版权声明:本文为博主原创文章,未经博主允许不得转载。系列教程:Android开发之从零开始系列大家要是看到有错误的...
    Anlia阅读 20,582评论 1 24
  • 今天吃晚饭的时候,老公跟我说今天接了三个电话,都是请喝喜酒的。我一听脑袋顿时大了起来,饭都没心思吃了。气得骂了句粗...
    灬北风江上寒阅读 645评论 30 31
  • 8月4号,沉寂了很久的大西北混合旅群收到严总发来的消息,“小伙伴们 ,国庆要不要再来一次畅游哇”,“先预留我”,“...
    kungewb阅读 640评论 0 1