Path :
在Android中复杂的图形的绘制绝大多数是通过path来实现,比如绘制一条曲线,然后让一个物体随着这个曲线运动,比如搜索按钮,比如一个简单时钟的实现:
那么什么是path呢!
定义:path 就是路径,就是图形的路径的集合,它里边包含了路径里边的坐标点,等等的属性。我们可以获取到任意点的坐标,正切值。
那么要获取Path上边所有点的坐标还需要用到一个类,PathMeasure;
PathMesure:
PathMeasure是一个用来测量Path的类,主要有以下方法:
构造方法
方法名 | 释义 |
---|---|
PathMeasure() | 创建一个空的PathMeasure |
PathMeasure(Path path, boolean forceClosed) | 创建 PathMeasure 并关联一个指定的Path(Path需要已经创建完成)。 |
公共方法
返回值 | 方法名 | 释义 |
---|---|---|
void | setPath(Path path, boolean forceClosed) | 关联一个Path |
boolean | isClosed() | 是否闭合 |
float | getLength() | 获取Path的长度 |
boolean | nextContour() | 跳转到下一个轮廓 |
boolean | getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) | 截取片段 |
boolean | getPosTan(float distance, float[] pos, float[] tan) | 获取指定长度的位置坐标及该点切线值 |
boolean | getMatrix(float distance, Matrix matrix, int flags) | 获取指定长度的位置坐标及该点Matrix |
可以看到,这个就等于是一个Path的一个工具类,方法很简单,那么就开始我们所要做的按钮跟时钟的开发吧
(1)搜索按钮:
首先上图:
要实现这个功能首先要把他分解开来做;
创建搜索按钮的path路径,然后创建外圈旋转的path,
public void initPath(){ mPath_search = new Path(); mPath_circle = new Path(); mMeasure = new PathMeasure(); // 注意,不要到360度,否则内部会自动优化,测量不能取到需要的数值 RectF oval1 = new RectF(-50, -50, 50, 50); // 放大镜圆环 mPath_search.addArc(oval1, 45, 359.9f); RectF oval2 = new RectF(-100, -100, 100, 100); // 外部圆环 mPath_circle.addArc(oval2, 45, -359.9f); float[] pos = new float[2]; mMeasure.setPath(mPath_circle, false); // 放大镜把手的位置 mMeasure.getPosTan(0, pos, null); mPath_search.lineTo(pos[0], pos[1]); // 放大镜把手 Log.i("TAG", "pos=" + pos[0] + ":" + pos[1]); }
我们要的效果就是点击搜索按钮的时候开始从按钮变为旋转,然后搜索结束以后变为搜索按钮。
所以我们可以确定有四种状态:
public enum Seach_State{ START,END,NONE,SEARCHING }
然后根据状态来进行动态绘制path,动态绘制path就要使用到PathMeasure测量当前path的坐标,然后进行绘制。
private void drawPath(Canvas c) { c.translate(mWidth / 2, mHeight / 2); switch (mState){ case NONE: c.drawPath(mPath_search,mPaint); break; case START: mMeasure.setPath(mPath_search,true); Path path = new Path(); mMeasure.getSegment(mMeasure.getLength() * curretnAnimationValue,mMeasure.getLength(),path, true); c.drawPath(path,mPaint); break; case SEARCHING: mMeasure.setPath(mPath_circle,true); Path path_search = new Path(); mMeasure.getSegment(mMeasure.getLength()*curretnAnimationValue -30,mMeasure.getLength()*curretnAnimationValue,path_search,true); c.drawPath(path_search,mPaint); break; case END: mMeasure.setPath(mPath_search,true); Path path_view = new Path(); mMeasure.getSegment(0,mMeasure.getLength()*curretnAnimationValue,path_view,true); c.drawPath(path_view,mPaint); break; } }
然后就是需要通过使用属性动画来返回当前该绘制的百分百,通过这个值来进行计算要绘制的path。
下边是整个代码:
package com.duoku.platform.demo.canvaslibrary.attract.view; import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.RectF; import android.util.AttributeSet; import android.util.Log; import android.view.View; /** * Created by chenpengfei_d on 2016/9/7. */ public class SearchView extends View { private Paint mPaint; private Context mContext; private Path mPath_circle; private Path mPath_search; private PathMeasure mMeasure; private ValueAnimator mValueAnimator_search; private long defaultduration=3000; private float curretnAnimationValue; private Seach_State mState = Seach_State.SEARCHING; public SearchView(Context context) { super(context); init(context); } public SearchView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public SearchView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } public void init(Context context){ this.mContext = context; initPaint(); initPath(); initAnimation(); } public void initPaint(){ mPaint = new Paint(); mPaint.setDither(true); mPaint.setStrokeCap(Paint.Cap.ROUND);//设置笔头效果 mPaint.setAntiAlias(true); mPaint.setColor(Color.RED); mPaint.setStrokeWidth(3); mPaint.setStyle(Paint.Style.STROKE); } public void initPath(){ mPath_search = new Path(); mPath_circle = new Path(); mMeasure = new PathMeasure(); // 注意,不要到360度,否则内部会自动优化,测量不能取到需要的数值 RectF oval1 = new RectF(-50, -50, 50, 50); // 放大镜圆环 mPath_search.addArc(oval1, 45, 359.9f); RectF oval2 = new RectF(-100, -100, 100, 100); // 外部圆环 mPath_circle.addArc(oval2, 45, -359.9f); float[] pos = new float[2]; mMeasure.setPath(mPath_circle, false); // 放大镜把手的位置 mMeasure.getPosTan(0, pos, null); mPath_search.lineTo(pos[0], pos[1]); // 放大镜把手 Log.i("TAG", "pos=" + pos[0] + ":" + pos[1]); } public void initAnimation(){ mValueAnimator_search = ValueAnimator.ofFloat(0f,1.0f).setDuration(defaultduration); mValueAnimator_search.addUpdateListener(updateListener); mValueAnimator_search.addListener(animationListener); } private ValueAnimator.AnimatorUpdateListener updateListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { curretnAnimationValue = (float) animation.getAnimatedValue(); invalidate(); } }; private Animator.AnimatorListener animationListener = new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if(mState ==Seach_State.START){ setState(Seach_State.SEARCHING); } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawPath(canvas); } private int mWidth,mHeight; @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; } private void drawPath(Canvas c) { c.translate(mWidth / 2, mHeight / 2); switch (mState){ case NONE: c.drawPath(mPath_search,mPaint); break; case START: mMeasure.setPath(mPath_search,true); Path path = new Path(); mMeasure.getSegment(mMeasure.getLength() * curretnAnimationValue,mMeasure.getLength(),path, true); c.drawPath(path,mPaint); break; case SEARCHING: mMeasure.setPath(mPath_circle,true); Path path_search = new Path(); mMeasure.getSegment(mMeasure.getLength()*curretnAnimationValue -30,mMeasure.getLength()*curretnAnimationValue,path_search,true); c.drawPath(path_search,mPaint); break; case END: mMeasure.setPath(mPath_search,true); Path path_view = new Path(); mMeasure.getSegment(0,mMeasure.getLength()*curretnAnimationValue,path_view,true); c.drawPath(path_view,mPaint); break; } } public void setState(Seach_State state){ this.mState = state; startSearch(); } public void startSearch(){ switch (mState){ case START: mValueAnimator_search.setRepeatCount(0); break; case SEARCHING: mValueAnimator_search.setRepeatCount(ValueAnimator.INFINITE); mValueAnimator_search.setRepeatMode(ValueAnimator.REVERSE); break; case END: mValueAnimator_search.setRepeatCount(0); break; } mValueAnimator_search.start(); } public enum Seach_State{ START,END,NONE,SEARCHING } }
(学习的点:path可以组合,可以把不同的path放置到一个path里边,然后进行统一的绘制)
(2)时钟:
效果:
说一下时钟的思路啊,网上很多时钟都是通过Canvas绘制基本图形实现的,没有通过path来实现的,使用path实现是为了以后更加灵活的控制时钟的绘制效果,比如我们要让最外边的圆圈逆时针旋转,还比如在上边添加些小星星啥的,用path的话会更加灵活。
时钟的实现分部分:
1、创建外圈path路径
2、创建刻度path路径,要区分整点,绘制时间点
3、绘制指针,(这个使用的是canvas绘制的线段,也可以使用Path,可以自己测试)
需要计算当前时针,分针,秒针的角度,然后进行绘制
整体代码:
package com.duoku.platform.demo.canvaslibrary.attract.view; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.os.Handler; import android.util.AttributeSet; import android.view.View; import java.util.Calendar; /** * Created by chenpengfei_d on 2016/9/8. */ public class TimeView extends View { private Paint mPaint,mPaint_time; private Paint mPaint_h,mPaint_m,mPaint_s; private Path mPath_Circle; private Path mPath_Circle_h; private Path mPath_Circle_m; private Path mPath_h,mPath_m,mPath_s; private Path mPath_duration; private PathMeasure mMeasure; private PathMeasure mMeasure_h; private PathMeasure mMeasure_m; private Handler mHandler = new Handler(); private Runnable clockRunnable; private boolean isRunning; public TimeView(Context context) { super(context); init(); } public TimeView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public TimeView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } int t = 3; public void init(){ //初始化画笔 mPaint = new Paint(); mPaint.setDither(true); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(2); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setStrokeJoin(Paint.Join.ROUND); mPaint.setColor(Color.RED); mPaint_time = new Paint(); mPaint_time.setDither(true); mPaint_time.setAntiAlias(true); mPaint_time.setStyle(Paint.Style.STROKE); mPaint_time.setStrokeWidth(2); mPaint_time.setTextSize(15); mPaint_time.setStrokeCap(Paint.Cap.ROUND); mPaint_time.setStrokeJoin(Paint.Join.ROUND); mPaint_time.setColor(Color.RED); mPaint_h = new Paint(); mPaint_h.setDither(true); mPaint_h.setAntiAlias(true); mPaint_h.setStyle(Paint.Style.STROKE); mPaint_h.setStrokeWidth(6); mPaint_h.setTextSize(15); mPaint_h.setStrokeCap(Paint.Cap.ROUND); mPaint_h.setStrokeJoin(Paint.Join.ROUND); mPaint_h.setColor(Color.RED); mPaint_m = new Paint(); mPaint_m.setDither(true); mPaint_m.setAntiAlias(true); mPaint_m.setStyle(Paint.Style.STROKE); mPaint_m.setStrokeWidth(4); mPaint_m.setTextSize(15); mPaint_m.setStrokeCap(Paint.Cap.ROUND); mPaint_m.setStrokeJoin(Paint.Join.ROUND); mPaint_m.setColor(Color.RED); mPaint_s = new Paint(); mPaint_s.setDither(true); mPaint_s.setAntiAlias(true); mPaint_s.setStyle(Paint.Style.STROKE); mPaint_s.setStrokeWidth(2); mPaint_s.setTextSize(15); mPaint_s.setStrokeCap(Paint.Cap.ROUND); mPaint_s.setStrokeJoin(Paint.Join.ROUND); mPaint_s.setColor(Color.RED); //初始化刻度 mPath_Circle = new Path(); mPath_Circle.addCircle(0,0,250, Path.Direction.CCW); mPath_Circle_h = new Path(); mPath_Circle_h.addCircle(0,0,220, Path.Direction.CCW); mPath_Circle_m = new Path(); mPath_Circle_m.addCircle(0,0,235, Path.Direction.CCW); //初始化PathMeasure测量path坐标, mMeasure = new PathMeasure(); mMeasure.setPath(mPath_Circle,true); mMeasure_h = new PathMeasure(); mMeasure_h.setPath(mPath_Circle_h,true); mMeasure_m = new PathMeasure(); mMeasure_m.setPath(mPath_Circle_m,true); //获取刻度path mPath_duration = new Path(); for (int i = 60; i>0 ;i --){ Path path = new Path(); float pos [] = new float[2]; float tan [] = new float[2]; float pos2 [] = new float[2]; float tan2 [] = new float[2]; float pos3 [] = new float[2]; float tan3 [] = new float[2]; mMeasure.getPosTan(mMeasure.getLength()*i/60,pos,tan); mMeasure_h.getPosTan(mMeasure_h.getLength()*i/60,pos2,tan2); mMeasure_m.getPosTan(mMeasure_m.getLength()*i/60,pos3,tan3); float x = pos[0]; float y = pos[1]; float x2 = pos2[0]; float y2 = pos2[1]; float x3 = pos3[0]; float y3 = pos3[1]; path.moveTo(x , y); if(i% 5 ==0){ path.lineTo(x2,y2); if(t>12){ t = t-12; } String time = t++ +""; Path path_time = new Path(); mMeasure_h.getPosTan(mMeasure_h.getLength()*(i-1)/60,pos2,tan2); mPaint.getTextPath(time,0,time.length(),(x2- (x2/15)),y2-(y2/15),path_time); path.close(); path.addPath(path_time); }else{ path.lineTo(x3,y3); } mPath_duration.addPath(path); clockRunnable = new Runnable() {//里面做的事情就是每隔一秒,刷新一次界面 @Override public void run() { //线程中刷新界面 postInvalidate(); mHandler.postDelayed(this, 1000); } }; } mPath_h = new Path(); mPath_h.rLineTo(50,30); mPath_m = new Path(); mPath_m.rLineTo(80,80); mPath_s = new Path(); mPath_s.rLineTo(130,50); } private int mWidth,mHeight; @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; mHeight = h; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if(!isRunning){ isRunning = true; mHandler.postDelayed(clockRunnable,1000); }else{ canvas.translate(mWidth/2,mHeight/2); canvas.drawPath(mPath_Circle,mPaint); canvas.save(); canvas.drawPath(mPath_duration,mPaint_time); canvas.drawPoint(0,0,mPaint_time); drawClockPoint(canvas); } } private Calendar cal; private int hour; private int min; private int second; private float hourAngle,minAngle,secAngle; /** * 绘制三个指针 * @param canvas */ private void drawClockPoint(Canvas canvas) { cal = Calendar.getInstance(); hour = cal.get(Calendar.HOUR);//Calendar.HOUR获取的是12小时制,Calendar.HOUR_OF_DAY获取的是24小时制 min = cal.get(Calendar.MINUTE); second = cal.get(Calendar.SECOND); //计算时分秒指针各自需要偏移的角度 hourAngle = (float)hour / 12 * 360 + (float)min / 60 * (360 / 12);//360/12是指每个数字之间的角度 minAngle = (float)min / 60 * 360; secAngle = (float)second / 60 * 360; //下面将时、分、秒指针按照各自的偏移角度进行旋转,每次旋转前要先保存canvas的原始状态 canvas.save(); canvas.rotate(hourAngle,0, 0); canvas.drawLine(0, 0, mWidth/6, getHeight() / 6 - 65, mPaint_h);//时针长度设置为65 canvas.restore(); canvas.save(); canvas.rotate(minAngle,0, 0); canvas.drawLine(0, 0, mWidth/6, getHeight() / 6 - 90 , mPaint_m);//分针长度设置为90 canvas.restore(); canvas.save(); canvas.rotate(secAngle,0, 0); canvas.drawLine(0, 0, mWidth/6, getHeight() / 6 - 110 , mPaint_s);//秒针长度设置为110 canvas.restore(); } }
这其实还不算特别复杂的动画,也许你有啥好的想法,可以自己通过Path + 属性动画来实现更好看的效果;
比如星空的效果,比如动态绘制文字 + 路径实现类似ppt中播放的一些特效,比如电子书的自动翻页。
(3)下边再介绍一个知识,就是svg:
svg是什么东西呢?
他的学名叫做可缩放矢量图形,是基于可扩展标记语言(标准通用标记语言的子集),用于描述二维矢量图形的一种图形格式。
这种格式的图形式可以加载到Android的Path里边。
既然可以加载到Path里边,那么是不是就可以实现更复杂的效果呢,下边看图:
这样的动画自己绘制也可以,只要有整个路径的path,但是如果使用svg的话那么久相当的简单了。
实现流程:
主要是解析svg文件,获取path路径集合,然后绘制的话基本上跟以前的一样。
因为网上有人已经给出工具类了,不是很复杂,就不自己写了。
贴出俩个类:
专门绘制Svg的View以及自定义的参数文件attrs:
<pre name="code" class="java"><?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="SvgView"> <attr name="pathColor" format="color|reference"/> <attr name="pathWidth" format="dimension|reference"/> <attr name="svg" format="reference"/> <attr name="fill" format="boolean"/> <attr name="fillColor" format="color|reference"/> <attr name="naturalColors" format="boolean"/> </declare-styleable> </resources>
package com.duoku.platform.demo.canvaslibrary.attract.view; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.Interpolator; import com.duoku.platform.demo.canvaslibrary.R; import com.duoku.platform.demo.canvaslibrary.attract.view.Utils.SvgUtils; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.AnimatorSet; import com.nineoldandroids.animation.ObjectAnimator; import java.util.ArrayList; import java.util.List; /** * SvgView 是一个绘制svg的paths路径的动画控件 */ public class SvgView extends View implements SvgUtils.AnimationStepListener { public static final String LOG_TAG = "SvgView"; /** * 默认画笔. */ private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); /** * U工具类用来解析svg文件 */ private final SvgUtils svgUtils = new SvgUtils(paint); /** * 整个svg文件的path集合 */ private List<SvgUtils.SvgPath> paths = new ArrayList<>(); private final Object mSvgLock = new Object(); private Thread mLoader; /** * svg文件的资源id */ private int svgResourceId; /** * 使用第三方动画框架的动画构造者 */ private AnimatorBuilder animatorBuilder; private AnimatorSetBuilder animatorSetBuilder; /** * 绘制的进度 */ private float progress = 0f; /** * 是否使用svg文件中的自然颜色 */ private boolean naturalColors; /** * 是否view充满了svg本来的颜色在绘制的过程中 */ private boolean fillAfter; private boolean fill; private int fillColor; private int width; private int height; private Bitmap mTempBitmap; private Canvas mTempCanvas; public SvgView(Context context) { this(context, null); } public SvgView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SvgView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); paint.setStyle(Paint.Style.STROKE); getFromAttributes(context, attrs); } private void getFromAttributes(Context context, AttributeSet attrs) { final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SvgView); try { if (a != null) { paint.setColor(a.getColor(R.styleable.SvgView_pathColor, 0xff00ff00)); paint.setStrokeWidth(a.getDimensionPixelSize(R.styleable.SvgView_pathWidth, 8)); svgResourceId = a.getResourceId(R.styleable.SvgView_svg, 0); naturalColors = a.getBoolean(R.styleable.SvgView_naturalColors, false); fill = a.getBoolean(R.styleable.SvgView_fill,false); fillColor = a.getColor(R.styleable.SvgView_fillColor,Color.argb(0,0,0,0)); } } finally { if (a != null) { a.recycle(); } invalidate(); } } public void setPaths(final List<Path> paths) { for (Path path : paths) { this.paths.add(new SvgUtils.SvgPath(path, paint)); } synchronized (mSvgLock) { updatePathsPhaseLocked(); } } public void setPath(final Path path) { paths.add(new SvgUtils.SvgPath(path, paint)); synchronized (mSvgLock) { updatePathsPhaseLocked(); } } public void setPercentage(float percentage) { if (percentage < 0.0f || percentage > 1.0f) { throw new IllegalArgumentException("setPercentage not between 0.0f and 1.0f"); } progress = percentage; synchronized (mSvgLock) { updatePathsPhaseLocked(); } invalidate(); } private void updatePathsPhaseLocked() { final int count = paths.size(); for (int i = 0; i < count; i++) { SvgUtils.SvgPath svgPath = paths.get(i); svgPath.path.reset(); svgPath.measure.getSegment(0.0f, svgPath.length * progress, svgPath.path, true); // Required only for Android 4.4 and earlier svgPath.path.rLineTo(0.0f, 0.0f); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if(mTempBitmap==null || (mTempBitmap.getWidth()!=canvas.getWidth()||mTempBitmap.getHeight()!=canvas.getHeight()) ) { mTempBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888); mTempCanvas = new Canvas(mTempBitmap); } mTempBitmap.eraseColor(0); synchronized (mSvgLock) { mTempCanvas.save(); mTempCanvas.translate(getPaddingLeft(), getPaddingTop()); fill(mTempCanvas); final int count = paths.size(); for (int i = 0; i < count; i++) { final SvgUtils.SvgPath svgPath = paths.get(i); final Path path = svgPath.path; final Paint paint1 = naturalColors ? svgPath.paint : paint; mTempCanvas.drawPath(path, paint1); } fillAfter(mTempCanvas); mTempCanvas.restore(); applySolidColor(mTempBitmap); canvas.drawBitmap(mTempBitmap,0,0,null); } } private void fillAfter(final Canvas canvas) { if (svgResourceId != 0 && fillAfter && Math.abs(progress - 1f) < 0.00000001) { svgUtils.drawSvgAfter(canvas, width, height); } } private void fill(final Canvas canvas) { if (svgResourceId != 0 && fill) { svgUtils.drawSvgAfter(canvas, width, height); } } private void applySolidColor(final Bitmap bitmap) { if(fill && fillColor!=Color.argb(0,0,0,0) ) if (bitmap != null) { for(int x=0;x<bitmap.getWidth();x++) { for(int y=0;y<bitmap.getHeight();y++) { int argb = bitmap.getPixel(x,y); int alpha = Color.alpha(argb); if(alpha!=0) { int red = Color.red(fillColor); int green = Color.green(fillColor); int blue = Color.blue(fillColor); argb = Color.argb(alpha,red,green,blue); bitmap.setPixel(x,y,argb); } } } } } @Override protected void onSizeChanged(final int w, final int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (mLoader != null) { try { mLoader.join(); } catch (InterruptedException e) { Log.e(LOG_TAG, "Unexpected error", e); } } if (svgResourceId != 0) { mLoader = new Thread(new Runnable() { @Override public void run() { svgUtils.load(getContext(), svgResourceId); synchronized (mSvgLock) { width = w - getPaddingLeft() - getPaddingRight(); height = h - getPaddingTop() - getPaddingBottom(); paths = svgUtils.getPathsForViewport(width, height); updatePathsPhaseLocked(); } } }, "SVG Loader"); mLoader.start(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (svgResourceId != 0) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(widthSize, heightSize); return; } int desiredWidth = 0; int desiredHeight = 0; final float strokeWidth = paint.getStrokeWidth() / 2; for (SvgUtils.SvgPath path : paths) { desiredWidth += path.bounds.left + path.bounds.width() + strokeWidth; desiredHeight += path.bounds.top + path.bounds.height() + strokeWidth; } int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(widthMeasureSpec); int measuredWidth, measuredHeight; if (widthMode == MeasureSpec.AT_MOST) { measuredWidth = desiredWidth; } else { measuredWidth = widthSize; } if (heightMode == MeasureSpec.AT_MOST) { measuredHeight = desiredHeight; } else { measuredHeight = heightSize; } setMeasuredDimension(measuredWidth, measuredHeight); } public void setFillAfter(final boolean fillAfter) { this.fillAfter = fillAfter; } public void setFill(final boolean fill) { this.fill = fill; } public void setFillColor(final int color){ this.fillColor=color; } public void useNaturalColors() { naturalColors = true; } public AnimatorBuilder getPathAnimator() { if (animatorBuilder == null) { animatorBuilder = new AnimatorBuilder(this); } return animatorBuilder; } public AnimatorSetBuilder getSequentialPathAnimator() { if (animatorSetBuilder == null) { animatorSetBuilder = new AnimatorSetBuilder(this); } return animatorSetBuilder; } public int getPathColor() { return paint.getColor(); } public void setPathColor(final int color) { paint.setColor(color); } public float getPathWidth() { return paint.getStrokeWidth(); } public void setPathWidth(final float width) { paint.setStrokeWidth(width); } public int getSvgResource() { return svgResourceId; } public void setSvgResource(int svgResource) { svgResourceId = svgResource; } public static class AnimatorBuilder { private int duration = 350; private Interpolator interpolator; private int delay = 0; private final ObjectAnimator anim; private ListenerStart listenerStart; private ListenerEnd animationEnd; private SvgViewAnimatorListener SvgViewAnimatorListener; public AnimatorBuilder(final SvgView SvgView) { anim = ObjectAnimator.ofFloat(SvgView, "percentage", 0.0f, 1.0f); } public AnimatorBuilder duration(final int duration) { this.duration = duration; return this; } public AnimatorBuilder interpolator(final Interpolator interpolator) { this.interpolator = interpolator; return this; } public AnimatorBuilder delay(final int delay) { this.delay = delay; return this; } public AnimatorBuilder listenerStart(final ListenerStart listenerStart) { this.listenerStart = listenerStart; if (SvgViewAnimatorListener == null) { SvgViewAnimatorListener = new SvgViewAnimatorListener(); anim.addListener(SvgViewAnimatorListener); } return this; } public AnimatorBuilder listenerEnd(final ListenerEnd animationEnd) { this.animationEnd = animationEnd; if (SvgViewAnimatorListener == null) { SvgViewAnimatorListener = new SvgViewAnimatorListener(); anim.addListener(SvgViewAnimatorListener); } return this; } /** * 开始动画. */ public void start() { anim.setDuration(duration); anim.setInterpolator(interpolator); anim.setStartDelay(delay); anim.start(); } private class SvgViewAnimatorListener implements Animator.AnimatorListener { @Override public void onAnimationStart(Animator animation) { if (listenerStart != null) listenerStart.onAnimationStart(); } @Override public void onAnimationEnd(Animator animation) { if (animationEnd != null) animationEnd.onAnimationEnd(); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } } public interface ListenerStart { void onAnimationStart(); } public interface ListenerEnd { void onAnimationEnd(); } } @Override public void onAnimationStep() { invalidate(); } /** * 动画构建的类 */ public static class AnimatorSetBuilder { private int duration = 1000; private Interpolator interpolator; private int delay = 0; private final List<Animator> animators = new ArrayList<>(); /** * 动画开始监听 */ private AnimatorBuilder.ListenerStart listenerStart; /** * 动画结束监听 */ private AnimatorBuilder.ListenerEnd animationEnd; /** * 动画监听器. */ private AnimatorSetBuilder.SvgViewAnimatorListener SvgViewAnimatorListener; /** * 动画path的顺序 */ private AnimatorSet animatorSet = new AnimatorSet(); /** * 动画路径的列表 */ private List<SvgUtils.SvgPath> paths; public AnimatorSetBuilder(final SvgView SvgView) { paths = SvgView.paths; for (SvgUtils.SvgPath path : paths) { path.setAnimationStepListener(SvgView); ObjectAnimator animation = ObjectAnimator.ofFloat(path, "length", 0.0f, path.getLength()); animators.add(animation); } animatorSet.playSequentially(animators); } /** *设置动画时长 */ public AnimatorSetBuilder duration(final int duration) { this.duration = duration / paths.size(); return this; } /** * 设置插值器. */ public AnimatorSetBuilder interpolator(final Interpolator interpolator) { this.interpolator = interpolator; return this; } /** *设置动画延迟 */ public AnimatorSetBuilder delay(final int delay) { this.delay = delay; return this; } /** *设置动画开始监听 */ public AnimatorSetBuilder listenerStart(final AnimatorBuilder.ListenerStart listenerStart) { this.listenerStart = listenerStart; if (SvgViewAnimatorListener == null) { SvgViewAnimatorListener = new SvgViewAnimatorListener(); animatorSet.addListener(SvgViewAnimatorListener); } return this; } /** * 设置动画结束的监听器 */ public AnimatorSetBuilder listenerEnd(final AnimatorBuilder.ListenerEnd animationEnd) { this.animationEnd = animationEnd; if (SvgViewAnimatorListener == null) { SvgViewAnimatorListener = new SvgViewAnimatorListener(); animatorSet.addListener(SvgViewAnimatorListener); } return this; } /** * 开始动画 */ public void start() { resetAllPaths(); animatorSet.cancel(); animatorSet.setDuration(duration); animatorSet.setInterpolator(interpolator); animatorSet.setStartDelay(delay); animatorSet.start(); } /** *重置所有path */ private void resetAllPaths() { for (SvgUtils.SvgPath path : paths) { path.setLength(0); } } /** * svg动画回调监听接口 */ private class SvgViewAnimatorListener implements Animator.AnimatorListener { @Override public void onAnimationStart(Animator animation) { if (listenerStart != null) listenerStart.onAnimationStart(); } @Override public void onAnimationEnd(Animator animation) { if (animationEnd != null) animationEnd.onAnimationEnd(); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } } } }
Svg解析工具类:
package com.duoku.platform.demo.canvaslibrary.attract.view.Utils; import android.content.Context; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.util.Log; import com.caverock.androidsvg.PreserveAspectRatio; import com.caverock.androidsvg.SVG; import com.caverock.androidsvg.SVGParseException; import java.util.ArrayList; import java.util.List; /** * Util class to init and get paths from svg. */ public class SvgUtils { /** * It is for logging purposes. */ private static final String LOG_TAG = "SVGUtils"; /** * All the paths with their attributes from the svg. */ private final List<SvgPath> mPaths = new ArrayList<>(); /** * The paint provided from the view. */ private final Paint mSourcePaint; /** * The init svg. */ private SVG mSvg; /** * Init the SVGUtils with a paint for coloring. * * @param sourcePaint - the paint for the coloring. */ public SvgUtils(final Paint sourcePaint) { mSourcePaint = sourcePaint; } /** * Loading the svg from the resources. * * @param context Context object to get the resources. * @param svgResource int resource id of the svg. */ public void load(Context context, int svgResource) { if (mSvg != null) return; try { mSvg = SVG.getFromResource(context, svgResource); mSvg.setDocumentPreserveAspectRatio(PreserveAspectRatio.UNSCALED); } catch (SVGParseException e) { Log.e(LOG_TAG, "Could not load specified SVG resource", e); } } /** * Draw the svg to the canvas. * * @param canvas The canvas to be drawn. * @param width The width of the canvas. * @param height The height of the canvas. */ public void drawSvgAfter(final Canvas canvas, final int width, final int height) { final float strokeWidth = mSourcePaint.getStrokeWidth(); rescaleCanvas(width, height, strokeWidth, canvas); } /** * Render the svg to canvas and catch all the paths while rendering. * * @param width - the width to scale down the view to, * @param height - the height to scale down the view to, * @return All the paths from the svg. */ public List<SvgPath> getPathsForViewport(final int width, final int height) { final float strokeWidth = mSourcePaint.getStrokeWidth(); Canvas canvas = new Canvas() { private final Matrix mMatrix = new Matrix(); @Override public int getWidth() { return width; } @Override public int getHeight() { return height; } @Override public void drawPath(Path path, Paint paint) { Path dst = new Path(); //noinspection deprecation getMatrix(mMatrix); path.transform(mMatrix, dst); paint.setAntiAlias(true); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(strokeWidth); mPaths.add(new SvgPath(dst, paint)); } }; rescaleCanvas(width, height, strokeWidth, canvas); return mPaths; } /** * Rescale the canvas with specific width and height. * * @param width The width of the canvas. * @param height The height of the canvas. * @param strokeWidth Width of the path to add to scaling. * @param canvas The canvas to be drawn. */ private void rescaleCanvas(int width, int height, float strokeWidth, Canvas canvas) { if (mSvg == null) return; final RectF viewBox = mSvg.getDocumentViewBox(); final float scale = Math.min(width / (viewBox.width() + strokeWidth), height / (viewBox.height() + strokeWidth)); canvas.translate((width - viewBox.width() * scale) / 2.0f, (height - viewBox.height() * scale) / 2.0f); canvas.scale(scale, scale); mSvg.renderToCanvas(canvas); } /** * Path with bounds for scalling , length and paint. */ public static class SvgPath { /** * Region of the path. */ private static final Region REGION = new Region(); /** * This is done for clipping the bounds of the path. */ private static final Region MAX_CLIP = new Region(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); /** * The path itself. */ public final Path path; /** * The paint to be drawn later. */ public final Paint paint; /** * The length of the path. */ public float length; /** * Listener to notify that an animation step has happened. */ AnimationStepListener animationStepListener; /** * The bounds of the path. */ public final Rect bounds; /** * The measure of the path, we can use it later to get segment of it. */ public final PathMeasure measure; /** * Constructor to add the path and the paint. * * @param path The path that comes from the rendered svg. * @param paint The result paint. */ public SvgPath(Path path, Paint paint) { this.path = path; this.paint = paint; measure = new PathMeasure(path, false); this.length = measure.getLength(); REGION.setPath(path, MAX_CLIP); bounds = REGION.getBounds(); } /** * Sets the animation step listener. * * @param animationStepListener AnimationStepListener. */ public void setAnimationStepListener(AnimationStepListener animationStepListener) { this.animationStepListener = animationStepListener; } /** * Sets the length of the path. * * @param length The length to be set. */ public void setLength(float length) { path.reset(); measure.getSegment(0.0f, length, path, true); path.rLineTo(0.0f, 0.0f); if (animationStepListener != null) { animationStepListener.onAnimationStep(); } } /** * @return The length of the path. */ public float getLength() { return length; } } public interface AnimationStepListener { /** * Called when an animation step happens. */ void onAnimationStep(); } }
说一下使用的流程吧。
(1)在布局文件中添加自定义的SvgView,并指定其svg文件
<com.duoku.platform.demo.canvaslibrary.attract.view.SvgView android:id="@+id/svg_view" android:layout_width="match_parent" android:layout_height="match_parent" app:svg="@raw/pig" />
(2)在代码中开启动画效果即可。
<span style="white-space:pre"> </span>SvgView mSvgView; mSvgView = (SvgView)findViewById(R.id.svg_view); mSvgView.getPathAnimator().duration(5000) .start();
如果你还想了解更多的关于svg的资料的话那么希望自己百度。或是等我整理下关系学习Svg的知识,然后会在进行记录。
Demo等以后会上传。
GitHub地址;https://github.com/tianxia--/CanvasSample