点击上方“Android技术杂货铺”,选择“置顶公众号”
干货文章,第一时间送达!
作者:laer_L
链接:https://www.jianshu.com/p/cc526bb34414,本文经作者授权推送。
很久没写东西了,又来一下,可能看了这个东效你会觉得很简单,但是如果你看完,我相信你还是会发现简单里面还是有文章,也许并不是看起来那样简单,本文精华在于 性能
老规矩还是先上GIF
也许看到这个图,你就不想再继续看下去了,心想这个动画很简单啊,不就是创建循环创建view,再给每个view的动画,每个view的开始运动的方向随机,再给一个加速器就搞定了嘛,如果你也是这样想那就该把这个文章看完了
分析:
-
首先创建水滴动画、缩放伴随透明度变化
-
消失时缩放伴随移动
-
水滴展示中是一直上下浮动的
-
每个水滴上下浮动的方向不定
-
每个水滴运动的速度时而快时而慢(这点也许你看不出,所以我再把
抖动的范围加大再来一个GIF)
首先我们肯定不能用每个view对应一个动画来处理,因为如果我是100低水滴,那岂不是要100个动画,这不得卡死呀,所以肯定是一个动画来完成,开始我第一想到的也是用ValueAnimator来做,但是一个ValueAnimator怎么去控制每个view的运动方向呢,有可能你会说每个view在初始化的时候给一个反向,确实可以解决运动方向不同的问题,但是怎么解决view运动的快慢不一样,并且时而快时而慢呢,并且每个view的运动规律根本不一样,最后我选择了handler来处理,但是到这里你以为完了嘛,继续往下看吧
正题
1 . 首先创建view
1、给view一个随机的方向并且保存到view的tag里
//随机设置view动画的方向
view.setTag(R.string.isUp, mRandom.nextBoolean());
2、随机设置view的位置(我这里并非完全随机,而是给了一些值,然后随机选择这些值)、这里用了一个新的集合保存已经选择到的数,下次选择的时候排除这些值,因为最好水滴不要完全重合嘛。
/**但是其实这不是我最终的方法,先往下看吧,还有彩蛋**/
/**
* 获取x轴或是y轴上的随机值
*
* @return
*/
private double getX_YRandom(List<Float> choseRandoms,List<Float> saveRandoms) {
float random = 0;
while (random == 0 || saveRandoms.contains(random)) {
random = choseRandoms.get(mRandom.nextInt(choseRandoms.size()));
}
saveRandoms.add(random);
return random;
}
还有一个动画显示view,这个就不展示了,绝对都能搞定
2 . 接下来为view设置一个初始的随机加速度(其实也是随机在已有的值中选取,因为速度不能相差太大)
/**控制水滴动画的快慢*/
private List<Float> mSpds = Arrays.asList(2.0f, 1.7f, 1.5f, 1.3f);
/**
* 设置所有子view的加速度
*/
private void setViewsSpd() {
for (int i = 0; i < mViews.size(); i++) {
View view = mViews.get(i);
setSpd(view);
}
}
/**
* 设置所有子view的加速度
*/
private void setViewsSpd() {
for (int i = 0; i < mViews.size(); i++) {
View view = mViews.get(i);
setSpd(view);
}
}
3 . 接下来就是使用handler设置view的偏移量了,这部分也是很关键的
/**
* 设置偏移
*/
private void setOffSet() {
for (int i = 0; i < mViews.size(); i++) {
View view = mViews.get(i);
float spd = (float) view.getTag(R.string.spd);
float original = (float) view.getTag(R.string.original_y);
float step = CHANGE_RANGE / BASE_OFFSET_MUL * spd;
//拿到初始设置的方向
boolean isUp = (boolean) view.getTag(R.string.isUp);
float translationY;
if (isUp) {
translationY = view.getY() - step;
} else {
translationY = view.getY() + step;
}
//控制view的移动范围不能大于我们规定的范围,如果到了这个值还要改变view的运动方向
if (translationY - original > CHANGE_RANGE) {
translationY = original + CHANGE_RANGE;
view.setTag(R.string.isUp, true);
} else if (translationY - original < -CHANGE_RANGE) {
translationY = original - CHANGE_RANGE;
//每次view运动到最下边,再一次向上运动时改变view的加速度,这样不就做到了view时而快时而慢了么
setSpd(view);
view.setTag(R.string.isUp, false);
}
view.setY(translationY);
}
}
4 . 接下来水滴点击后的消失动画,也不说了,都行
到这里动效就完了,但是我的工作并没完,打开profiler一看OMG,在初始化view的地方内存剧增,数量稍稍多一点(10个)还会卡主,看来还的优化啊
很明显private double getX_YRandom(List choseRandoms, List saveRandoms)这个方法走了太多次,原因就在于我是循环创建view,并且在这个循环内为view随机创建位置,但是为了不完全重合,我这里又一次循环知道是一个不同的值为止,也就是说这里双重循环了
-
优化随机取用一个值后,就把这个值从集合移除,这样不就不会取到一样的值了么,到这里就真的完了,优化后实测200个都没有一点卡顿,读者可以根据自己需求优化水滴的位置逻辑算法,因为我们产品明确说了最多6滴,所以我现在的水滴位置计算逻辑足够了,还是来个GIF吧
全部代码
/**
* 创建时间: 2018/1/9
* 创建人: laitianbing
* 描述: 蚂蚁森林模拟
*/
public class WaterView extends FrameLayout {
private static final int WHAT_ADD_PROGRESS = 1;
/**用来计算偏差值基础倍数,值越大,偏移越小*/
private static final int BASE_OFFSET_MUL = 12;
/**view变化的y抖动范围*/
private static final int CHANGE_RANGE = 50;
/**控制抖动动画执行的快慢*/
public static final int PROGRESS_DELAY_MILLIS = 60;
/**控制移除view的动画执行时间*/
public static final int REMOVE_DELAY_MILLIS = 2000;
/**添加水滴时动画显示view执行的时间*/
public static final int ANIMATION_SHOW_VIEW_DURATION = 500;
/**控制水滴动画的快慢*/
private List<Float> mSpds = Arrays.asList(2.0f, 1.7f, 1.5f, 1.3f);
/**x最多可选取的随机数值*/
private static final List<Float> X_MAX_CHOSE_RANDOMS = Arrays.asList(
0.01f,0.05f,0.1f,0.6f,0.11f, 0.16f, 0.21f, 0.26f, 0.31f, 0.7f, 0.75f, 0.8f, 0.85f, 0.87f);
/**y最多可选取的随机数值*/
private static final List<Float> Y_MAX_CHOSE_RANDOMS = Arrays.asList(
0.01f,0.06f,0.11f, 0.17f, 0.23f, 0.29f, 0.35f, 0.41f, 0.47f, 0.53f, 0.59f, 0.65f, 0.71f, 0.77f, 0.83f);
/**x坐标当前可选的随机数组*/
private List<Float> mXCurrentCanShoseRandoms = new ArrayList<>();
/**y坐标当前可选的随机数组*/
private List<Float> mYCurrentCanShoseRandoms = new ArrayList<>();
/**已经选取x的随机数值*/
private List<Float> mXRandoms = new ArrayList<>();
/**已经选取y的随机数值*/
private List<Float> mYRandoms = new ArrayList<>();
private Random mRandom = new Random();
private List<View> mViews = new ArrayList<>();
private int mChildViewRes = R.layout.water_item;//子view的资源文件
private LayoutInflater mInflater;
private int mTotalConsumeWater;//总的已经点击的水滴
private boolean isOpenAnimtion;//是否开启动画
private boolean isCancelAnimtion;//是否销毁动画
private int maxX, maxY;//子view的x坐标和y坐标的最大取值
private float mMaxSpace;//父控件对角线的距离
private Point mDestroyPoint ;//view销毁时的点
public WaterView(@NonNull Context context) {
this(context, null);
}
public WaterView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public WaterView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mInflater = LayoutInflater.from(getContext());
}
@SuppressLint("HandlerLeak") private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (isCancelAnimtion) {
return;
}
setOffSet();
mHandler.sendEmptyMessageDelayed(WHAT_ADD_PROGRESS, PROGRESS_DELAY_MILLIS);
}
};
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mMaxSpace = (float) Math.sqrt(w * w + h * h);
mDestroyPoint=new Point((int) getX(), h);
maxX = w;
maxY = h;
}
/**
* 重置子view
*/
private void reset() {
isCancelAnimtion = true;
isOpenAnimtion = false;
for (int i = 0; i < mViews.size(); i++) {
removeView(mViews.get(i));
}
mViews.clear();
mXRandoms.clear();
mYRandoms.clear();
mYCurrentCanShoseRandoms.clear();
mXCurrentCanShoseRandoms.clear();
mHandler.removeCallbacksAndMessages(null);
}
/**
* 设置水滴
* @param waters
*/
public void setWaters(final List<Water> waters) {
if (waters == null || waters.isEmpty()) {
return;
}
post(new Runnable() {
@Override
public void run() {
setDatas(waters);
}
});
}
/**
* 设置数据
* @param waters
*/
private void setDatas(List<Water> waters) {
reset();
isCancelAnimtion = false;
setCurrentCanChoseRandoms();
addWaterView(waters);
setViewsSpd();
startAnimation();
}
private void setCurrentCanChoseRandoms() {
mXCurrentCanShoseRandoms.addAll(X_MAX_CHOSE_RANDOMS);
mYCurrentCanShoseRandoms.addAll(Y_MAX_CHOSE_RANDOMS);
}
/**
* 添加水滴view
*/
private void addWaterView(List<Water> waters) {
for (int i = 0; i < waters.size(); i++) {
final Water water = waters.get(i);
View view = mInflater.inflate(mChildViewRes, this, false);
TextView tvWater = view.findViewById(R.id.tv_water);
view.setTag(water);
tvWater.setText(String.valueOf(water.getNumber()) + "g");
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
handViewClick(view);
}
});
//随机设置view动画的方向
view.setTag(R.string.isUp, mRandom.nextBoolean());
setChildViewLocation(view);
mViews.add(view);
addShowViewAnimation(view);
}
}
/**
* 添加显示动画
* @param view
*/
private void addShowViewAnimation(View view) {
addView(view);
view.setAlpha(0);
view.setScaleX(0);
view.setScaleY(0);
view.animate().alpha(1).scaleX(1).scaleY(1).setDuration(ANIMATION_SHOW_VIEW_DURATION).start();
}
/**
* 处理view点击
*
* @param view
*/
private void handViewClick(View view) {
//移除当前集合中的该view
mViews.remove(view);
Object tag = view.getTag();
if (tag instanceof Water) {
Water waterTag = (Water) tag;
mTotalConsumeWater += waterTag.getNumber();
Toast.makeText(getContext(), "当前点击的是:" + waterTag.getName() + "水滴的值是:"
+ waterTag.getNumber() + "总的水滴数是" + mTotalConsumeWater, Toast.LENGTH_SHORT).show();
}
view.setTag(R.string.original_y, view.getY());
animRemoveView(view);
}
/**
* 设置view在父控件中的位置
*
* @param view
*/
private void setChildViewLocation(View view) {
view.setX((float) (maxX * getX_YRandom(mXCurrentCanShoseRandoms,mXRandoms)));
view.setY((float) (maxY * getX_YRandom(mYCurrentCanShoseRandoms,mYRandoms)));
view.setTag(R.string.original_y, view.getY());
}
/**
* 获取x轴或是y轴上的随机值
*
* @return
*/
private double getX_YRandom(List<Float> choseRandoms,List<Float> saveRandoms) {
if (choseRandoms.size() <= 0) {
//防止水滴别可选项的个数还要多,这里就重新对可选项赋值
setCurrentCanChoseRandoms();
}
//取用一个随机数,就移除一个随机数
float random = choseRandoms.get(mRandom.nextInt(choseRandoms.size()));
choseRandoms.remove(random);
saveRandoms.add(random);
return random;
}
/**
* 设置所有子view的加速度
*/
private void setViewsSpd() {
for (int i = 0; i < mViews.size(); i++) {
View view = mViews.get(i);
setSpd(view);
}
}
/**
* 设置View的spd
*
* @param view
*/
private void setSpd(View view) {
float spd = mSpds.get(mRandom.nextInt(mSpds.size()));
view.setTag(R.string.spd, spd);
}
/**
* 设置偏移
*/
private void setOffSet() {
for (int i = 0; i < mViews.size(); i++) {
View view = mViews.get(i);
float spd = (float) view.getTag(R.string.spd);
float original = (float) view.getTag(R.string.original_y);
float step = CHANGE_RANGE / BASE_OFFSET_MUL * spd;
boolean isUp = (boolean) view.getTag(R.string.isUp);
float translationY;
if (isUp) {
translationY = view.getY() - step;
} else {
translationY = view.getY() + step;
}
if (translationY - original > CHANGE_RANGE) {
translationY = original + CHANGE_RANGE;
view.setTag(R.string.isUp, true);
} else if (translationY - original < -CHANGE_RANGE) {
translationY = original - CHANGE_RANGE;
setSpd(view);
view.setTag(R.string.isUp, false);
}
view.setY(translationY);
}
}
/**
* 获取两个点之间的距离
*
* @param p1
* @param p2
* @return
*/
public float getDistance(Point p1, Point p2) {
float _x = Math.abs(p2.x - p1.x);
float _y = Math.abs(p2.y - p1.y);
return (float) Math.sqrt(_x * _x + _y * _y);
}
/**
* 动画移除view
* @param view
*/
private void animRemoveView(final View view) {
final float x = view.getX();
final float y = view.getY();
float space = getDistance(new Point((int) x, (int) y), mDestroyPoint);
ValueAnimator animator = ValueAnimator.ofFloat(x, 0);
animator.setDuration((long) (REMOVE_DELAY_MILLIS / mMaxSpace * space));
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
if (isCancelAnimtion) {
return;
}
float value = (float) valueAnimator.getAnimatedValue();
float alpha = value / x;
float translationY = y + (x - value) * (maxY - y) / x;
setViewProperty(view, alpha, translationY, value);
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
removeView(view);
}
});
animator.start();
}
/**
* 设置view的属性
* @param view
* @param alpha
* @param translationY
* @param translationX
*/
private void setViewProperty( View view, float alpha,float translationY,float translationX) {
view.setTranslationY(translationY);
view.setTranslationX(translationX);
view.setAlpha(alpha);
view.setScaleY(alpha);
view.setScaleX(alpha);
}
/**
* 开启水滴抖动动画
*/
private void startAnimation() {
if (isOpenAnimtion) {
return;
}
mHandler.sendEmptyMessage(WHAT_ADD_PROGRESS);
isOpenAnimtion = true;
}
/**
* 销毁
*/
public void onDestroy() {
isCancelAnimtion = true;
mHandler.removeCallbacksAndMessages(this);
}
}
喜欢就赞一个吧。
往期干货
1
2
3