自定義View:實現炫酷的點贊特效(仿即刻)

1.寫在文前

按照慣例,反手就是一個超連接: github地址git

2.目標

本文要實現的View效果以下圖:程序員

效果圖.gif

3.分析

從效果圖容易看出,圖中的功能主要分爲兩個部分:github

  • 左側大拇指動畫
  • 右側的文字動畫

3.1 左側(PraiseView)

不難發現左側動畫效果主要由三部分組成:canvas

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

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

  • 對整個View使用scale動畫
  • 對View中的VectorDrawable使用scale動畫 細心的客觀已經發現了當四條線段存在的時候,點擊以後,線段也是會隨之縮放的。沒錯,豆豆正是對整個View進行了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);
                ...
                // 省略無關代碼
                break;
        }
        return super.onTouchEvent(event);
    }
複製代碼

####    3.1.1 圓圈擴散 沒錯,就是畫圈圈。一樣,仔細的同志應該已經發現了些什麼,冥冥之中彷佛有些什麼不可告人的祕密。 是的,這裏有兩個須要注意的地方:bash

  • 初始圓圈的半徑,和中心位置,也就是圈圈該畫在哪裏(從圖中不難看出,圓圈是包裹着拇指的)
  • measure出View的大小,確認drawable的bound(不自行measure肯定view的大小的話,默認的大小是隻會包裹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();
    }
複製代碼

由此計算出了view和drawable的大小,從而能夠去畫他了。這樣咱們就確認了圈圈該畫在哪裏,接下來的擴散效果,只須要控制圈圈的半徑便可,依舊看代碼:ide

@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 線段效果 線段怎麼去畫呢?中小學老師告訴咱們,兩點確認一條線段。問題隨之轉換:佈局

  • 那麼咱們如何確認這兩點的位置呢?
  • 爲了可持續發展,咱們該怎麼樣去肯定兩條線段直接的距離呢? 各位客官,不妨喝杯茶,吃點瓜子,思考下上面這個問題。 ... ... // 優雅的喝茶timing ... 細心的朋友已經注意到我以前的onMeasure方法中有一個initPointFs(1.3);沒錯,就是在獲取View的大小後,進行了對點的計算,看代碼:
/**
     * 用於計算 線條的長度
     * @param scale 外圓半徑爲內圓半徑的scale倍數
     */
    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;
            // 點p1爲mDrawable外接圓上的點
            PointF p1 = new PointF(
                    mCircleCenter.x + (float) (radius * Math.cos(Math.toRadians(result))),
                    mCircleCenter.y + (float) (radius * Math.sin(Math.toRadians(result)))
            );

            // 點p1爲mDrawable外接圓scale倍上的點
            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);
        }
    }
複製代碼

經過代碼註解不難發現,這裏咱們巧妙的利用同心圓和角度的方式來肯定了4條線段,8個點集合的值(豆豆不由感嘆,數學對程序員的重要性)。這樣作的好處就是足夠靈活,不管View大小如何變,線段的間隔和長短都是適宜的。 至此左側的拇指動畫效果,算是告一段落了。post

3.2 右側(RecordView)

右邊的數字翻牌效果,乍看起來很簡單,無非就是drawText()累加以後從新drawText();原理上是這樣的沒錯,不過值得注意的是:動畫

  • 無需變化的數位上的值不會被翻動
  • 上下翻動時前一個數字會漸漸隱掉 先看,測量過程: 從圖中咱們不難發現,測量的高度值應當爲Text的3倍,用於顯示前一個,當前,和下一個的數字值 寬度能夠直接從api中獲取當前的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的內容了,而內容很簡單,就是一系列的String值。到這裏都比較容易實現,而難點則是,肯定上一個和下一個值,以及他們的位置。 細心的朋友可能已經發如今measure的時候,咱們有一個mTextHeigh記錄了文字的高度,pointY記錄了兩倍文字的高度,沒錯這裏就是利用mTextHeight來控制三個可能要畫出來的string值的位置的。

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

3.2.1 加1動畫

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

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

        // 數字位數進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();
    }
複製代碼

代碼比較簡單,無非是作了移動和透明度的動畫效果,這裏便解決了「上下翻動時前一個數字會漸漸隱掉」的需求,須要注意的點是,數字位進1時的利用空格佔位的處理,不作該處理,當數字進位後,動畫效果會差強人意,有興趣的朋友能夠去試試看。 結合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是累加的,所以有個往下滑動效果
            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);
                }
            }
        }
    }
複製代碼

這裏即是核心所在了:如何無需變化的數位上的值不會被翻動? onDraw方法中給出了咱們答案,思路很簡單:

  • 將接下來要顯示的數字和當前的正在顯示的數字的每一位數一一對好比果不一樣,則經過動畫效果重畫,相同,則不走動畫效果,直接畫出來便可。

至此gif圖中的兩部分效果都已經實現

3.3 總體(PraiseRecordView)

以上是分開獨立的兩個view,爲了更方便的使用這個效果,咱們須要將兩個view的功能整合在一塊兒,起到一個聯動效果,也就須要引入一個ViewGroup去肯定這兩個view(PraiseView和RecordView)的佈局,這部分主要涉及到layout,以及viewgroup測繪的時候,使用的是match_parent寬高時,如何控制子view的顯示,有興趣的朋友能夠直接去看代碼,這裏暫不作贅述了。

4 總結

行文至此,我不由點了根黃鶴樓,望着那嫋嫋的煙,一擡手摸着了天... 天邊飄來一個: github地址

5 福利

附贈優惠禮包自取: 阿里雲飛機票

相關文章
相關標籤/搜索