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

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

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

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

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

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

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

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

自定义圆球

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

/**
* @describe: 自定义仿支付宝蚂蚁森林水滴View
*/

public class WaterView extends View {

private Paint paint;
private ObjectAnimator mAnimator;
/**
* 文字颜色
*/
private int textColor = Color.parseColor("#69c78e");
/**
* 水滴填充颜色
*/
private int waterColor = Color.parseColor("#c3f593");
/**
* 球描边颜色
*/
private int storkeColor = Color.parseColor("#69c78e");
/**
* 描边线条宽度
*/
private float strokeWidth = 0.5f;
/**
* 文字字体大小
*/
private float textSize = 36;
/**
* 根据远近距离的不同计算得到的应该占的半径比例
*/
private float proportion;
/**
* 水滴球半径
*/
private int mRadius = 30;
/**
* 圆球文字内容
*/
private String textContent="3g";

public WaterView(Context context) {
super(context);
init();
}

public WaterView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

public WaterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
paint = new Paint();
paint.setAntiAlias(true);
}

@Override
public void draw(Canvas canvas) {
super.draw(canvas);
drawCircleView(canvas);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(Utils.dp2px(getContext(), (int) (2 * (mRadius+strokeWidth))),Utils.dp2px(getContext(), (int) (2 * (mRadius+strokeWidth))));
}

@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
start();
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stop();
}

@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility == VISIBLE) {
start();
} else {
stop();
}
}

private void drawCircleView(Canvas canvas){
//圆球
paint.setColor(waterColor);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(Utils.dp2px(getContext(), mRadius), Utils.dp2px(getContext(), mRadius), Utils.dp2px(getContext(), mRadius), paint);

//描边
paint.setColor(storkeColor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(Utils.dp2px(getContext(), (int) strokeWidth));
canvas.drawCircle(Utils.dp2px(getContext(), mRadius), Utils.dp2px(getContext(), mRadius), Utils.dp2px(getContext(), (int) (mRadius+strokeWidth)) , paint);

//圆球文字
paint.setTextSize(textSize);
paint.setColor(textColor);
paint.setStyle(Paint.Style.FILL);
drawVerticalText(canvas, Utils.dp2px(getContext(), mRadius), Utils.dp2px(getContext(), mRadius), textContent);
}

private void drawVerticalText(Canvas canvas, float centerX, float centerY, String text) {
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
float baseLine = -(fontMetrics.ascent + fontMetrics.descent) / 2;
float textWidth = paint.measureText(text);
float startX = centerX - textWidth / 2;
float endY = centerY + baseLine;
canvas.drawText(text, startX, endY, paint);
}


public void start() {
if (mAnimator == null) {
mAnimator = ObjectAnimator.ofFloat(this, "translationY", -6.0f, 6.0f, -6.0f);
mAnimator.setDuration(3500);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.setRepeatMode(ValueAnimator.RESTART);
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
mAnimator.start();
} else if (!mAnimator.isStarted()) {
mAnimator.start();
}
}

public void stop() {
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
}

public float getProportion() {
return proportion;
}

public void setProportion(float proportion) {
this.proportion = proportion;
}
}

 

动态随机添加小球

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

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

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

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

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

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

具体实现代码如下:

/**
* @describe: 支付宝蚂蚁森林水滴能量
*/

public class WaterFlake extends FrameLayout {

private OnWaterItemListener mOnWaterItemListener;
/**
* 小树坐标X
*/
private float treeCenterX = 0;
/**
* 小树坐标Y
*/
private float treeCenterY = 0;
/**
* 小树高度
*/
private int radius = 80;
/**
* 开始角度
*/
private double mStartAngle = 0;
/**
* 是否正在收集能量
*/
private boolean isCollect = false;

public WaterFlake(@NonNull Context context) {
super(context);
}

public WaterFlake(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

public WaterFlake(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
Rect rect = new Rect();
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).getHitRect(rect);
if (rect.contains(x, y)) {
if (mOnWaterItemListener != null) {
getChildAt(i).performClick();
mOnWaterItemListener.onItemClick(i);
startAnimator(getChildAt(i));
return true;
}
}
}
}
return super.onTouchEvent(event);
}

@Override
public boolean performClick() {
return super.performClick();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
if (childCount==0){
return;
}
int left, top;
// 根据tem的个数,计算角度
float angleDelay = -180 / childCount;
for (int i = 0; i < childCount; i++) {
WaterView child = (WaterView) getChildAt(i);
mStartAngle %= 180;

//设置CircleView小圆点的坐标信息
//坐标 = 旋转角度 * 半径 * 根据远近距离的不同计算得到的应该占的半径比例
// 则圆上任一点为:(x1,y1)
// x1 = x0 + r * cos(ao * 3.14 /180 )
// y1 = y0 + r * sin(ao * 3.14 /180 )
if (child.getVisibility() != GONE) {
left = (int) (getTreeCenterX() + radius * Math.cos(mStartAngle * 3.14 / 180) * (child.getProportion() / radius * 2));
top = (int) (getTreeCenterY() + radius * Math.sin(mStartAngle * 3.14 / 180) * (child.getProportion() / radius * 2));
child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredWidth());
}
mStartAngle += angleDelay;
}
}

/**
* 设置小球数据,根据数据集合创建小球数量
*
* @param modelList 数据集合
*/
public void setModelList(List<WaterModel> modelList, float treeCenterX, float treeCenterY) {
this.treeCenterX = treeCenterX;
this.treeCenterY = treeCenterY;
for (int i = 0; i < modelList.size(); i++) {
WaterView waterView = new WaterView(getContext(),(i+1)+"g");
waterView.setProportion(Utils.getRandom(radius, radius + 80));
addView(waterView);
}
}

/**
* 设置小球点击事件
*
* @param onWaterItemListener
*/
public void setOnWaterItemListener(OnWaterItemListener onWaterItemListener) {
mOnWaterItemListener = onWaterItemListener;
}

public interface OnWaterItemListener {
void onItemClick(int pos);
}

private void startAnimator(final View view) {
if (isCollect) {
return;
}
isCollect = true;

ObjectAnimator translatAnimatorY = ObjectAnimator.ofFloat(view, "translationY", getTreeCenterY());
translatAnimatorY.start();

ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
alphaAnimator.start();

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(translatAnimatorY).with(alphaAnimator);
animatorSet.setDuration(3000);
animatorSet.start();
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
removeViewInLayout(view);
isCollect = false;
}
});
}

public float getTreeCenterX() {
return treeCenterX;
}

public float getTreeCenterY() {
return treeCenterY;
}
}

 

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

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

TODO

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