【黄啊码】安卓实现支付宝中的蚂蚁森林效果

举报
黄啊码 发表于 2022/09/02 00:26:45 2022/09/02
【摘要】 最近公司产品突然有一个类似支付宝蚂蚁森林的功能,大致功能跟支付宝蚂蚁森林相像,在看了一下支付宝蚂蚁森林的效果之后,打算先撸一个控件出来,等公司效果图出来之后就可以放上去直接使用。 首先我们先大致看下支付宝的蚂蚁森林效果图: 这是目前我实现的效果图: 当我们拿到这个需求时先分析一波,不要忙着就动手开干,不然容易平地翻车。。 需...

最近公司产品突然有一个类似支付宝蚂蚁森林的功能,大致功能跟支付宝蚂蚁森林相像,在看了一下支付宝蚂蚁森林的效果之后,打算先撸一个控件出来,等公司效果图出来之后就可以放上去直接使用。
首先我们先大致看下支付宝的蚂蚁森林效果图:

这是目前我实现的效果图:

当我们拿到这个需求时先分析一波,不要忙着就动手开干,不然容易平地翻车。。
需要实现的功能有:

1、自定小圆球,圆球内文字、上下浮动、消失动画;

2、根据数据动态添加小球,并且位置随机分布在小树周围,不能重叠。这点是最重要的,涉及到一个随机位置生成算法的设计。

好了,当我们确定了我们要实现的功能之后就可以逐步开始撸代码了。

自定义圆球

这个比较容易实现,绘制一个圆,再在园内绘制文字,动画实现统一采用的是属性动画来实现,代码如下,注释写的比较详细就不一一解释了,懒...


  
  1. /**
  2. * @describe: 自定义仿支付宝蚂蚁森林水滴View
  3. */
  4. public class WaterView extends View {
  5. private Paint paint;
  6. private ObjectAnimator mAnimator;
  7. /**
  8. * 文字颜色
  9. */
  10. private int textColor = Color.parseColor("#69c78e");
  11. /**
  12. * 水滴填充颜色
  13. */
  14. private int waterColor = Color.parseColor("#c3f593");
  15. /**
  16. * 球描边颜色
  17. */
  18. private int storkeColor = Color.parseColor("#69c78e");
  19. /**
  20. * 描边线条宽度
  21. */
  22. private float strokeWidth = 0.5f;
  23. /**
  24. * 文字字体大小
  25. */
  26. private float textSize = 36;
  27. /**
  28. * 根据远近距离的不同计算得到的应该占的半径比例
  29. */
  30. private float proportion;
  31. /**
  32. * 水滴球半径
  33. */
  34. private int mRadius = 30;
  35. /**
  36. * 圆球文字内容
  37. */
  38. private String textContent="3g";
  39. public WaterView(Context context) {
  40. super(context);
  41. init();
  42. }
  43. public WaterView(Context context, @Nullable AttributeSet attrs) {
  44. super(context, attrs);
  45. init();
  46. }
  47. public WaterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  48. super(context, attrs, defStyleAttr);
  49. init();
  50. }
  51. private void init() {
  52. paint = new Paint();
  53. paint.setAntiAlias(true);
  54. }
  55. @Override
  56. public void draw(Canvas canvas) {
  57. super.draw(canvas);
  58. drawCircleView(canvas);
  59. }
  60. @Override
  61. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  62. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  63. setMeasuredDimension(Utils.dp2px(getContext(), (int) (2 * (mRadius+strokeWidth))),Utils.dp2px(getContext(), (int) (2 * (mRadius+strokeWidth))));
  64. }
  65. @Override
  66. protected void onAttachedToWindow() {
  67. super.onAttachedToWindow();
  68. start();
  69. }
  70. @Override
  71. protected void onDetachedFromWindow() {
  72. super.onDetachedFromWindow();
  73. stop();
  74. }
  75. @Override
  76. protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
  77. super.onVisibilityChanged(changedView, visibility);
  78. if (visibility == VISIBLE) {
  79. start();
  80. } else {
  81. stop();
  82. }
  83. }
  84. private void drawCircleView(Canvas canvas){
  85. //圆球
  86. paint.setColor(waterColor);
  87. paint.setStyle(Paint.Style.FILL);
  88. canvas.drawCircle(Utils.dp2px(getContext(), mRadius), Utils.dp2px(getContext(), mRadius), Utils.dp2px(getContext(), mRadius), paint);
  89. //描边
  90. paint.setColor(storkeColor);
  91. paint.setStyle(Paint.Style.STROKE);
  92. paint.setStrokeWidth(Utils.dp2px(getContext(), (int) strokeWidth));
  93. canvas.drawCircle(Utils.dp2px(getContext(), mRadius), Utils.dp2px(getContext(), mRadius), Utils.dp2px(getContext(), (int) (mRadius+strokeWidth)) , paint);
  94. //圆球文字
  95. paint.setTextSize(textSize);
  96. paint.setColor(textColor);
  97. paint.setStyle(Paint.Style.FILL);
  98. drawVerticalText(canvas, Utils.dp2px(getContext(), mRadius), Utils.dp2px(getContext(), mRadius), textContent);
  99. }
  100. private void drawVerticalText(Canvas canvas, float centerX, float centerY, String text) {
  101. Paint.FontMetrics fontMetrics = paint.getFontMetrics();
  102. float baseLine = -(fontMetrics.ascent + fontMetrics.descent) / 2;
  103. float textWidth = paint.measureText(text);
  104. float startX = centerX - textWidth / 2;
  105. float endY = centerY + baseLine;
  106. canvas.drawText(text, startX, endY, paint);
  107. }
  108. public void start() {
  109. if (mAnimator == null) {
  110. mAnimator = ObjectAnimator.ofFloat(this, "translationY", -6.0f, 6.0f, -6.0f);
  111. mAnimator.setDuration(3500);
  112. mAnimator.setInterpolator(new LinearInterpolator());
  113. mAnimator.setRepeatMode(ValueAnimator.RESTART);
  114. mAnimator.setRepeatCount(ValueAnimator.INFINITE);
  115. mAnimator.start();
  116. } else if (!mAnimator.isStarted()) {
  117. mAnimator.start();
  118. }
  119. }
  120. public void stop() {
  121. if (mAnimator != null) {
  122. mAnimator.cancel();
  123. mAnimator = null;
  124. }
  125. }
  126. public float getProportion() {
  127. return proportion;
  128. }
  129. public void setProportion(float proportion) {
  130. this.proportion = proportion;
  131. }
  132. }

 

动态随机添加小球

这里我采用的是集成FrameLayout 通过设置小球数据,动态将小球add进去,比较简便,在这里最重要是动态随机添加小球的算法,解决了这个算法就好办了。通过仔细观察支付宝蚂蚁森林的效果实现,我们可以发现一般小球都是在树的正上方随机分布的。所以我想以小树的根为中心,小树的高度为半径为一个扇形,在这个扇形上方随机摆放小球。

公式:坐标 = 旋转角度 * 半径 * 根据远近距离的不同计算得到的应该占的半径比例

圆上任一点(x1,y1)坐标的计算公式:

x1 = x0 + r * cos(ao * 3.14 /180 )

y1 = y0 + r * sin(ao * 3.14 /180 )

具体实现代码如下:


  
  1. /**
  2. * @describe: 支付宝蚂蚁森林水滴能量
  3. */
  4. public class WaterFlake extends FrameLayout {
  5. private OnWaterItemListener mOnWaterItemListener;
  6. /**
  7. * 小树坐标X
  8. */
  9. private float treeCenterX = 0;
  10. /**
  11. * 小树坐标Y
  12. */
  13. private float treeCenterY = 0;
  14. /**
  15. * 小树高度
  16. */
  17. private int radius = 80;
  18. /**
  19. * 开始角度
  20. */
  21. private double mStartAngle = 0;
  22. /**
  23. * 是否正在收集能量
  24. */
  25. private boolean isCollect = false;
  26. public WaterFlake(@NonNull Context context) {
  27. super(context);
  28. }
  29. public WaterFlake(@NonNull Context context, @Nullable AttributeSet attrs) {
  30. super(context, attrs);
  31. }
  32. public WaterFlake(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  33. super(context, attrs, defStyleAttr);
  34. }
  35. @Override
  36. public boolean onTouchEvent(MotionEvent event) {
  37. if (event.getAction() == MotionEvent.ACTION_DOWN) {
  38. int x = (int) event.getX();
  39. int y = (int) event.getY();
  40. Rect rect = new Rect();
  41. for (int i = 0; i < getChildCount(); i++) {
  42. getChildAt(i).getHitRect(rect);
  43. if (rect.contains(x, y)) {
  44. if (mOnWaterItemListener != null) {
  45. getChildAt(i).performClick();
  46. mOnWaterItemListener.onItemClick(i);
  47. startAnimator(getChildAt(i));
  48. return true;
  49. }
  50. }
  51. }
  52. }
  53. return super.onTouchEvent(event);
  54. }
  55. @Override
  56. public boolean performClick() {
  57. return super.performClick();
  58. }
  59. @Override
  60. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  61. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  62. measureChildren(widthMeasureSpec, heightMeasureSpec);
  63. }
  64. @Override
  65. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  66. int childCount = getChildCount();
  67. if (childCount==0){
  68. return;
  69. }
  70. int left, top;
  71. // 根据tem的个数,计算角度
  72. float angleDelay = -180 / childCount;
  73. for (int i = 0; i < childCount; i++) {
  74. WaterView child = (WaterView) getChildAt(i);
  75. mStartAngle %= 180;
  76. //设置CircleView小圆点的坐标信息
  77. //坐标 = 旋转角度 * 半径 * 根据远近距离的不同计算得到的应该占的半径比例
  78. // 则圆上任一点为:(x1,y1)
  79. // x1 = x0 + r * cos(ao * 3.14 /180 )
  80. // y1 = y0 + r * sin(ao * 3.14 /180 )
  81. if (child.getVisibility() != GONE) {
  82. left = (int) (getTreeCenterX() + radius * Math.cos(mStartAngle * 3.14 / 180) * (child.getProportion() / radius * 2));
  83. top = (int) (getTreeCenterY() + radius * Math.sin(mStartAngle * 3.14 / 180) * (child.getProportion() / radius * 2));
  84. child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredWidth());
  85. }
  86. mStartAngle += angleDelay;
  87. }
  88. }
  89. /**
  90. * 设置小球数据,根据数据集合创建小球数量
  91. *
  92. * @param modelList 数据集合
  93. */
  94. public void setModelList(List<WaterModel> modelList, float treeCenterX, float treeCenterY) {
  95. this.treeCenterX = treeCenterX;
  96. this.treeCenterY = treeCenterY;
  97. for (int i = 0; i < modelList.size(); i++) {
  98. WaterView waterView = new WaterView(getContext(),(i+1)+"g");
  99. waterView.setProportion(Utils.getRandom(radius, radius + 80));
  100. addView(waterView);
  101. }
  102. }
  103. /**
  104. * 设置小球点击事件
  105. *
  106. * @param onWaterItemListener
  107. */
  108. public void setOnWaterItemListener(OnWaterItemListener onWaterItemListener) {
  109. mOnWaterItemListener = onWaterItemListener;
  110. }
  111. public interface OnWaterItemListener {
  112. void onItemClick(int pos);
  113. }
  114. private void startAnimator(final View view) {
  115. if (isCollect) {
  116. return;
  117. }
  118. isCollect = true;
  119. ObjectAnimator translatAnimatorY = ObjectAnimator.ofFloat(view, "translationY", getTreeCenterY());
  120. translatAnimatorY.start();
  121. ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
  122. alphaAnimator.start();
  123. AnimatorSet animatorSet = new AnimatorSet();
  124. animatorSet.play(translatAnimatorY).with(alphaAnimator);
  125. animatorSet.setDuration(3000);
  126. animatorSet.start();
  127. animatorSet.addListener(new AnimatorListenerAdapter() {
  128. @Override
  129. public void onAnimationEnd(Animator animation) {
  130. removeViewInLayout(view);
  131. isCollect = false;
  132. }
  133. });
  134. }
  135. public float getTreeCenterX() {
  136. return treeCenterX;
  137. }
  138. public float getTreeCenterY() {
  139. return treeCenterY;
  140. }
  141. }

 

小球摆放随机算法有多种实现方式,这只是其中一种,写的不好的地方,还望各位指正,欢迎大家一起交流学习。

目前代码还有一些地方需要完善的,逐步更新。。。

TODO

优化小球随机生成算法,保证每个小球尽量不会重叠

文章来源: markwcm.blog.csdn.net,作者:黄啊码,版权归原作者所有,如需转载,请联系作者。

原文链接:markwcm.blog.csdn.net/article/details/126642258

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。