Path学习

作者: laifrog | 来源:发表于2017-06-03 13:44 被阅读145次

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

相关文章

  • Path学习

    最近项目中要实现加速球效果。是时候该学习一波了,好了废话不多说,记笔记,还是从自己发憷的自定义view开始。 先来...

  • Android_path总结

    要熟悉地掌握自定义控件是,path的学习是必经之路,下面我们的了解一下Path: 一、path是什么? Path类...

  • path模块学习

    path.resolve() 方法会将路径或路径片段的序列解析为绝对路径 path.join() __dirnam...

  • Digital marketing

    My next 10 years on career: 学习path: * digital marketing :...

  • Android Path学习笔记

    简单说说   Path是android中用来封装几何学路径的一个类,因为Path在图形绘制上占的比重还是相当大的,...

  • 笔记6:词频统计

    import string path = r'F:\5-学习\python学习\20160923\Walden.t...

  • 基础篇

    Linux学习 一、环境变量PATH 1、echo $PATH查看环境变量 2、添加ls2命令,临时生效 3、编辑...

  • Golang 学习笔记(08)—— 文件操作

    本文为转载,原文:Golang 学习笔记(08)—— 文件操作 path 在path包中封装了一些路径相关的操作,...

  • Node-2

    path模块 path模块:用于处理文件路径,var path = require('path') path.ex...

  • Node Native (3) path、global

    一、path const path = require('path'); 属性 path.delimiter:提供...

网友评论

    本文标题:Path学习

    本文链接:https://www.haomeiwen.com/subject/kzdhzttx.html