本篇文章已受权微信公众号 hongyangAndroid (鸿洋)独家发布 javascript
转载请标明出处:
gold.xitu.io/post/581c51…
本文出自:【张旭童的稀土掘金】(gold.xitu.io/user/56de21…)
代码传送门:喜欢的话,随手点个star。多谢
github.com/mcxtzhang/P…java一 概述
本来只是想模仿一下我魂牵梦萦的StoreHouse效果,没想到意外撸出来一个工具库。android
最简单用法,给我一个path(能够有多段),我还你一个动画。git
I have a path.I have a view. (Oh~),Path(Anim)View.github
<com.mcxtzhang.pathanimlib.PathAnimView
android:id="@+id/pathAnimView1"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:background="@color/blue"
android:padding="5dp"/>复制代码
Path sPath = new Path();
sPath.moveTo(0, 0);
sPath.addCircle(40, 40, 30, Path.Direction.CW);
pathAnimView1.setSourcePath(sPath);复制代码
先看效果图:(真机效果更棒哦,我本身的手机是去年某款599的手机,算是低端的了,6个View一块儿动画,不会卡,查看GPU呈现模式,95%时间都处于16ms线如下。性能还能够的)canvas
目前可配参数:
1 绘制方面,支持绘制Path的前景 背景色。api
//设置颜色
fillView2.setColorBg(Color.WHITE).setColorFg(Color.BLACK);复制代码
2 动画方面,目前支持设置动画的时长,是否无限循环等。微信
//设置了动画总时长,只执行一次的动画
fillView2.setAnimTime(3000).setAnimInfinite(false).startAnim();复制代码
3 仿StoreHouse风格的View,还支持设置残影的长度。架构
//设动画时长,设置了stoneHouse残影长度
storeView3.setPathMaxLength(1200).setAnimTime(20000).startAnim();复制代码
4 固然你能够拿到Paint本身搞事情:ide
//固然你能够本身拿到Paint,而后搞事情,我这里设置线条宽度
pathAnimView1.getPaint().setStrokeWidth(10);复制代码
PathAnimView的数据源是Path。(给我一个Path,还你一个动画View)
因此内置了几种将别的资源->Path的方法。
1 直接传string。 StoreHouse风格支持的A-Z,0-9 "." "- " " "(源自百万大神的库文末也有鸣谢,)
//根据String 转化成Path
setSourcePath(PathParserUtils.getPathFromArrayFloatList(StoreHousePath.getPath("ZhangXuTong", 1.1f, 16)));复制代码
2 定义在R.array.xxx里
//动态设置 从StringArray里取
storeView2.setSourcePath(PathParserUtils.getPathFromStringArray(this, R.array.storehouse, 3));复制代码
3 简单的SVG(半成品)
之前从gayHub上找了一个SVG-PATH的转换类:SvgPathParser,如今派上了用场,简单的SVG-PATH,能够,复杂的还有问题,还须要继续寻找更加方案。
//SVG转-》path
//还在完善中,我从github上找了以下工具类,发现简单的SVG能够转path,复杂点的 就乱了
/* SvgPathParser svgPathParser = new SvgPathParser(); try { Path path = svgPathParser.parsePath("M1,1 L1,50 L50,50 L50,50 L50,1 Z"); storeView3.setSourcePath(path); } catch (ParseException e) { e.printStackTrace(); }*/复制代码
普通PathAnimView
效果如图1 3。动画是 进度填充直到满的效果。
<com.mcxtzhang.pathanimlib.PathAnimView
android:id="@+id/pathAnimView1"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:background="@color/blue"
android:padding="5dp"/>复制代码
高仿StoreHouse风格AnimView:
这种View显示出来的效果如图2 4 6 。动画是 残影流动的效果。
<com.mcxtzhang.pathanimlib.StoreHouseAnimView
android:id="@+id/storeView3"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:background="@android:color/black"
android:padding="5dp"/>复制代码
fillView1.startAnim();复制代码
fillView1.stopAnim();复制代码
fillView1.clearAnim();复制代码
看到这里细心的朋友可能会发现,上一节,我没有提第5个图View是怎么定义的, 并且第五个View的效果,貌似和其余的不同,仔细看动画是否是像Android L+的系统自带进度条ProgressBar的效果?
那说明它的动画效果和我先前提到的两种不同,是的,一开始我撸是照着StoreHouse那种效果撸的,这是我次日才扩展的。
高级的用法,就是本控件动画的扩展性。
你彻底能够经过继承PathAnimHelper类
,重写onPathAnimCallback()
方法,扩展动画,图5就是这么来的。
先讲用法预览,稍后章节会详解。
用法:
对任意一个普通的PathAnimView,设置一个自定义的PathAnimHelper类便可:
//代码示例 动态对path加工,经过Helper
pathAnimView1.setPathAnimHelper(new CstSysLoadAnimHelper(pathAnimView1, pathAnimView1.getSourcePath(), pathAnimView1.getAnimPath()));复制代码
自定义的PathAnimHelper类:
/** * 介绍:自定义的PathAnimHelper,实现相似Android L+ 进度条效果 * 做者:zhangxutong * 邮箱:zhangxutong@imcoming.com * 时间: 2016/11/3. */
public class CstSysLoadAnimHelper extends PathAnimHelper {
public CstSysLoadAnimHelper(View view, Path sourcePath, Path animPath) {
super(view, sourcePath, animPath);
}
public CstSysLoadAnimHelper(View view, Path sourcePath, Path animPath, long animTime, boolean isInfinite) {
super(view, sourcePath, animPath, animTime, isInfinite);
}
@Override
public void onPathAnimCallback(View view, Path sourcePath, Path animPath, PathMeasure pathMeasure, ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
//获取一个段落
float end = pathMeasure.getLength() * value;
float begin = (float) (end - ((0.5 - Math.abs(value - 0.5)) * pathMeasure.getLength()));
animPath.reset();
animPath.lineTo(0, 0);
pathMeasure.getSegment(begin, end, animPath, true);
}
}复制代码
伸手党看到这里若是感兴趣,就能够直接一步gayhub了
(github.com/mcxtzhang/P…)
后文比较长,须要自带耐心观看。
这里我简单画了一下本文介绍的几个类的类图:
对于重要方法和属性标注了一下。
PathAnimView
继承自View,是一个自定义View。
PathAnimHelper
,专一作
Path动画。它默认的实现是
逐渐填充 的动画效果。
通常状况下只须要更换PathAnimHelper
,PathAnimView
便可作出不一样的动画。(图1第5个View)
可是若是须要扩充一些动画属性供用户设置,例如仿StoreHouse风格的动画View,想暴露 残影长度 属性供设置。
我这里采用的是:继承自PathAnimView
,并增长属性get、set 方法,并重写getInitAnimHeper()
方法,返回自定义的PathAnimHelper
。
如StoreHouseAnimView
继承自PathAnimView
,增长了残影长度的get、set方法。并重写getInitAnimHeper()
方法,返回StoreHouseAnimHelper
对象。 StoreHouseAnimHelper
类继承的是PathAnimHelper
。
基础类是PathAnimView
和PathAnimHelper
。
先看PathAnimView
:
这里我将一些不重要的get、set方法和构造方法剔除,留下比较重要的方法。
一个作路径动画的View
代码自己不难,注释也比较详细,核心的话,就是onDraw()
方法咯:
我这里用平移作的paddingLeft、paddingTop。
先利用源Path(mSourcePath)绘制底边的样子。
再利用变化的animPath(mAnimPath)绘制前景,这样animPath不断变化,而且重绘View->onDraw(),前景就会不断变化,造成动画效果。
那么核心就是animPath的的变化了,animPath的变化交由 mPathAnimHelper去作。
核心源码以下:
public class PathAnimView extends View {
protected Path mSourcePath;//须要作动画的源Path
protected Path mAnimPath;//用于绘制动画的Path
protected Paint mPaint;
protected int mColorBg = Color.GRAY;//背景色
protected int mColorFg = Color.WHITE;//前景色 填充色
protected PathAnimHelper mPathAnimHelper;//Path动画工具类
protected int mPaddingLeft, mPaddingTop;
public PathAnimView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/** * 这个方法可能会常常用到,用于设置源Path * * @param sourcePath * @return */
public PathAnimView setSourcePath(Path sourcePath) {
mSourcePath = sourcePath;
initAnimHelper();
return this;
}
/** * INIT FUNC **/
protected void init() {
//Paint
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
//动画路径只要初始化便可
mAnimPath = new Path();
//初始化动画帮助类
initAnimHelper();
}
/** * 初始化动画帮助类 */
protected void initAnimHelper() {
mPathAnimHelper = getInitAnimHeper();
//mPathAnimHelper = new PathAnimHelper(this, mSourcePath, mAnimPath, 1500, true);
}
/** * 子类可经过重写这个方法,返回自定义的AnimHelper * * @return */
protected PathAnimHelper getInitAnimHeper() {
return new PathAnimHelper(this, mSourcePath, mAnimPath);
}
/** * draw FUNC **/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//平移
canvas.translate(mPaddingLeft, mPaddingTop);
//先绘制底,
mPaint.setColor(mColorBg);
canvas.drawPath(mSourcePath, mPaint);
//再绘制前景,mAnimPath不断变化,不断重绘View的话,就会有动画效果。
mPaint.setColor(mColorFg);
canvas.drawPath(mAnimPath, mPaint);
}
/** * 设置动画 循环 */
public PathAnimView setAnimInfinite(boolean infinite) {
mPathAnimHelper.setInfinite(infinite);
return this;
}
/** * 设置动画 总时长 */
public PathAnimView setAnimTime(long animTime) {
mPathAnimHelper.setAnimTime(animTime);
return this;
}
/** * 执行循环动画 */
public void startAnim() {
mPathAnimHelper.startAnim();
}
/** * 中止动画 */
public void stopAnim() {
mPathAnimHelper.stopAnim();
}
/** * 清除并中止动画 */
public void clearAnim() {
stopAnim();
mAnimPath.reset();
mAnimPath.lineTo(0, 0);
invalidate();
}
}复制代码
看看最基础的PathAnimHelper
类是怎么作的,同样省略一些代码:
它是一个PathAnimView的Path动画的工具类
值得一提的是,这里的动画时间,是指循环取出SourcePath里的N段Path的总时间。
startAnim()
方法是入口,这个方法会在PathAnimView里被调用。
在startAnim()
方法里,先初始化一个PathMeasure
,以及重置animPath
。
而后利用PathMeasure.nextContour()
方法,循环一遍SourcePath的Path段数count,
利用这个count求出每段小Path应该执行的动画时间:totalDuaration / count
。
而后便调用loopAnim()
方法,循环取出每一段path ,并执行动画。
loopAnim()
方法里,定义一个无限循环的属性动画mAnimator
,
为其设置AnimatorUpdateListener
和onAnimationRepeat
,监听动画的更新和重复。
重点就在这两个监听器里:
public void onAnimationUpdate(ValueAnimator animation) {
//增长一个callback 便于子类重写搞事情
onPathAnimCallback(view, sourcePath, animPath, pathMeasure, animation);
//通知View刷新本身
view.invalidate();
}复制代码
动画每次Update的时候,回调onPathAnimCallback()
方法,在里面对animPath作处理。
对AnimPath处理之后,就可让View绘制新animPath造成动画了:
而后就是让View重绘,这样就会重走onDraw()方法,就是上一节提到的内容。
onPathAnimCallback()
方法也很简单,按动画进度值,取出当前这一小段的path的部分路径,赋值给animPath。
public void onPathAnimCallback(View view, Path sourcePath, Path animPath, PathMeasure pathMeasure, ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
//获取一个段落
pathMeasure.getSegment(0, pathMeasure.getLength() * value, animPath, true);
}复制代码
在Repeat监听器里:
public void onAnimationRepeat(Animator animation) {
//绘制完一条Path以后,再绘制下一条
pathMeasure.nextContour();
//长度为0 说明一次循环结束
if (pathMeasure.getLength() == 0) {
if (isInfinite) {//若是须要循环动画
animPath.reset();
animPath.lineTo(0, 0);
pathMeasure.setPath(sourcePath, false);
} else {//不须要就中止(由于repeat是无限 须要手动中止)
animation.end();
}
}
}复制代码
由于SourcePath里是可能含有1+段Path的,这里是合适的时机,利用pathMeasure.nextContour();
循环取出下一段Path, 判断一下新Path的长度,若是为0,说明这一次大循环结束,即用户视觉上的一次动画进度100%了。
这里判断咱们设置的isInfinite
属性,
若是是true,说明是循环动画,那么作初始化工做:
清空咱们的animPath,初始化pathMeasure。(和startAnim()
方法里的初始化工做一致)。
若是是false,说明动画须要中止,那么手动调用animation.end()
中止动画。(图1,第三个动画)
核心源码以下:
public class PathAnimHelper {
protected static final long mDefaultAnimTime = 1500;//默认动画总时间
protected View mView;//执行动画的View
protected Path mSourcePath;//源Path
protected Path mAnimPath;//用于绘制动画的Path
protected long mAnimTime;//动画一共的时间
protected boolean mIsInfinite;//是否无限循环
protected ValueAnimator mAnimator;//动画对象
public PathAnimHelper(View view, Path sourcePath, Path animPath, long animTime, boolean isInfinite) {
if (view == null || sourcePath == null || animPath == null) {
Log.e(TAG, "PathAnimHelper init error: view 、sourcePath、animPath can not be null");
return;
}
mView = view;
mSourcePath = sourcePath;
mAnimPath = animPath;
mAnimTime = animTime;
mIsInfinite = isInfinite;
}
/** * 执行动画 */
public void startAnim() {
startAnim(mView, mSourcePath, mAnimPath, mAnimTime, mIsInfinite);
}
/** * 一个SourcePath 内含多段Path,循环取出每段Path,并作一个动画 * 自定义动画的总时间 * 和是否循环 * * @param view 须要作动画的自定义View * @param sourcePath 源Path * @param animPath 自定义View用这个Path作动画 * @param totalDuaration 动画一共的时间 * @param isInfinite 是否无限循环 */
protected void startAnim(View view, Path sourcePath, Path animPath, long totalDuaration, boolean isInfinite) {
if (view == null || sourcePath == null || animPath == null) {
return;
}
PathMeasure pathMeasure = new PathMeasure();
//先重置一下须要显示动画的path
animPath.reset();
animPath.lineTo(0, 0);
pathMeasure.setPath(sourcePath, false);
//这里仅仅是为了 计算一下每一段的duration
int count = 0;
while (pathMeasure.getLength() != 0) {
pathMeasure.nextContour();
count++;
}
//通过上面这段计算duration代码的折腾 须要从新初始化pathMeasure
pathMeasure.setPath(sourcePath, false);
loopAnim(view, sourcePath, animPath, totalDuaration, pathMeasure, totalDuaration / count, isInfinite);
}
/** * 循环取出每一段path ,并执行动画 * * @param animPath 自定义View用这个Path作动画 * @param pathMeasure 用于测量的PathMeasure */
protected void loopAnim(final View view, final Path sourcePath, final Path animPath, final long totalDuaration, final PathMeasure pathMeasure, final long duration, final boolean isInfinite) {
//动画正在运行的话,先stop吧。万一有人要使用新动画呢,(正经用户不会这么用。)
stopAnim();
mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.setDuration(duration);
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//增长一个callback 便于子类重写搞事情
onPathAnimCallback(view, sourcePath, animPath, pathMeasure, animation);
//通知View刷新本身
view.invalidate();
}
});
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationRepeat(Animator animation) {
//每段path走完后,要补一下 某些状况会出现 animPath不满的状况
pathMeasure.getSegment(0, pathMeasure.getLength(), animPath, true);
//绘制完一条Path以后,再绘制下一条
pathMeasure.nextContour();
//长度为0 说明一次循环结束
if (pathMeasure.getLength() == 0) {
if (isInfinite) {//若是须要循环动画
animPath.reset();
animPath.lineTo(0, 0);
pathMeasure.setPath(sourcePath, false);
} else {//不须要就中止(由于repeat是无限 须要手动中止)
animation.end();
}
}
}
});
mAnimator.start();
}
/** * 中止动画 */
public void stopAnim() {
if (null != mAnimator && mAnimator.isRunning()) {
mAnimator.end();
}
}
/** * 用于子类继承搞事情,对animPath进行再次操做的函数 * * @param view * @param sourcePath * @param animPath * @param pathMeasure */
public void onPathAnimCallback(View view, Path sourcePath, Path animPath, PathMeasure pathMeasure, ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
//获取一个段落
pathMeasure.getSegment(0, pathMeasure.getLength() * value, animPath, true);
}
}复制代码
至此两个最基础的类就讲完了,如此简单就可实现图1第一、3个动画效果。
咱们前面提过,扩展动画,核心是继承PathAnimHelper 重写onPathAnimCallback()
方法便可,因此实现StoreHouse风格,核心类就是StoreHouseAnimHelper
。
核心代码以下:
public class StoreHouseAnimHelper extends PathAnimHelper {
private final static long MAX_LENGTH = 400;
private long mPathMaxLength;//残影路径最大长度
Path mStonePath;//暂存一下路径,最终要复制给animPath的
PathMeasure mPm;
private ArrayList<Float> mPathLengthArray;//路径长度array
private SparseArray<Boolean> mPathNeedAddArray;//路径是否须要被所有Add的Array
private int partIndex;//残缺的index
private float partLength;//残缺部分的长度
public StoreHouseAnimHelper(View view, Path sourcePath, Path animPath, long animTime, boolean isInfinite) {
super(view, sourcePath, animPath, animTime, isInfinite);
mPathMaxLength = MAX_LENGTH;
mStonePath = new Path();
mPm = new PathMeasure();
mPathLengthArray = new ArrayList<>();//顺序存放path的length
mPathNeedAddArray = new SparseArray<>();
}
@Override
public void onPathAnimCallback(View view, Path sourcePath, Path animPath, PathMeasure pathMeasure, ValueAnimator animation) {
super.onPathAnimCallback(view, sourcePath, animPath, pathMeasure, animation);
//仿StoneHouse效果 ,如今的作法很挫
//重置变量
mStonePath.reset();
mStonePath.lineTo(0, 0);
mPathLengthArray.clear();
//循环一遍AnimPath,记录里面每一段小Path的length。
mPm.setPath(animPath, false);
while (mPm.getLength() != 0) {
mPathLengthArray.add(mPm.getLength());
mPm.nextContour();
}
//逆序遍历AnimPath,记录哪些子Path是须要add的,而且记录那段须要部分add的path的下标
mPathNeedAddArray.clear();
float totalLength = 0;
partIndex = 0;
partLength = 0;
for (int i = mPathLengthArray.size() - 1; i >= 0; i--) {
if (totalLength + mPathLengthArray.get(i) <= mPathMaxLength) {//加上了也没满
mPathNeedAddArray.put(i, true);
totalLength = totalLength + mPathLengthArray.get(i);
} else if (totalLength < mPathMaxLength) {//加上了满了,可是不加就没满
partIndex = i;
partLength = mPathMaxLength - totalLength;
totalLength = totalLength + mPathLengthArray.get(i);
}
}
//循环Path,并获得最终要显示的AnimPath
mPm.setPath(animPath, false);
int i = 0;
while (mPm.getLength() != 0) {
if (mPathNeedAddArray.get(i, false)) {
mPm.getSegment(0, mPm.getLength(), mStonePath, true);
} else if (i == partIndex) {
mPm.getSegment(mPm.getLength() - partLength, mPm.getLength(), mStonePath, true);
}
mPm.nextContour();
i++;
}
animPath.set(mStonePath);
}
}复制代码
直接上码了,得益于咱们的设计,很简单:
重写getInitAnimHeper()
返回咱们的StoreHouseAnimHelper
,并增长残影长度的get、set方法。
public class StoreHouseAnimView extends PathAnimView {
public StoreHouseAnimView(Context context) {
this(context, null);
}
public StoreHouseAnimView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public StoreHouseAnimView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/** * GET SET FUNC **/
public long getPathMaxLength() {
return ((StoreHouseAnimHelper) mPathAnimHelper).getPathMaxLength();
}
/** * 设置残影最大长度 * * @param pathMaxLength * @return */
public StoreHouseAnimView setPathMaxLength(long pathMaxLength) {
((StoreHouseAnimHelper) mPathAnimHelper).setPathMaxLength(pathMaxLength);
return this;
}
@Override
protected PathAnimHelper getInitAnimHeper() {
return new StoreHouseAnimHelper(this, mSourcePath, mAnimPath);
}
}复制代码
前面提过,如图1第五个动画的效果,就是后期我加入扩展的,分析一下这种效果,它和普通的PathAnimView
的效果只有动画不一样,也不须要额外引入属性暴露出去供设置,因此这种场景,咱们只须要重写一个PathAnimHelper
类,set给PathAnimView
便可。
代码第一章节也提过,
一点注意的地方就是,这里没有同第四章节那样调用super.onPathAnimCallback(view, sourcePath, animPath, pathMeasure, animation);
。
由于第四章仿StoreHouse的效果,是在第三章的效果基础之上加工而成的。因此须要PathAnimHeper
先处理一下。
而咱们这里实现的仿系统ProgressBar的效果,则是彻底重写的。
核心方法以下重写,很简单,再也不赘述:
@Override
public void onPathAnimCallback(View view, Path sourcePath, Path animPath, PathMeasure pathMeasure, ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
//获取一个段落
float end = pathMeasure.getLength() * value;
float begin = (float) (end - ((0.5 - Math.abs(value - 0.5)) * pathMeasure.getLength()));
animPath.reset();
animPath.lineTo(0, 0);
pathMeasure.getSegment(begin, end, animPath, true);
}复制代码
总结起来就是 I have a path.I have a view. (Oh~),Path(Anim)View.
利用这条裤子,只要传一个Path进去,就能够实现多姿多彩的酷炫Path动画,若是对动画不满意,还能够本身动态扩展。
目前最急需完善的:
SVG->Android PATH的转换,
但愿有知道的兄弟能够告知一下,多谢。
代码传送门:喜欢的话,随手点个star。多谢
github.com/mcxtzhang/P…
StoreHouse风格点阵资源引用处,也是我第一次看见StoreHouse动画:百万大神的库: github.com/liaohuqiu/a…
一开始我看到这种动画,我仍是个小菜鸡,也不知道怎么实现的,可是一直在我脑海里挥之不去。
后来忽然有一天想到能够用Path、PathMeasure作自定义View动画来实现,就开始动笔写了起来。
发现Path的路径不太好获取,因而翻看百万大神的库,发现他并非使用的Path动画,可是的的确确是利用点阵设置数据源的,因而我就借助了这些最原始的点阵资源,撸出了这么一个Path动画。
最初只是想实现这么一个效果,了却个人心愿,没想到还有意外收获。有了这个Path动画工具类库。说实话写这么一个东西,我不知不觉也提高了,之前可能不太会把层级分的这么开,利用继承组合的方式去扩展功能。之前大多仍是C V 一份代码改一改,像图上的效果,我可能会分开自定义三个View去作,复制一些重复代码也不在意,看来坚持会有收获。但愿咱们都一块儿进步。