老司機(jī)帶你順手?jǐn)]一個支付寶螞蟻森林水滴能量效果

老司機(jī)吴旋,不存在的损肛,其實(shí)我還算不上厢破,哈哈。治拿。摩泪。


最近公司產(chǎn)品突然有一個類似支付寶螞蟻森林的功能,大致功能跟支付寶螞蟻森林相像劫谅,在看了一下支付寶螞蟻森林的效果之后见坑,打算先擼一個控件出來,等公司效果圖出來之后就可以放上去直接使用捏检。
首先我們先大致看下支付寶的螞蟻森林效果圖:


支付寶螞蟻森林

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


自定實(shí)現(xiàn)效果

當(dāng)我們拿到這個需求時先分析一波鳄梅,不要忙著就動手開干,不然容易平地翻車未檩。戴尸。
需要實(shí)現(xiàn)的功能有:

1、自定小圓球冤狡,圓球內(nèi)文字孙蒙、上下浮動、消失動畫悲雳;
2挎峦、根據(jù)數(shù)據(jù)動態(tài)添加小球,并且位置隨機(jī)分布在小樹周圍合瓢,不能重疊坦胶。這點(diǎn)是最重要的,涉及到一個隨機(jī)位置生成算法的設(shè)計晴楔。

好了顿苇,當(dāng)我們確定了我們要實(shí)現(xiàn)的功能之后就可以逐步開始擼代碼了。

自定義圓球

這個比較容易實(shí)現(xiàn)税弃,繪制一個圓纪岁,再在園內(nèi)繪制文字,動畫實(shí)現(xiàn)統(tǒng)一采用的是屬性動畫來實(shí)現(xiàn)则果,代碼如下幔翰,注釋寫的比較詳細(xì)就不一一解釋了,懶...

/**
 * @author: xiaohaibin.
 * @time: 2018/1/5
 * @mail:xhb_199409@163.com
 * @github:https://github.com/xiaohaibin
 * @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)近距離的不同計算得到的應(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;
    }
}

動態(tài)隨機(jī)添加小球

這里我采用的是集成FrameLayout 通過設(shè)置小球數(shù)據(jù)西壮,動態(tài)將小球add進(jìn)去遗增,比較簡便,在這里最重要是動態(tài)隨機(jī)添加小球的算法款青,解決了這個算法就好辦了做修。通過仔細(xì)觀察支付寶螞蟻森林的效果實(shí)現(xiàn),我們可以發(fā)現(xiàn)一般小球都是在樹的正上方隨機(jī)分布的。所以我想以小樹的根為中心缓待,小樹的高度為半徑為一個扇形,在這個扇形上方隨機(jī)擺放小球渠牲。

公式:坐標(biāo) = 旋轉(zhuǎn)角度 * 半徑 * 根據(jù)遠(yuǎn)近距離的不同計算得到的應(yīng)該占的半徑比例

圓上任一點(diǎn)(x1,y1)坐標(biāo)的計算公式:

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

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

具體實(shí)現(xiàn)代碼如下:

/**
 * @author: xiaohaibin.
 * @time: 2018/1/5
 * @mail:xhb_199409@163.com
 * @github:https://github.com/xiaohaibin
 * @describe: 支付寶螞蟻森林水滴能量
 */

public class WaterFlake extends FrameLayout {

    private OnWaterItemListener mOnWaterItemListener;
    /**
     * 小樹坐標(biāo)X
     */
    private float treeCenterX = 0;
    /**
     * 小樹坐標(biāo)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;
        // 根據(jù)tem的個數(shù)旋炒,計算角度
        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)近距離的不同計算得到的應(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)方式签杈,這只是其中一種瘫镇,寫的不好的地方,還望各位指正答姥,歡迎大家一起交流學(xué)習(xí)铣除。

目前代碼還有一些地方需要完善的,逐步更新鹦付。尚粘。。

TODO

優(yōu)化小球隨機(jī)生成算法敲长,保證每個小球盡量不會重疊

最后附上源碼鏈接:https://github.com/xiaohaibin/CustomWaterView 喜歡的話郎嫁,麻煩給個Star支持一下,謝謝~~~

如果覺得文章幫到你祈噪,不求打賞泽铛,喜歡我的文章可以關(guān)注我和朋友一起運(yùn)營的微信公眾號,將會定期推送優(yōu)質(zhì)技術(shù)文章辑鲤,求關(guān)注~~~

歡迎關(guān)注“大話安卓”微信公眾號

歡迎加入“大話安卓”技術(shù)交流群盔腔,一起分享,共同進(jìn)步

歡迎加入“大話安卓”技術(shù)交流群月褥,互相學(xué)習(xí)提升
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弛随,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子宁赤,更是在濱河造成了極大的恐慌撵幽,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件礁击,死亡現(xiàn)場離奇詭異盐杂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)哆窿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門链烈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人挚躯,你說我怎么就攤上這事强衡。” “怎么了码荔?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵漩勤,是天一觀的道長感挥。 經(jīng)常有香客問我,道長越败,這世上最難降的妖魔是什么触幼? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮究飞,結(jié)果婚禮上置谦,老公的妹妹穿的比我還像新娘。我一直安慰自己亿傅,他們只是感情好媒峡,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著葵擎,像睡著了一般谅阿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酬滤,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天奔穿,我揣著相機(jī)與錄音,去河邊找鬼敏晤。 笑死贱田,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的嘴脾。 我是一名探鬼主播男摧,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼译打!你這毒婦竟也來了耗拓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤奏司,失蹤者是張志新(化名)和其女友劉穎乔询,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體韵洋,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡竿刁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了搪缨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片食拜。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖副编,靈堂內(nèi)的尸體忽然破棺而出负甸,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布呻待,位于F島的核電站打月,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蚕捉。R本人自食惡果不足惜奏篙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鱼冀。 院中可真熱鬧报破,春花似錦悠就、人聲如沸千绪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽荸型。三九已至,卻和暖如春炸茧,著一層夾襖步出監(jiān)牢的瞬間瑞妇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工梭冠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辕狰,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓控漠,卻偏偏與公主長得像蔓倍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子盐捷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345