Android模擬實(shí)現(xiàn)支付寶螞蟻森林效果
前言
最近公司產(chǎn)品突然有一個(gè)類似支付寶螞蟻森林的功能,大致功能跟支付寶螞蟻森林相像,在看了一下支付寶螞蟻森林的效果之后,打算先擼一個(gè)控件出來(lái),等公司效果圖出來(lái)之后就可以放上去直接使用。
首先我們先大致看下支付寶的螞蟻森林效果圖:

這是目前我實(shí)現(xiàn)的效果圖:

當(dāng)我們拿到這個(gè)需求時(shí)先分析一波,不要忙著就動(dòng)手開(kāi)干,不然容易平地翻車。。
需要實(shí)現(xiàn)的功能有:
1、自定小圓球,圓球內(nèi)文字、上下浮動(dòng)、消失動(dòng)畫(huà);
2、根據(jù)數(shù)據(jù)動(dòng)態(tài)添加小球,并且位置隨機(jī)分布在小樹(shù)周圍,不能重疊。這點(diǎn)是最重要的,涉及到一個(gè)隨機(jī)位置生成算法的設(shè)計(jì)。
好了,當(dāng)我們確定了我們要實(shí)現(xiàn)的功能之后就可以逐步開(kāi)始擼代碼了。
自定義圓球
這個(gè)比較容易實(shí)現(xiàn),繪制一個(gè)圓,再在園內(nèi)繪制文字,動(dòng)畫(huà)實(shí)現(xiàn)統(tǒng)一采用的是屬性動(dòng)畫(huà)來(lái)實(shí)現(xiàn),代碼如下,注釋寫(xiě)的比較詳細(xì)就不一一解釋了,懶...
/**
* @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;
/**
* 根據(jù)遠(yuǎn)近距離的不同計(jì)算得到的應(yīng)該占的半徑比例
*/
private float proportion;
/**
* 水滴球半徑
*/
private int mRadius = 30;
/**
* 圓球文字內(nèi)容
*/
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;
}
}動(dòng)態(tài)隨機(jī)添加小球
這里我采用的是集成FrameLayout 通過(guò)設(shè)置小球數(shù)據(jù),動(dòng)態(tài)將小球add進(jìn)去,比較簡(jiǎn)便,在這里最重要是動(dòng)態(tài)隨機(jī)添加小球的算法,解決了這個(gè)算法就好辦了。通過(guò)仔細(xì)觀察支付寶螞蟻森林的效果實(shí)現(xiàn),我們可以發(fā)現(xiàn)一般小球都是在樹(shù)的正上方隨機(jī)分布的。所以我想以小樹(shù)的根為中心,小樹(shù)的高度為半徑為一個(gè)扇形,在這個(gè)扇形上方隨機(jī)擺放小球。
公式:坐標(biāo) = 旋轉(zhuǎn)角度 * 半徑 * 根據(jù)遠(yuǎn)近距離的不同計(jì)算得到的應(yīng)該占的半徑比例
圓上任一點(diǎn)(x1,y1)坐標(biāo)的計(jì)算公式:
x1 = x0 + r * cos(ao * 3.14 /180 )
y1 = y0 + r * sin(ao * 3.14 /180 )

具體實(shí)現(xiàn)代碼如下:
/**
* @describe: 支付寶螞蟻森林水滴能量
*/
public class WaterFlake extends FrameLayout {
private OnWaterItemListener mOnWaterItemListener;
/**
* 小樹(shù)坐標(biāo)X
*/
private float treeCenterX = 0;
/**
* 小樹(shù)坐標(biāo)Y
*/
private float treeCenterY = 0;
/**
* 小樹(shù)高度
*/
private int radius = 80;
/**
* 開(kāi)始角度
*/
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;
// 根據(jù)tem的個(gè)數(shù),計(jì)算角度
float angleDelay = -180 / childCount;
for (int i = 0; i < childCount; i++) {
WaterView child = (WaterView) getChildAt(i);
mStartAngle %= 180;
//設(shè)置CircleView小圓點(diǎn)的坐標(biāo)信息
//坐標(biāo) = 旋轉(zhuǎn)角度 * 半徑 * 根據(jù)遠(yuǎn)近距離的不同計(jì)算得到的應(yīng)該占的半徑比例
// 則圓上任一點(diǎn)為:(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;
}
}
/**
* 設(shè)置小球數(shù)據(jù),根據(jù)數(shù)據(jù)集合創(chuàng)建小球數(shù)量
*
* @param modelList 數(shù)據(jù)集合
*/
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);
}
}
/**
* 設(shè)置小球點(diǎn)擊事件
*
* @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;
}
}小球擺放隨機(jī)算法有多種實(shí)現(xiàn)方式,這只是其中一種,寫(xiě)的不好的地方,還望各位指正,歡迎大家一起交流學(xué)習(xí)。
目前代碼還有一些地方需要完善的,逐步更新。。。
TODO
優(yōu)化小球隨機(jī)生成算法,保證每個(gè)小球盡量不會(huì)重疊
到此這篇關(guān)于Android模擬實(shí)現(xiàn)支付寶螞蟻森林效果的文章就介紹到這了,更多相關(guān)Android螞蟻森林內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android開(kāi)發(fā)獲取當(dāng)前系統(tǒng)日期和時(shí)間功能示例
這篇文章主要介紹了Android開(kāi)發(fā)獲取當(dāng)前系統(tǒng)日期和時(shí)間功能,結(jié)合實(shí)例形式分析了Android布局、事件響應(yīng)、監(jiān)聽(tīng)以及時(shí)間獲取相關(guān)操作技巧,需要的朋友可以參考下2019-04-04
Android 中build.prop 文件與 getprop 命令
這篇文章主要介紹了Android 中build.prop 文件與 getprop 命令的相關(guān)資料,需要的朋友可以參考下2017-06-06
Android布局自定義Shap圓形ImageView可以單獨(dú)設(shè)置背景與圖片
這篇文章主要介紹了Android布局自定義Shap圓形ImageView可以單獨(dú)設(shè)置背景與圖片 的相關(guān)資料,需要的朋友可以參考下2016-01-01
Android自定義控件之圓形進(jìn)度條動(dòng)畫(huà)
這篇文章主要為大家詳細(xì)介紹了Android自定義控件之圓形進(jìn)度條動(dòng)畫(huà),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07
實(shí)例講解Android App使用自帶的SQLite數(shù)據(jù)庫(kù)的基本方法
這篇文章主要介紹了Android App使用自帶的SQLite數(shù)據(jù)庫(kù)的基本方法,SQLite是一個(gè)小巧的內(nèi)嵌型數(shù)據(jù)庫(kù),在數(shù)據(jù)庫(kù)需求不大的情況下使用SQLite其實(shí)非常有效,需要的朋友可以參考下2016-04-04
Android中使用AsyncTask實(shí)現(xiàn)文件下載以及進(jìn)度更新提示
AsyncTask,它使創(chuàng)建需要與用戶界面交互的長(zhǎng)時(shí)間運(yùn)行的任務(wù)變得更簡(jiǎn)單,本篇文章主要介紹了Android中使用AsyncTask實(shí)現(xiàn)文件下載以及進(jìn)度更新提示,有興趣的可以了解一下。2016-12-12
android開(kāi)發(fā)環(huán)境遇到adt無(wú)法啟動(dòng)的問(wèn)題分析及解決方法
開(kāi)始研究android開(kāi)發(fā),搭建開(kāi)發(fā)環(huán)境的時(shí)候就出了問(wèn)題,真是束手無(wú)策2013-02-02

