本文共 8714 字,大约阅读时间需要 29 分钟。
作者:laer_L
应鸿洋大神建议完善一下细节,之前确实写得比较简洁,适合有功底的人看,本文重在思路和性能,就不介绍自定义 view 和 handler 避免内存泄漏或是导致空指针这些了,喜欢请 clone 项目并 star、fork 一下,感谢各位。
APP 需要做一个类似蚂蚁森林的功能模块,动效和蚂蚁森林接近,水滴最多 6 滴,根据经验来说这种交互肯定用 RN、H5 亦或游戏引擎来做最佳,但是没办法产品提了我们也得做。
老规矩还是先上GIF
蚂蚁森林.gif
也许看到这个图,你就不想再继续看下去了,心想这个动画很简单啊,不就是创建循环创建view,再给每个 view 的动画,每个 view 的开始运动的方向随机,再给一个加速器就搞定了嘛,如果你也是这样想那就该把这个文章看完了
首先创建水滴动画、缩放伴随透明度变化
消失时缩放伴随移动
水滴展示中是一直上下浮动的
每个水滴上下浮动的方向不定
每个水滴运动的速度时而快时而慢(这点也许你看不出,所以我再把抖动的范围加大再来一个GIF)
蚂蚁森林2.gif
首先我们肯定不能用每个 view 对应一个动画来处理,因为如果我是 100 低水滴,那岂不是要 100 个动画,这不得卡死呀,所以肯定是一个动画来完成,开始我第一想到的也是用 ValueAnimator 来做,但是一个 ValueAnimator 怎么去控制每个 view 的运动方向呢,有可能你会说每个 view 在初始化的时候给一个反向,确实可以解决运动方向不同的问题,但是怎么解决 view 运动的快慢不一样,并且时而快时而慢呢,并且每个 view 的运动规律根本不一样,最后我选择了 handler 来处理。
/* * 处理思路: * ->将森林水滴作为一个总体而不是单个的view,自定义一个ViewGroup容器 * ->循环创建view * ->为view随机设置位置(在一些固定的集合中随机选取,尽量保证水滴不重合) * ->为view设置一个初始的运动方向(注:由于每个view的运动方向不同,所以我选择将方向绑定到view的tag中) * ->为view设置一个初始的速度(同理:将初始速度绑定到view的tag中) * ->添加view到容器中,并缩放伴随透明度显示 * ->开启handler达到view上下位移动画(注意点:这里我们需要定一个临界值来改变view的速度,到达view时而快时而慢的目的) * ->点击view后,缩放、透明度伴随位移移除水滴 * ->界面销毁时停止调用handler避免内存泄漏,空指针等异常 * */
下面的讲解我将会贴出重要部分的代码,也就是思路关键点,完整代码还是请 clone 项目。
首先创建view
创建view代码块:/** * 添加水滴view */ private void addWaterView(Listwaters) { 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); } }
解释
创建view最重要的两件事情: 1、给view一个随机的方向并且保存到view的tag里 //随机设置view动画的方向 view.setTag(R.string.isUp, mRandom.nextBoolean()); 2、随机设置view的位置(我这里并非完全随机,而是给了一些值,然后随机选择这些值)、这里用了一个新的集合保存已经选择到的数,下次选择的时候排除这些值,因为最好水滴不要完全重合嘛。/**但是其实这不是我最终的方法,先往下看吧,还有彩蛋**/ /** * 获取x轴或是y轴上的随机值 * * @return */ private double getX_YRandom(ListchoseRandoms,List saveRandoms) { float random = 0; while (random == 0 || saveRandoms.contains(random)) { random = choseRandoms.get(mRandom.nextInt(choseRandoms.size())); } saveRandoms.add(random); return random; }
动画显示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设置一个初始的随机加速度(其实也是随机在已有的值中选取,因为速度不能相差太大)
/**控制水滴动画的快慢*/ private ListmSpds = Arrays.asList(0.5f, 0.3f, 0.2f, 0.1f); /** * 设置所有子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的tag里,这里两个参数setTag()方法不大了解的可以百度一下 view.setTag(R.string.spd, spd); }
接下来就是开启动画,使用handler设置view的偏移量了,这部分也是很关键的,还包括了处理水滴时而快时而慢的处理
/** * 设置偏移 */ private void setOffSet() { for (int i = 0; i < mViews.size(); i++) { View view = mViews.get(i); //拿到上次view保存的速度 float spd = (float) view.getTag(R.string.spd); //水滴初始的位置 float original = (float) view.getTag(R.string.original_y); float step = spd; boolean isUp = (boolean) view.getTag(R.string.isUp); float translationY; //根据水滴tag中的上下移动标识移动view 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); } }
接下来水滴点击后的消失动画
/** * 动画移除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); }
处理界面销毁
/** * 界面销毁时回调 */ @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); onDestroy(); } /** * 销毁 */ private void onDestroy() { isCancelAnimtion = true; mHandler.removeCallbacksAndMessages(this); } @SuppressLint("HandlerLeak") private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { //根据isCancelAnimtion来标识是否退出,防止界面销毁时,再一次改变UI if (isCancelAnimtion) { return; } setOffSet(); mHandler.sendEmptyMessageDelayed(WHAT_ADD_PROGRESS, PROGRESS_DELAY_MILLIS); } };
到这里动效就完了,运行就能达到想要的样子了,但是我的工作并没完,打开profiler一看OMG,在初始化view的地方内存剧增,数量稍稍多一点(10个)还会卡主,看来还的优化啊
image.png
很明显 private double getX_YRandom(ListchoseRandoms, ListsaveRandoms) 这个方法走了太多次,原因就在于我是循环创建view,并且在这个循环内为 view 随机创建位置,但是为了不完全重合,我这里又一次循环知道是一个不同的值为止,也就是说这里双重循环了
优化随机取用一个值后,就把这个值从集合移除,这样不就不会取到一样的值了么
/** * 获取x轴或是y轴上的随机值 * @return */ private double getX_YRandom(ListchoseRandoms, List saveRandoms) { if (choseRandoms.size() <= 0) { //防止水滴别可选项的个数还要多,这里就重新对可选项赋值 setCurrentCanChoseRandoms(); } //取用一个随机数,就移除一个随机数,达到不用循环遍历来确保获取不一样的值 float random = choseRandoms.get(mRandom.nextInt(choseRandoms.size())); choseRandoms.remove(random); saveRandoms.add(random); return random; }
顺便提一下有可能我们在创建水滴时,父容器还未初始化完成,处理如下
/** * 设置水滴 * @param waters */ public void setWaters(final Listwaters) { if (waters == null || waters.isEmpty()) { return; } //确保初始化完成 post(new Runnable() { @Override public void run() { setDatas(waters); } }); }
到这里就真的完了,优化后实测200个都没有一点卡顿,读者可以根据自己需求优化水滴的位置逻辑算法,因为我们产品明确说了最多6滴,所以我现在的水滴位置计算逻辑足够了,还是来个GIF吧
70个水滴.gif
https://link.jianshu.com/?t=https%3A%2F%2Fgithub.com%2F93Laer%2FWaterView
喜欢就赞一个吧,你的赞就是我的动力
Android Profiler使用讲解
https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fa_maker%2Farticle%2Fdetails%2F78958587分享朋友圈 也是另一种赞赏
The more we share, The more we have
欢迎加入数据君高效数据分析社区
进入大数据干货交流群可以加微信号:tongyuannow
目前100000+人已关注加入我们