效果图
什么?非入侵式?这是啥东西?也很简单,就是即拿即用,不去为了某个效果而去重写或者自定义布局。
看到上面的效果图,或许脑海里最先想到的是重写ViewGroup
的dispatchDraw
方法然后将自定义的ViewGroup在加入到xml中,这确实是一种实现思路。另一种就是我说的这个'非入侵'
思路,即将动画View抽出做成一个工具类,动态添加和删除。
思路:
- 自定义AnimationView,重写onDraw方法。
- 添加的顶层ViewGroup中。(顶层View其实就是
DecorView
也就是FrameLayout
正好将其View覆盖到最上面) - 播放具体动画
addView前需要先拿到目标View到屏幕的具体坐标值,这样才能在准确的位置展示动画
获取顶层View:
Window window = activity.getWindow();
ViewGroup container = (ViewGroup) window.getDecorView();
复制代码
获取目标View对于窗口的坐标
int[] viewXY = new int[2];
targetView.getLocationInWindow(viewXY); //下标0表示x,1表示y=>(x,y)
复制代码
绑定View
Window window = activity.getWindow();
if (window == null || activity.isFinishing()) {
return;
}
ViewGroup container = (ViewGroup) window.getDecorView();
...
if (likeAnimateView == null) {
likeAnimateView = new LikeAnimateView(window.getContext());
int[] viewXY = new int[2];
targetView.getLocationInWindow(viewXY);
int centerX = viewXY[0] + targetView.getWidth() / 2;
int centerY = viewXY[1] + targetView.getHeight() / 2;
likeAnimateView.setTargetXY(centerX, centerY);
ViewGroup.LayoutParams params = container.getLayoutParams();
ViewGroup.LayoutParams animParams = new ViewGroup.LayoutParams(params.width, params.height);
likeAnimateView.setLayoutParams(animParams);
...
container.addView(likeAnimateView);
}
复制代码
获取目标view的
绝对坐标
,计算目标view的中心坐标
,传入即将绑定的AnimateView,设置AnimateView大小为顶层View的大小
,添加view
具体动画实现:
这个动画仔细看其实是一个发散式
的效果(由内圈逐渐扩散至外圈的过程)
内圆上任意一点到外圆上任意一点到路径就是这个动画过程
分析完是一个路径的过程那么就需要用到Path
和PathMeasure
类配合使用
Path
设置圆路径
this.mPath.addCircle(x, y, 20, Path.Direction.CW);
复制代码
PathMeasure
Path路径点的坐标追踪
this.mPathMeasure.setPath(this.mPath, true);//第二个参数表示是否闭合路径
复制代码
PathMeasure.getPosTan(dis,pos,null)
获取Path
路径上对应坐标
this.mPathMeasure.getPosTan(discount, point, null);
复制代码
LikeInfo
类结构
public static class LikeInfo {
public Bitmap mBitmap;
public int x;
public int y;
public int startX, startY;
public Path path = new Path();
public PathMeasure pathMeasure = new PathMeasure();
public Paint paint = new Paint();
public ValueAnimator valueAnimator = new ValueAnimator();
public int zWidth = 0;
public LikeInfo() {
x = 0;
y = 0;
paint.setColor(Color.RED);
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.STROKE);
}
public void setBitmap(Bitmap bitmap) {
this.mBitmap = BitmapUtil.bitmapScale(bitmap, 60, 60);
}
}
复制代码
x,y
表示一张图的坐标,startX
,startY
开始的xy坐标主要用于计算当前坐标点与开始坐标点距离,可根据勾股定理
计算斜边长度a^2 + b^2 = c^2
开根号并与总路径做除法可得到一个比例
,根据比例可以进行图片的透明度动画控制
, 每个LikeInfo类有自己的动画控制类ValueAnimator
和画笔Paint
,Path
和PathMeasure
。每个Bitmap需要大小一致,做了图片比例缩放。
内圆部分代码:
this.mPath = new Path();
this.mPath.addCircle(x, y, 20, Path.Direction.CW);
this.mPathMeasure = new PathMeasure();
this.mPathMeasure.setPath(this.mPath, true);
float len = this.mPathMeasure.getLength();
float[] point = new float[2];
int index = 0;
//第一个初始圈
for (int i = 0; i < len; i += (len / LikeAnimationHelper.likesList.size())) {
if (index < LikeAnimationHelper.likesList.size()) {
this.mPathMeasure.getPosTan(i, point, null);
LikeAnimationHelper.LikeInfo likeInfo = LikeAnimationHelper.likesList.get(index);
likeInfo.x = (int) point[0];
likeInfo.y = (int) point[1];
likeInfo.startX = (int) point[0];
likeInfo.startY = (int) point[1];
}
index++;
}
复制代码
内圆部分
this.mPathMeasure.getLength()
获取的是路径内所有点坐标的数组长度,point
数组用于接收getPosTan
获取点具体坐标。i += (len / LikeAnimationHelper.likesList.size())
这里主要取根据动画图片list的平均分布坐标,这样可以在内圆中平均的在Canvas
中的Bitmap图片绘制到内圆上
内圆绘制效果
外圆部分代码:
this.mPath.reset();
this.mPath.addCircle(x, y, 250, Path.Direction.CW);
this.mPathMeasure.setPath(this.mPath, false);
len = this.mPathMeasure.getLength();
for (int i = 0; i < len; i += (len / LikeAnimationHelper.likesList.size())) {
this.mPathMeasure.getPosTan(i, point, null);
if (index < LikeAnimationHelper.likesList.size()) {
LikeAnimationHelper.LikeInfo likeInfo = LikeAnimationHelper.likesList.get(index);
likeInfo.path.reset();//重置
likeInfo.path.moveTo(likeInfo.x, likeInfo.y);
likeInfo.path.lineTo(point[0], point[1]);
likeInfo.zWidth = LikeAnimationHelper.computeDistance(likeInfo.x, likeInfo.y, point[0], point[1]);
likeInfo.pathMeasure.setPath(likeInfo.path, false);
likeInfo.valueAnimator.setFloatValues(0, likeInfo.pathMeasure.getLength());
likeInfo.valueAnimator.setDuration(LikeAnimationHelper.getRandomInt(200,1000));
likeInfo.valueAnimator.setInterpolator(new DecelerateInterpolator());
likeInfo.valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
float[] pos = new float[2];
likeInfo.pathMeasure.getPosTan(value, pos, null);
likeInfo.x = (int) pos[0];
likeInfo.y = (int) pos[1];
int bW = LikeAnimationHelper.computeDistance(likeInfo.startX, likeInfo.startY, likeInfo.x, likeInfo.y);
//当前路程 / 总路程 = 比例
float fit = (bW * 1.0f) / (likeInfo.zWidth * 1.0f);
//比例 * 255 = 透明度
int alpha = (int) Math.min((fit) * 255, 255);
//设置画笔透明度
likeInfo.paint.setAlpha(255 - alpha);
invalidate();
}
});
}
index++;
}
复制代码
前几行不用多说,和内圆部分代码相似,获取平均分布坐标点,for循环里面给每个动画对象计算距离和设置动画其中
moveTo
和lineTo
绘制内圆上一点
到外圆上一点
所形成的路径
,zWidth
代表其总路径长度(勾股定理
可得出斜边长度),将路径添加到PathMeasure
中后续在动画回调addUpdateListener
中通过getPosTan
获取当前路径每个坐标,然后依次将坐标设置到LikeInfo
中的x,y
上,就完成了整体移动的动画操作。
likeInfo.valueAnimator.setDuration(LikeAnimationHelper.getRandomInt(200,1000))
设置setDuration这里做了一个随机执行时间到处理,也就在动画中可以看作为打散
效果,若用固定的数值则是一个内圆扩散
的效果
外圆及其路径效果图:
问题:如何确定动画全部执行完了?
valueAnimator中有一个addListener
方法里面有一个回调onAnimationEnd
likeInfo.valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
endIndex += 1;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
复制代码
当
endIndex >= LikeAnimationHelper.likesList.size()
时表示动画已经执行完毕
private Thread mThread = new Thread() {
@Override
public void run() {
super.run();
while (endIndex < LikeAnimationHelper.likesList.size()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
post(new Runnable() {
@Override
public void run() {
//结束接口回调
iLikeAniationListener.onEnd();
}
});
}
};
复制代码
启动一个线程,每100毫秒检查一次即可
源码地址
LikeAnimationHelper
工具类方法startAnimation(Activity activity, View targetView)
参数1.activity 2.目标ViewLikeAnimateView
动画View