貝塞爾曲線(Bezier)之 QQ 消息拖拽動畫效果

博主聲明:

轉載請在開頭附加本文鏈接及作者信息,并標記為轉載。本文由博主 威威喵 原創,請多支持與指教。

本文首發于此 博主威威喵 | 博客主頁https://blog.csdn.net/smile_running

這幾天突然發現 QQ 的消息拖拽動畫效果還挺不錯的,以前都沒去留意它,這幾看了一點關于貝塞爾曲線的知識,這不剛好沙場練兵。于是從昨天開始呢,我就已經開始補點高數的知識了。雖然我現在已經準大四了,眨眼間就快畢業了,高數的知識還是從大一開始學習的,現在基本忘了差不多了。

扯了一點關于我的學習經歷,回到本篇問題的關鍵,QQ 消息拖拽效果是怎樣的呢?于是,我在模擬器裝了一個 QQ 應用,特地找了一下小號,記得這個號好像是我初中申請的賬號,以前那會兒 cf、飛車、dnf 特別流行,搞了幾個小號搬磚,哈哈。我們來看看消息拖拽的效果吧:

貝塞爾曲線(Bezier)之 QQ 消息拖拽動畫效果

這樣的效果做起來并不簡單,尤其是曲線的計算方面,如果你也像我一樣忘了高數的知識點的話,建議你去翻翻三角函數那部分的知識, 本文不會教你這些基本公式,也不會教你自定義 view 的基本流程,本篇目的:計算和實現拖拽的粘性效果。如果這些基本知識不具備的話,推薦你去看下我的自定義 view 相關文章。

有了上一篇(點擊這里:貝塞爾曲線(Bezier)之愛心點贊曲線動畫效果)對貝塞爾曲線的基本了解和寫了一個小案例的鋪墊,在這次寫這個 QQ 消息拖拽效果的時候,顯然輕松了許多。好了,廢話就說這么多,下面進入重點內容。

首先,看上面的效果顯示情況,可以看成兩個小圓,一個比較大一點,可以拖拽出去,另一個小一點,但會隨著兩個圓的距離改變大小。我們的步驟:在 onDraw 里面繪制兩個圓,用手指可以拖動一個大圓,并且小圓的大小會隨著兩圓的距離更改。這部分代碼非常簡單,我就不做多的介紹了,如果你對下面代碼有不解之處,還請自己補充知識。直接貼代碼:

package nd.no.xww.qqmessagedragview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
 * @author xww
 * @desciption : 仿 QQ 消息拖拽消失的效果(大圓:不會消失,且大小一致。小圓:與大圓的距離協調改變大小)
 * @date 2019/8/2
 * @time 8:54
 */
public class QQMessageDragView extends View {

    private Paint mPaint;

    //大圓
    private float mBigCircleX;
    private float mBigCircleY;
    private final int BIG_CIRCLE_RADUIS = 50;
    //小圓
    private float mSmallCircleX;
    private float mSmallCircleY;
    private int mSmallDefRaduis = 40;
    private int mSmallHideRaduis = 15;
    private int mSmallCircleRaduis = mSmallDefRaduis;

    private Bitmap mMessageBitmap;

    private void init() {
        mPaint = new Paint();
        mPaint.setDither(true);
        mPaint.setAntiAlias(true);
        mPaint.setColor(getResources().getColor(android.R.color.holo_red_dark));

        mMessageBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.message);
        mMessageBitmap = Bitmap.createScaledBitmap(mMessageBitmap, 150, 150, false);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY ? MeasureSpec.getSize(widthMeasureSpec) : 200
                , MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY ? MeasureSpec.getSize(heightMeasureSpec) : 200);
    }

    public QQMessageDragView(Context context) {
        this(context, null);
    }

    public QQMessageDragView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public QQMessageDragView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mSmallCircleRaduis > mSmallHideRaduis) {
            canvas.drawCircle(mSmallCircleX, mSmallCircleY, mSmallCircleRaduis, mPaint);
        }
        canvas.drawCircle(mBigCircleX, mBigCircleY, BIG_CIRCLE_RADUIS, mPaint);
//        canvas.drawBitmap(mMessageBitmap, mBigCircleX, mBigCircleY, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float downX = event.getX();
        float downY = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mSmallCircleRaduis = mSmallDefRaduis;
                mSmallCircleX = mBigCircleX = downX;
                mSmallCircleY = mBigCircleY = downY;
                break;
            case MotionEvent.ACTION_MOVE:
                mBigCircleX = event.getX();
                mBigCircleY = event.getY();
                int disCircle = calculateDisCircle(mSmallCircleX, mSmallCircleY, mBigCircleX, mBigCircleY);
                mSmallCircleRaduis = mSmallDefRaduis - disCircle / mSmallHideRaduis;
                break;
            case MotionEvent.ACTION_UP:
                mSmallCircleRaduis = 0;
                break;
        }
        invalidate();
        return true;
    }

    // 兩點之間的距離公式 √(x2-x1)2+(y2-y1)2
    private int calculateDisCircle(float mSmallCircleX, float mSmallCircleY, float mBigCircleX, float mBigCircleY) {
        return (int) Math.sqrt(Math.pow((mSmallCircleX - mBigCircleX), 2) + Math.pow((mSmallCircleY - mBigCircleY), 2));
    }

}

運行上面的代碼,你就會看到和我一樣的效果:

image

好了,上面的代碼只是做了一個鋪墊,也是必須實現的第一步。接下來重頭戲開始,我們講講一些數學相關知識吧,本人高數也不怎么樣,大學除了基本必修高數,也沒去深入學習,不過這也不影響我們下面的操作。

首先,扔出一張草圖,畫的就這樣,將就看吧:

image

這上面應該不難看懂吧,兩個紅色圓就相當于我們拖拽的圓一樣,從上面的草圖中,我們目前已知的有 c1 c2 r1 r2 這四個屬性值,c1 c2 代表圓心坐標,r1 r2 是半徑。

當用手指去拖拽大圓的時候,它們之間的聯系就用那兩根藍色的曲線來表示,兩曲線對應的在兩圓上的坐標點就是 p1 p2 p3 p4 四個點,這四個點會伴隨這兩圓的距離發生改變,你可以想象一下效果。

那么,從上圖中,我們就要去計算 p1 p2 p3 p4 這四個點的坐標,然后將四點封閉起來繪制成路徑即可。可是,說的比較輕巧,從目前我們已知的條件當中,能用得上的就 c1 c2 r1 r2 四個了,如何去求呢?看接下來的這張圖:

image

從這張圖的計算過程中,我們可以求得綠色三角形的角 a 的相關方程式。因為我們已知 c1 圓心的坐標值,就可以得出 p1 點的坐標值,如上圖 p1x p1y 的值。

這樣的話,我們可以利用三角函數公式得出 b 邊和 c 邊的值,如上圖,最終得到的一個方程式中,僅存在一個角 a 是我們未知的,接下來我們就要去計算角 a 的值,看下圖:

image

來到第一張圖,看上面的黃色輔助線,假設它形成的是直角。我們就可以得到這兩條輔助線的邊長 dy 和 dx 。又根據三角形的補角兩平行線之間的夾角相等的定理,我們得出圖中的三個角 a 都是一樣的大小。

這樣我們可以得到一個等式:tanA = dy / dx ,最終,角 a = arctan( tanA )

這時候我們就取到了 a 相關的等式了,而 dx dy 都是可以計算出來的,所以一連串下來,相關的等式都成立了,從而就可以計算出一個點 p1,獲得 p1 點后,p2 p3 p4 不就手到擒來嘛。

最后要想形成貝塞爾曲線的效果,除了 p1 p2 p3 p4 以外,我們還需要一個控制點,如圖上的點 M,它是形成曲線的控制點,也是至關重要的一個點,它的坐標就是 M點 ( (c1x+c2x) / 2 , (c1y+c2y) / 2 )

那么本篇數學相關的計算部分就已經結束了,你還以為程序員不需要數學知識嘛,哈哈。下面就是該怎么寫程序了,把數學公式化為程序代碼,這就得看你的編程水平啦。

我寫了好一會兒,都是那個坐標值正負的問題卡了我挺久的,不過最終還是把代碼給搞出來了,四個點的計算方法如下:

    private float p1X;
    private float p1Y;
    private float p2X;
    private float p2Y;
    private float p3X;
    private float p3Y;
    private float p4X;
    private float p4Y;
    //控制點
    private float controlX;
    private float controlY;

    private float dx, dy;
    private double angleA;
    private double tanA;
    private Path bezierPath;
    private Path mBezierPath;

    /**
     * 貝塞爾 p1 p2 p3 p4 四個點坐標的計算
     *
     * @return
     */
    private Path drawDragBezier() {
        if (mSmallCircleRaduis < mSmallHideRaduis) {
            return null;
        }

        dx = mBigCircleX - mSmallCircleX;
        dy = mBigCircleY - mSmallCircleY;

        tanA = dy / dx;
        angleA = Math.atan(tanA);

        //控制點的計算
        controlX = (mSmallCircleX + mBigCircleX) / 2;
        controlY = (mSmallCircleY + mBigCircleY) / 2;

        p1X = (float) (mSmallCircleX + Math.sin(angleA) * mSmallCircleRaduis);
        p1Y = (float) (mSmallCircleY - Math.cos(angleA) * mSmallCircleRaduis);

        p2X = (float) (mBigCircleX + Math.sin(angleA) * BIG_CIRCLE_RADUIS);
        p2Y = (float) (mBigCircleY - Math.cos(angleA) * BIG_CIRCLE_RADUIS);

        p3X = (float) (mBigCircleX - Math.sin(angleA) * BIG_CIRCLE_RADUIS);
        p3Y = (float) (mBigCircleY + Math.cos(angleA) * BIG_CIRCLE_RADUIS);

        p4X = (float) (mSmallCircleX - Math.sin(angleA) * mSmallCircleRaduis);
        p4Y = (float) (mSmallCircleY + Math.cos(angleA) * mSmallCircleRaduis);

        //繪制路徑
        bezierPath = new Path();
        bezierPath.moveTo(p1X, p1Y);
        bezierPath.quadTo(controlX, controlY, p2X, p2Y);
        bezierPath.lineTo(p3X, p3Y);
        bezierPath.quadTo(controlX, controlY, p4X, p4Y);
        bezierPath.close();
        return bezierPath;
    }
image.gif

然后呢,使用就很簡單了。返回一個路徑,我們只要畫出來就好了,修改 onDraw 代碼如下:

    @Override
    protected void onDraw(Canvas canvas) {
        mBezierPath = drawDragBezier();
        if (mBezierPath != null) {
            canvas.drawCircle(mSmallCircleX, mSmallCircleY, mSmallCircleRaduis, mPaint);
            canvas.drawPath(mBezierPath, mPaint);
        }
        canvas.drawCircle(mBigCircleX, mBigCircleY, BIG_CIRCLE_RADUIS, mPaint);
    }

好了吧,點擊運行,你將會看到如下的效果:

image

最后做了一點點小優化,拖拽時沒有超出范圍可以回到原來的位置,若超出拖拽的極限方法,導致兩個圓失去關聯時,代表要摧毀那個大圓,手指松開那一剎那,要將它隱藏掉,效果如下:

image

那么至此,我們的QQ消息的粘性動畫已經實現了,代碼倒是不難,難的是通過數學公式來計算出 p1 p2 p3 p4 點的坐標值,這可能會卡住很多人,主要還是因為數學功底不足,還是抽時間補補數學,它可是個很有魅力的機靈鬼。

補充:(對上面的特效進行優化處理)

今天,8 月 8 日,早上 5 點半左右,臺灣不幸遭到了地震,連我在福建中北部地帶都能偶感晃動,我好像迷迷糊糊中感覺床在搖晃,是 6 點多級的地震,在此祝愿臺灣人民安好。而且,受臺風的影響,家里下了好大的雨,不過倒是清涼了許多。

好了,讓我們來優化一下這個效果吧,博主之前還沒有處理的一些細節問題,比如這個 QQ 消息拖動,如果我們沒有將它拖斷掉,也就是線還連著,上次的做法是將它的坐標賦值給初始按下的坐標,這導致的效果是一瞬間就回去了,動畫太過生硬,體驗不是特別好,接下來我們來優化一下,讓它慢慢的回去,有一個過渡時間。

上次的代碼是這樣做的,直接回到手指起始按下的那一個點位置:

            case MotionEvent.ACTION_UP:
                if (!isAttached) {
                    //被扯斷了
                    isShowed = false;
                } else if (mSmallCircleRaduis >= mSmallHideRaduis) {//小圓的半徑如果大于顯示的半徑,意味著沒有拖段線
                    isShowed = true;//大圓要顯示
                    //回到原來手指按下的位置
                    mBigCircleX = mSmallCircleX;
                    mBigCircleY = mSmallCircleY;
                }
                mSmallCircleRaduis = 0;//每次手松開,小圓半徑規 0
                break;

這個肯定不行,要對它的值進行修改,我們的思想是這個樣子的,看圖

image

我們需要慢慢的改變大圓的半徑,就相當于改變被我們拉出來的那個圓的 x 坐標和 y 坐標,我們給它定一個時間段,讓它們一起開始變化,這個就得使用到屬性動畫來處理了,我們把上部分的代碼做如下修改即可

            case MotionEvent.ACTION_UP:
                if (!isAttached) {
                    //被扯斷了
                    isShowed = false;
                } else if (mSmallCircleRaduis >= mSmallHideRaduis) {//小圓的半徑如果大于顯示的半徑,意味著沒有拖段線。松開手,彈回去
                    isShowed = true;//大圓要顯示

                    animatorSet = new AnimatorSet();
                    xAnimator = ObjectAnimator.ofFloat(mBigCircleX, mSmallCircleX);
                    xAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            mBigCircleX = (float) animation.getAnimatedValue();

                            int disCircle = calculateDisCircle(mSmallCircleX, mSmallCircleY, mBigCircleX, mBigCircleY);
                            mSmallCircleRaduis = mSmallDefRaduis - disCircle / mSmallHideRaduis;//在拖動過程中,小圓半徑一直在縮小
                        }
                    });

                    yAnimator = ObjectAnimator.ofFloat(mBigCircleY, mSmallCircleY);
                    yAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            mBigCircleY = (float) animation.getAnimatedValue();
                            invalidate();
                        }
                    });
                    animatorSet.playTogether(xAnimator, yAnimator);
                    animatorSet.setInterpolator(new OvershootInterpolator(3f));
                    animatorSet.setDuration(10000);
                    animatorSet.start();
                    animatorSet.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            //動畫結束時,隱藏小圓
                            mSmallCircleRaduis = 0;//每次手松開,小圓半徑規 0
                        }
                    });
                }
                break;

那么,繪制那個粘性的貝塞爾曲線也要一直繪制了,不能松開就沒了吧,所以要把 onDraw 的里面的代碼改為如下:

    @Override
    protected void onDraw(Canvas canvas) {
        mBezierPath = drawDragBezier();
        //兩個圓還有聯系
        if (mBezierPath != null) {
            canvas.drawPath(mBezierPath, mPaint);
        }
        if (isAttached) {
            canvas.drawCircle(mSmallCircleX, mSmallCircleY, mSmallCircleRaduis, mPaint);
        }
        //如果是顯示的
        if (isShowed) {
            canvas.drawCircle(mBigCircleX, mBigCircleY, BIG_CIRCLE_RADUIS, mPaint);
        }
    }

好了,一起來看看效果吧。為了使效果更加明顯,我特地把縮回來的動畫改為 10S,足夠你看清楚了吧

image

我給它加了一個插值器,回來的時候有一個反彈的效果!彈彈彈,彈走魚尾紋。。。

不過呢,還有一個地方需要優化的,就是拖斷掉的時候,再松開會有一個消失的效果,我就搞的簡單一點,讓它慢慢的消失就好了。不過也可以學那個爆炸效果,會比較炫酷一點,我找了一下那個爆炸的圖片,懶得圖改成透明顏色了,需要的自己去查一查幀動畫就好了。

下面是放快的效果

image

最后的完整代碼

package nd.no.xww.qqmessagedragview;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.OvershootInterpolator;

/**
 * @author xww
 * @desciption : 仿 QQ 消息拖拽消失的效果(大圓:不會消失,且大小一致。小圓:與大圓的距離協調改變大小)
 * @date 2019/8/2
 * @time 8:54
 * @博主:威威喵
 */
public class QQMessageDragView extends View {

    private Paint mPaint;

    //大圓
    private float mBigCircleX;
    private float mBigCircleY;
    private float mBigCircleRaduis = 50;
    //小圓
    private float mSmallCircleX;
    private float mSmallCircleY;
    private int mSmallDefRaduis = 40;
    private int mSmallHideRaduis = 15;//扯斷的距離
    private int mSmallCircleRaduis = mSmallDefRaduis;

    private Bitmap mMessageBitmap;

    private boolean isAttached;//代表兩個關聯
    private boolean isFirst = true;//顯示大圓

    private void init() {
        mPaint = new Paint();
        mPaint.setDither(true);
        mPaint.setAntiAlias(true);
        mPaint.setColor(getResources().getColor(android.R.color.holo_red_dark));
        mPaint.setTextSize(30f);
        mMessageBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.message);
        mMessageBitmap = Bitmap.createScaledBitmap(mMessageBitmap, 150, 150, false);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY ? MeasureSpec.getSize(widthMeasureSpec) : 200
                , MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY ? MeasureSpec.getSize(heightMeasureSpec) : 200);
    }

    public QQMessageDragView(Context context) {
        this(context, null);
    }

    public QQMessageDragView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public QQMessageDragView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mBezierPath = drawDragBezier();
        //兩個圓還有聯系
        if (mBezierPath != null) {
            canvas.drawPath(mBezierPath, mPaint);
        }
        if (isAttached) {
            canvas.drawCircle(mSmallCircleX, mSmallCircleY, mSmallCircleRaduis, mPaint);
        }
        //如果第一次,不繪制圓
        if (isFirst) {
            return;
        }
        canvas.drawCircle(mBigCircleX, mBigCircleY, mBigCircleRaduis, mPaint);

    }

    private float raduis;

    AnimatorSet animatorSet;
    ValueAnimator xAnimator;
    ValueAnimator yAnimator;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float downX = event.getX();
        float downY = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 兩個圓關聯了
                mBigCircleRaduis = 50; // 大圓的初始值
                isFirst = false;
                isAttached = true;

                mSmallCircleRaduis = mSmallDefRaduis;
                mSmallCircleX = mBigCircleX = downX;
                mSmallCircleY = mBigCircleY = downY;
                break;
            case MotionEvent.ACTION_MOVE:
                mBigCircleX = event.getX();
                mBigCircleY = event.getY();

                int disCircle = calculateDisCircle(mSmallCircleX, mSmallCircleY, mBigCircleX, mBigCircleY);
                mSmallCircleRaduis = mSmallDefRaduis - disCircle / mSmallHideRaduis;//在拖動過程中,小圓半徑一直在縮小

                if (mSmallCircleRaduis < mSmallHideRaduis) {//小圓的半徑如果太小了,不顯示了。
                    isAttached = false;//表示兩個圓沒有關聯了,意味這線被拖斷了
                }
                break;
            case MotionEvent.ACTION_UP:
                if (!isAttached) { // 被扯斷了,兩圓沒有聯系了
                    ValueAnimator raduisAnimator = ObjectAnimator.ofFloat(mBigCircleRaduis, 0);
                    raduisAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            mBigCircleRaduis = (float) animation.getAnimatedValue();
                            invalidate();
                        }
                    });
                    raduisAnimator.setDuration(500);
                    raduisAnimator.start();
                } else if (mSmallCircleRaduis >= mSmallHideRaduis) {//小圓的半徑如果大于顯示的半徑,意味著沒有拖段線。松開手,彈回去
                    animatorSet = new AnimatorSet();
                    xAnimator = ObjectAnimator.ofFloat(mBigCircleX, mSmallCircleX);
                    xAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            mBigCircleX = (float) animation.getAnimatedValue();

                            int disCircle = calculateDisCircle(mSmallCircleX, mSmallCircleY, mBigCircleX, mBigCircleY);
                            mSmallCircleRaduis = mSmallDefRaduis - disCircle / mSmallHideRaduis;//在拖動過程中,小圓半徑一直在縮小
                        }
                    });

                    yAnimator = ObjectAnimator.ofFloat(mBigCircleY, mSmallCircleY);
                    yAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            mBigCircleY = (float) animation.getAnimatedValue();
                            invalidate();
                        }
                    });
                    animatorSet.playTogether(xAnimator, yAnimator);
                    animatorSet.setInterpolator(new OvershootInterpolator(2.5f));
                    animatorSet.setDuration(500);
                    animatorSet.start();
                    animatorSet.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            //動畫結束時,隱藏小圓
                            mSmallCircleRaduis = 0;//每次手松開,小圓半徑規 0
                        }
                    });
                }
                break;
        }
        invalidate();
        return true;
    }

    // 兩點之間的距離公式 √(x2-x1)2+(y2-y1)2
    private int calculateDisCircle(float mSmallCircleX, float mSmallCircleY, float mBigCircleX, float mBigCircleY) {
        return (int) Math.sqrt(Math.pow((mSmallCircleX - mBigCircleX), 2) + Math.pow((mSmallCircleY - mBigCircleY), 2));
    }

    private float p1X;
    private float p1Y;
    private float p2X;
    private float p2Y;
    private float p3X;
    private float p3Y;
    private float p4X;
    private float p4Y;
    //控制點
    private float controlX;
    private float controlY;

    private float dx, dy;
    private double angleA;
    private double tanA;
    private Path bezierPath;
    private Path mBezierPath;

    /**
     * 貝塞爾 p1 p2 p3 p4 四個點坐標的計算
     *
     * @return
     */
    private Path drawDragBezier() {
        if (mSmallCircleRaduis < mSmallHideRaduis || !isAttached) {
            return null;
        }

        dx = mBigCircleX - mSmallCircleX;
        dy = mBigCircleY - mSmallCircleY;

        tanA = dy / dx;
        angleA = Math.atan(tanA);

        //控制點的計算
        controlX = (mSmallCircleX + mBigCircleX) / 2;
        controlY = (mSmallCircleY + mBigCircleY) / 2;

        p1X = (float) (mSmallCircleX + Math.sin(angleA) * mSmallCircleRaduis);
        p1Y = (float) (mSmallCircleY - Math.cos(angleA) * mSmallCircleRaduis);

        p2X = (float) (mBigCircleX + Math.sin(angleA) * mBigCircleRaduis);
        p2Y = (float) (mBigCircleY - Math.cos(angleA) * mBigCircleRaduis);

        p3X = (float) (mBigCircleX - Math.sin(angleA) * mBigCircleRaduis);
        p3Y = (float) (mBigCircleY + Math.cos(angleA) * mBigCircleRaduis);

        p4X = (float) (mSmallCircleX - Math.sin(angleA) * mSmallCircleRaduis);
        p4Y = (float) (mSmallCircleY + Math.cos(angleA) * mSmallCircleRaduis);

        //繪制路徑
        bezierPath = new Path();
        bezierPath.moveTo(p1X, p1Y);
        bezierPath.quadTo(controlX, controlY, p2X, p2Y);
        bezierPath.lineTo(p3X, p3Y);
        bezierPath.quadTo(controlX, controlY, p4X, p4Y);
        bezierPath.close();
        return bezierPath;
    }

}

最后呢,給出本效果的全部代碼,期間由于隔了幾天再來繼續寫這個效果,代碼的關鍵處也補了一點點注釋。哈哈,隔了幾天沒去瞧一眼,差點給我整懵逼了,還好,還好。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容