目录
- 前言
- 自定义圆球
- 动态随机添加小球
前言
最近公司产品突然有一个类似支付宝蚂蚁森林的功能,大致功能跟支付宝蚂蚁森林相像,在看了一下支付宝蚂蚁森林的效果之后,打算先撸一个控件出来,等公司效果图出来之后就可以放上去直接使用。
首先我们先大致看下支付宝的蚂蚁森林效果图:
这是目前我实现的效果图:
当我们拿到这个需求时先分析一波,不要忙着就动手开干,不然容易平地翻车。。
需要实现的功能有:
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, { AttributeSet attrs) | |
super(context, attrs); | |
init(); | |
} | |
public WaterView(Context context, int defStyleAttr) { AttributeSet attrs, | |
super(context, attrs, defStyleAttr); | |
init(); | |
} | |
private void init() { | |
paint = new Paint(); | |
paint.setAntiAlias(true); | |
} | |
public void draw(Canvas canvas) { | |
super.draw(canvas); | |
drawCircleView(canvas); | |
} | |
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)))); | |
} | |
protected void onAttachedToWindow() { | |
super.onAttachedToWindow(); | |
start(); | |
} | |
protected void onDetachedFromWindow() { | |
super.onDetachedFromWindow(); | |
stop(); | |
} | |
protected void onVisibilityChanged(int visibility) { View changedView, | |
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 )
具体实现代码如下:
/** | |
* @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( { Context context) | |
super(context); | |
} | |
public WaterFlake( { Context context, AttributeSet attrs) | |
super(context, attrs); | |
} | |
public WaterFlake(int defStyleAttr) { Context context, AttributeSet attrs, | |
super(context, attrs, defStyleAttr); | |
} | |
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); | |
} | |
public boolean performClick() { | |
return super.performClick(); | |
} | |
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
super.onMeasure(widthMeasureSpec, heightMeasureSpec); | |
measureChildren(widthMeasureSpec, heightMeasureSpec); | |
} | |
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() { | |
public void onAnimationEnd(Animator animation) { | |
removeViewInLayout(view); | |
isCollect = false; | |
} | |
}); | |
} | |
public float getTreeCenterX() { | |
return treeCenterX; | |
} | |
public float getTreeCenterY() { | |
return treeCenterY; | |
} | |
} |
小球摆放随机算法有多种实现方式,这只是其中一种,写的不好的地方,还望各位指正,欢迎大家一起交流学习。