自定義View:實(shí)現(xiàn)炫酷的點(diǎn)贊特效(仿即刻)

1.寫在文前

按照慣例,反手就是一個(gè)超鏈接:
github地址

2.目標(biāo)

本文要實(shí)現(xiàn)的View效果如下圖:

效果圖.gif

3.分析

從效果圖容易看出,圖中的功能主要分為兩個(gè)部分:

  • 左側(cè)大拇指動(dòng)畫
  • 右側(cè)的文字動(dòng)畫

3.1 左側(cè)(PraiseView)

不難發(fā)現(xiàn)左側(cè)動(dòng)畫效果主要由三部分組成:

  1. MotionEvent_DOWN時(shí)的拇指縮小,UP時(shí)的放大效果
  2. MotionEvent_UP時(shí)的圓圈擴(kuò)散效果(水波紋效果)
  3. MotionEvent_UP時(shí)的上面的四條線段效果

拇指的縮放各位客觀想必也是心中有數(shù)的,無非就是兩種方式:

  • 對(duì)整個(gè)View使用scale動(dòng)畫
  • 對(duì)View中的VectorDrawable使用scale動(dòng)畫
    細(xì)心的客觀已經(jīng)發(fā)現(xiàn)了當(dāng)四條線段存在的時(shí)候,點(diǎn)擊之后,線段也是會(huì)隨之縮放的。沒錯(cuò),豆豆正是對(duì)整個(gè)View進(jìn)行了scale處理。
    代碼如下:
  // 處理拇指縮放效果
 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                move = event.getY();
                animate().scaleY(0.8f).scaleX(0.8f).start();
                break;
            case MotionEvent.ACTION_UP:
                getHandler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        animate().cancel();
                        setScaleX(1);
                        setScaleY(1);
                    }
                }, 300);
                ...
                // 省略無關(guān)代碼
                break;
        }
        return super.onTouchEvent(event);
    }

3.1.1 圓圈擴(kuò)散

沒錯(cuò),就是畫圈圈。同樣,仔細(xì)的同志應(yīng)該已經(jīng)發(fā)現(xiàn)了些什么,冥冥之中似乎有些什么不可告人的秘密。
是的,這里有兩個(gè)需要注意的地方:

  • 初始圓圈的半徑,和中心位置,也就是圈圈該畫在哪里(從圖中不難看出,圓圈是包裹著拇指的)
  • measure出View的大小,確認(rèn)drawable的bound(不自行measure確定view的大小的話,默認(rèn)的大小是只會(huì)包裹drawable哦~)
    廢話不多說,先看代碼:
// 測量View寬高
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        switch (widthSpecMode) {
            ...
            case MeasureSpec.AT_MOST:
                widthMeasureSpec = mDrawable.getIntrinsicWidth();
                break;
            ...
        }

        switch (heightSpecMode) {
            ...
            // wrap_content
            case MeasureSpec.AT_MOST:
                heightMeasureSpec = mDrawable.getIntrinsicHeight();
                break;
            ...
        }

        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
        initDrawable(mDrawable, widthMeasureSpec, heightMeasureSpec);
        initPointFs(1.3f);
    }

    // drawable的大小為view的0.6
    private void initDrawable(Drawable drawable, int width, int height) {
        mCircleCenter.x = width / 2f;
        mCircleCenter.y = height / 2;
        mDrawable = drawable;

        // drawable的邊長為view的0.6
        float diameter = (float) ((width > height ? height : width) * 0.6);

        int left = (int) ((width - diameter)/2);
        int top = (int)(height - diameter)/2;
        int right = (int) (left + diameter);
        int bottom = (int) (top + diameter);
        Rect drawableRect = new Rect(left, top, right, bottom);
        mDrawable.setBounds(drawableRect);
        requestLayout();
    }

由此計(jì)算出了view和drawable的大小,從而可以去畫他了。這樣我們就確認(rèn)了圈圈該畫在哪里,接下來的擴(kuò)散效果,只需要控制圈圈的半徑即可,依舊看代碼:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mDrawable.draw(canvas);

        drawEffect(canvas);
    }

    private void drawEffect(Canvas canvas) {
        // 畫圓
        if (mRadius > 0)
            canvas.drawCircle(mCircleCenter.x, mCircleCenter.y, mRadius, mPaint);
        if (drawLines == 1) {
            // 劃線
            ...
    }

    public void animation() {
        final float radius = getInitRadius(mDrawable);
        ObjectAnimator animator = ObjectAnimator.ofFloat(this, "radius", radius, radius * 1.5f, radius * 3.0f);
        animator.setInterpolator(new AnticipateInterpolator());
        animator.setDuration(500);
        // 畫線
        // ...
        set.start();
    }

至此我們完成了拇指的縮放和波紋效果,心里美滋滋有木有

3.1.2 線段效果

線段怎么去畫呢?中小學(xué)老師告訴我們,兩點(diǎn)確認(rèn)一條線段。問題隨之轉(zhuǎn)換:

  • 那么我們?nèi)绾未_認(rèn)這兩點(diǎn)的位置呢?
  • 為了可持續(xù)發(fā)展,我們?cè)撛趺礃尤ゴ_定兩條線段直接的距離呢?
    各位客官,不妨喝杯茶,吃點(diǎn)瓜子,思考下上面這個(gè)問題。
    ...
    ... // 優(yōu)雅的喝茶timing
    ...
    細(xì)心的朋友已經(jīng)注意到我之前的onMeasure方法中有一個(gè)initPointFs(1.3);沒錯(cuò),就是在獲取View的大小后,進(jìn)行了對(duì)點(diǎn)的計(jì)算,看代碼:
    /**
     * 用于計(jì)算 線條的長度
     * @param scale 外圓半徑為內(nèi)圓半徑的scale倍數(shù)
     */
    private void initPointFs(float scale) {
        mPointList.clear();
        float radius = getInitRadius(mDrawable);
        int base = -60;
        int factor = -20;
        for (int i = 0; i < 4; i++) {
            int result = base + factor * i;
            // 點(diǎn)p1為mDrawable外接圓上的點(diǎn)
            PointF p1 = new PointF(
                    mCircleCenter.x + (float) (radius * Math.cos(Math.toRadians(result))),
                    mCircleCenter.y + (float) (radius * Math.sin(Math.toRadians(result)))
            );

            // 點(diǎn)p1為mDrawable外接圓scale倍上的點(diǎn)
            PointF p2 = new PointF(
                    mCircleCenter.x + (float) (scale * radius * Math.cos(Math.toRadians(result))),
                    mCircleCenter.y + (float) (scale * radius * Math.sin(Math.toRadians(result)))
            );

            mPointList.add(p1);
            mPointList.add(p2);
        }
    }

通過代碼注解不難發(fā)現(xiàn),這里我們巧妙的利用同心圓和角度的方式來確定了4條線段,8個(gè)點(diǎn)集合的值(豆豆不禁感嘆,數(shù)學(xué)對(duì)程序員的重要性)。這樣做的好處就是足夠靈活,無論View大小如何變,線段的間隔和長短都是適宜的。
至此左側(cè)的拇指動(dòng)畫效果,算是告一段落了。

3.2 右側(cè)(RecordView)

右邊的數(shù)字翻牌效果,乍看起來很簡單,無非就是drawText()累加之后重新drawText();原理上是這樣的沒錯(cuò),不過值得注意的是:

  • 無需變化的數(shù)位上的值不會(huì)被翻動(dòng)
  • 上下翻動(dòng)時(shí)前一個(gè)數(shù)字會(huì)漸漸隱掉
    先看,測量過程:
    從圖中我們不難發(fā)現(xiàn),測量的高度值應(yīng)當(dāng)為Text的3倍,用于顯示前一個(gè),當(dāng)前,和下一個(gè)的數(shù)字值
    寬度可以直接從api中獲取當(dāng)前的text的寬即可,看代碼:
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ···
        switch (widthSpecMode) {
            ···
            case MeasureSpec.AT_MOST:
                int width = (int) mPaint.measureText("0", 0, 1) * mCurrentString.length();
                widthMeasureSpec = width;
                break;
            ···
        }

        switch (heightSpecMode) {
            ···
            case MeasureSpec.AT_MOST:
                mTextHeight = mPaint.getFontSpacing();
                heightMeasureSpec = (int) (mTextHeight * 3);
                break;
            case MeasureSpec.EXACTLY:
                mPaint.setTextSize(heightSpecSize / 4);
                mTextHeight = (int) mPaint.getFontSpacing();
                heightMeasureSpec = heightSpecSize;
                break;
        }
        pointY = 2 * mTextHeight;
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }

在測量出View的寬高之后,便要著手去畫view的內(nèi)容了,而內(nèi)容很簡單,就是一系列的String值。到這里都比較容易實(shí)現(xiàn),而難點(diǎn)則是,確定上一個(gè)和下一個(gè)值,以及他們的位置。
細(xì)心的朋友可能已經(jīng)發(fā)現(xiàn)在measure的時(shí)候,我們有一個(gè)mTextHeigh記錄了文字的高度,pointY記錄了兩倍文字的高度,沒錯(cuò)這里就是利用mTextHeight來控制三個(gè)可能要畫出來的string值的位置的。

這里有必要提一下的是,drawText(@NonNull String text, float x, float y, @NonNull Paint paint)這個(gè)方法中的float y對(duì)應(yīng)的是baseLine的y值,簡單的理解的話就是一串String的bottom的位置,畫出來的內(nèi)容是在bottom之上的。這也是為什么我們要用pointY = 2 * mTextHeight的理由。至此不難想到,我們的lastNum, currentNum, NextNum畫的位置,分別對(duì)應(yīng)mTextHeight, 2 * mTextHeight和3 * mTextHeight。至此三個(gè)值的位置便算是確定好了。

3.2.1 加1動(dòng)畫

先看加1的處理,上代碼:

    public void addOne() {
        mCurrentString = String.valueOf(mCurrentNum);
        mCurrentNum++;
        mNextString = String.valueOf(mCurrentNum);
        mStatus = ADD;

        // 數(shù)字位數(shù)進(jìn)1
        if (mCurrentString.length() < mNextString.length()) {
            mCurrentString = " " + mCurrentString;
            requestLayout();
        }

        ObjectAnimator animator = ObjectAnimator.ofFloat(this, "pointY", 2 * mTextHeight, mTextHeight);
        ObjectAnimator alphaAnim = ObjectAnimator.ofInt(this, "paintAlpha", 255, 0);
        AnimatorSet set = new AnimatorSet();
        set.playTogether(alphaAnim, animator);
        set.start();
    }

代碼比較簡單,無非是做了移動(dòng)和透明度的動(dòng)畫效果,這里便解決了“上下翻動(dòng)時(shí)前一個(gè)數(shù)字會(huì)漸漸隱掉”的需求,需要注意的點(diǎn)是,數(shù)字位進(jìn)1時(shí)的利用空格占位的處理,不做該處理,當(dāng)數(shù)字進(jìn)位后,動(dòng)畫效果會(huì)差強(qiáng)人意,有興趣的朋友可以去試試看。
結(jié)合onDraw方法再來看看:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mStatus == NONE) {
            canvas.drawText(mCurrentString, 0, pointY, mPaint);
        } else if (mStatus == ADD) {
            for (int i = mNextString.length() - 1; i >= 0; i--) {
                String next = String.valueOf(mNextString.charAt(i));
                String current = String.valueOf(mCurrentString.charAt(i));

                // i位置需要改變
                if (!next.equals(current)) {
                    mPaint.setAlpha(mPaintAlpha);
                    canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, pointY, mPaint);

                    // mPaintAlpha : 255  -  0 遞減
                    mPaint.setAlpha(255 - mPaintAlpha);
                    canvas.drawText(next, mPaint.measureText("0", 0, 1) * i, mTextHeight + pointY, mPaint);
                    // i位置不需要改變
                } else {
                    mPaint.setAlpha(255);
                    canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, mTextHeight * 2, mPaint);
                }
            }
        } else if (mStatus == REDUCE) {
            // pointY是累加的,因此有個(gè)往下滑動(dòng)效果
            for (int i = mCurrentString.length() - 1; i >= 0; i--) {
                String last = String.valueOf(mLastString.charAt(i));
                String current = String.valueOf(mCurrentString.charAt(i));

                // i位置需要改變
                if (!last.equals(current)) {
                    mPaint.setAlpha(mPaintAlpha);
                    canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, mTextHeight + pointY, mPaint);

                    // mPaintAlpha : 255  -  0 遞減
                    mPaint.setAlpha(255 - mPaintAlpha);
                    canvas.drawText(last, mPaint.measureText("0", 0, 1) * i, pointY, mPaint);
                    // i位置不需要改變
                } else {
                    mPaint.setAlpha(255);
                    canvas.drawText(current, mPaint.measureText("0", 0, 1) * i, mTextHeight * 2, mPaint);
                }
            }
        }
    }

這里便是核心所在了:如何無需變化的數(shù)位上的值不會(huì)被翻動(dòng)?
onDraw方法中給出了我們答案,思路很簡單:

  • 將接下來要顯示的數(shù)字和當(dāng)前的正在顯示的數(shù)字的每一位數(shù)一一對(duì)比如果不同,則通過動(dòng)畫效果重畫,相同,則不走動(dòng)畫效果,直接畫出來即可。

至此gif圖中的兩部分效果都已經(jīng)實(shí)現(xiàn)

3.3 整體(PraiseRecordView)

以上是分開獨(dú)立的兩個(gè)view,為了更方便的使用這個(gè)效果,我們需要將兩個(gè)view的功能整合在一起,起到一個(gè)聯(lián)動(dòng)效果,也就需要引入一個(gè)ViewGroup去確定這兩個(gè)view(PraiseView和RecordView)的布局,這部分主要涉及到layout,以及viewgroup測繪的時(shí)候,使用的是match_parent寬高時(shí),如何控制子view的顯示,有興趣的朋友可以直接去看代碼,這里暫不做贅述了。同時(shí)

4 總結(jié)

行文至此,我不禁點(diǎn)了根黃鶴樓,望著那裊裊的煙,一抬手摸著了天...
天邊飄來一個(gè):
github地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,053評(píng)論 4 61
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,769評(píng)論 25 709
  • 今天的晨讀《連接,如何應(yīng)對(duì)親密關(guān)系中的焦慮》中,關(guān)于親密關(guān)系,我延伸的理解為不只局限于是夫妻、戀人之間的...
    靜亦境閱讀 282評(píng)論 4 4
  • 文/彬睿 今天,早早的吃完早餐,我們?nèi)規(guī)厦妹镁团d高采烈的出門了,因?yàn)槲乙桶职謰寢屢黄鹑Q戰(zhàn)“峨眉山之巔”。坐...
    茉莉初綻閱讀 293評(píng)論 0 2
  • 金錢這東西,就好像是個(gè)魔咒,有錢的人愛,沒錢的人更愛,窮人愛,富人還愛,大人愛,小孩也愛,男人女人老人都愛。不愛不...
    張淑萍閱讀 384評(píng)論 0 0

友情鏈接更多精彩內(nèi)容