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

博主聲明:

轉(zhuǎn)載請(qǐng)?jiān)陂_頭附加本文鏈接及作者信息没酣,并標(biāo)記為轉(zhuǎn)載。本文由博主 威威喵 原創(chuàng)报账,請(qǐng)多支持與指教当宴。

本文首發(fā)于此 博主威威喵 | 博客主頁(yè)https://blog.csdn.net/smile_running

這幾天突然發(fā)現(xiàn) QQ 的消息拖拽動(dòng)畫效果還挺不錯(cuò)的,以前都沒去留意它踱阿,這幾看了一點(diǎn)關(guān)于貝塞爾曲線的知識(shí)管钳,這不剛好沙場(chǎng)練兵钦铁。于是從昨天開始呢,我就已經(jīng)開始補(bǔ)點(diǎn)高數(shù)的知識(shí)了才漆。雖然我現(xiàn)在已經(jīng)準(zhǔn)大四了牛曹,眨眼間就快畢業(yè)了,高數(shù)的知識(shí)還是從大一開始學(xué)習(xí)的醇滥,現(xiàn)在基本忘了差不多了黎比。

扯了一點(diǎn)關(guān)于我的學(xué)習(xí)經(jīng)歷,回到本篇問(wèn)題的關(guān)鍵鸳玩,QQ 消息拖拽效果是怎樣的呢阅虫?于是,我在模擬器裝了一個(gè) QQ 應(yīng)用怀喉,特地找了一下小號(hào)书妻,記得這個(gè)號(hào)好像是我初中申請(qǐng)的賬號(hào),以前那會(huì)兒 cf躬拢、飛車躲履、dnf 特別流行,搞了幾個(gè)小號(hào)搬磚聊闯,哈哈工猜。我們來(lái)看看消息拖拽的效果吧:

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

這樣的效果做起來(lái)并不簡(jiǎn)單,尤其是曲線的計(jì)算方面菱蔬,如果你也像我一樣忘了高數(shù)的知識(shí)點(diǎn)的話篷帅,建議你去翻翻三角函數(shù)那部分的知識(shí), 本文不會(huì)教你這些基本公式拴泌,也不會(huì)教你自定義 view 的基本流程魏身,本篇目的:計(jì)算和實(shí)現(xiàn)拖拽的粘性效果。如果這些基本知識(shí)不具備的話蚪腐,推薦你去看下我的自定義 view 相關(guān)文章箭昵。

有了上一篇(點(diǎn)擊這里:貝塞爾曲線(Bezier)之愛心點(diǎn)贊曲線動(dòng)畫效果)對(duì)貝塞爾曲線的基本了解和寫了一個(gè)小案例的鋪墊,在這次寫這個(gè) QQ 消息拖拽效果的時(shí)候回季,顯然輕松了許多家制。好了,廢話就說(shuō)這么多泡一,下面進(jìn)入重點(diǎn)內(nèi)容颤殴。

首先,看上面的效果顯示情況鼻忠,可以看成兩個(gè)小圓涵但,一個(gè)比較大一點(diǎn),可以拖拽出去,另一個(gè)小一點(diǎn)贤笆,但會(huì)隨著兩個(gè)圓的距離改變大小蝇棉。我們的步驟:在 onDraw 里面繪制兩個(gè)圓,用手指可以拖動(dòng)一個(gè)大圓芥永,并且小圓的大小會(huì)隨著兩圓的距離更改篡殷。這部分代碼非常簡(jiǎn)單,我就不做多的介紹了埋涧,如果你對(duì)下面代碼有不解之處板辽,還請(qǐng)自己補(bǔ)充知識(shí)。直接貼代碼:

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 消息拖拽消失的效果(大圓:不會(huì)消失棘催,且大小一致劲弦。小圓:與大圓的距離協(xié)調(diào)改變大小)
 * @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;
    }

    // 兩點(diǎn)之間的距離公式 √(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));
    }

}

運(yùn)行上面的代碼醇坝,你就會(huì)看到和我一樣的效果:

image

好了邑跪,上面的代碼只是做了一個(gè)鋪墊,也是必須實(shí)現(xiàn)的第一步呼猪。接下來(lái)重頭戲開始画畅,我們講講一些數(shù)學(xué)相關(guān)知識(shí)吧,本人高數(shù)也不怎么樣宋距,大學(xué)除了基本必修高數(shù)轴踱,也沒去深入學(xué)習(xí),不過(guò)這也不影響我們下面的操作谚赎。

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

image

這上面應(yīng)該不難看懂吧雳灵,兩個(gè)紅色圓就相當(dāng)于我們拖拽的圓一樣,從上面的草圖中,我們目前已知的有 c1 c2 r1 r2 這四個(gè)屬性值,c1 c2 代表圓心坐標(biāo)昼弟,r1 r2 是半徑。

當(dāng)用手指去拖拽大圓的時(shí)候,它們之間的聯(lián)系就用那兩根藍(lán)色的曲線來(lái)表示岛啸,兩曲線對(duì)應(yīng)的在兩圓上的坐標(biāo)點(diǎn)就是 p1 p2 p3 p4 四個(gè)點(diǎn)钓觉,這四個(gè)點(diǎn)會(huì)伴隨這兩圓的距離發(fā)生改變,你可以想象一下效果坚踩。

那么荡灾,從上圖中,我們就要去計(jì)算 p1 p2 p3 p4 這四個(gè)點(diǎn)的坐標(biāo),然后將四點(diǎn)封閉起來(lái)繪制成路徑即可批幌〈∪瘢可是,說(shuō)的比較輕巧荧缘,從目前我們已知的條件當(dāng)中皆警,能用得上的就 c1 c2 r1 r2 四個(gè)了,如何去求呢截粗?看接下來(lái)的這張圖:

image

從這張圖的計(jì)算過(guò)程中信姓,我們可以求得綠色三角形的角 a 的相關(guān)方程式。因?yàn)槲覀円阎?c1 圓心的坐標(biāo)值绸罗,就可以得出 p1 點(diǎn)的坐標(biāo)值意推,如上圖 p1x p1y 的值。

這樣的話珊蟀,我們可以利用三角函數(shù)公式得出 b 邊和 c 邊的值菊值,如上圖,最終得到的一個(gè)方程式中育灸,僅存在一個(gè)角 a 是我們未知的腻窒,接下來(lái)我們就要去計(jì)算角 a 的值,看下圖:

image

來(lái)到第一張圖描扯,看上面的黃色輔助線定页,假設(shè)它形成的是直角。我們就可以得到這兩條輔助線的邊長(zhǎng) dy 和 dx 绽诚。又根據(jù)三角形的補(bǔ)角兩平行線之間的夾角相等的定理典徊,我們得出圖中的三個(gè)角 a 都是一樣的大小。

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

這時(shí)候我們就取到了 a 相關(guān)的等式了,而 dx dy 都是可以計(jì)算出來(lái)的蜂桶,所以一連串下來(lái)儡毕,相關(guān)的等式都成立了,從而就可以計(jì)算出一個(gè)點(diǎn) p1扑媚,獲得 p1 點(diǎn)后腰湾,p2 p3 p4 不就手到擒來(lái)嘛。

最后要想形成貝塞爾曲線的效果疆股,除了 p1 p2 p3 p4 以外费坊,我們還需要一個(gè)控制點(diǎn),如圖上的點(diǎn) M旬痹,它是形成曲線的控制點(diǎn)附井,也是至關(guān)重要的一個(gè)點(diǎn)讨越,它的坐標(biāo)就是 M點(diǎn) ( (c1x+c2x) / 2 , (c1y+c2y) / 2 )

那么本篇數(shù)學(xué)相關(guān)的計(jì)算部分就已經(jīng)結(jié)束了,你還以為程序員不需要數(shù)學(xué)知識(shí)嘛永毅,哈哈把跨。下面就是該怎么寫程序了,把數(shù)學(xué)公式化為程序代碼沼死,這就得看你的編程水平啦着逐。

我寫了好一會(huì)兒,都是那個(gè)坐標(biāo)值正負(fù)的問(wèn)題卡了我挺久的漫雕,不過(guò)最終還是把代碼給搞出來(lái)了滨嘱,四個(gè)點(diǎn)的計(jì)算方法如下:

    private float p1X;
    private float p1Y;
    private float p2X;
    private float p2Y;
    private float p3X;
    private float p3Y;
    private float p4X;
    private float p4Y;
    //控制點(diǎn)
    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 四個(gè)點(diǎn)坐標(biāo)的計(jì)算
     *
     * @return
     */
    private Path drawDragBezier() {
        if (mSmallCircleRaduis < mSmallHideRaduis) {
            return null;
        }

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

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

        //控制點(diǎn)的計(jì)算
        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

然后呢,使用就很簡(jiǎn)單了浸间。返回一個(gè)路徑太雨,我們只要畫出來(lái)就好了,修改 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);
    }

好了吧魁蒜,點(diǎn)擊運(yùn)行囊扳,你將會(huì)看到如下的效果:

image

最后做了一點(diǎn)點(diǎn)小優(yōu)化,拖拽時(shí)沒有超出范圍可以回到原來(lái)的位置兜看,若超出拖拽的極限方法锥咸,導(dǎo)致兩個(gè)圓失去關(guān)聯(lián)時(shí),代表要摧毀那個(gè)大圓细移,手指松開那一剎那搏予,要將它隱藏掉,效果如下:

image

那么至此弧轧,我們的QQ消息的粘性動(dòng)畫已經(jīng)實(shí)現(xiàn)了雪侥,代碼倒是不難,難的是通過(guò)數(shù)學(xué)公式來(lái)計(jì)算出 p1 p2 p3 p4 點(diǎn)的坐標(biāo)值精绎,這可能會(huì)卡住很多人速缨,主要還是因?yàn)閿?shù)學(xué)功底不足,還是抽時(shí)間補(bǔ)補(bǔ)數(shù)學(xué)代乃,它可是個(gè)很有魅力的機(jī)靈鬼旬牲。

補(bǔ)充:(對(duì)上面的特效進(jìn)行優(yōu)化處理)

今天,8 月 8 日搁吓,早上 5 點(diǎn)半左右原茅,臺(tái)灣不幸遭到了地震,連我在福建中北部地帶都能偶感晃動(dòng)堕仔,我好像迷迷糊糊中感覺床在搖晃擂橘,是 6 點(diǎn)多級(jí)的地震,在此祝愿臺(tái)灣人民安好贮预。而且贝室,受臺(tái)風(fēng)的影響,家里下了好大的雨仿吞,不過(guò)倒是清涼了許多滑频。

好了,讓我們來(lái)優(yōu)化一下這個(gè)效果吧唤冈,博主之前還沒有處理的一些細(xì)節(jié)問(wèn)題峡迷,比如這個(gè) QQ 消息拖動(dòng),如果我們沒有將它拖斷掉你虹,也就是線還連著绘搞,上次的做法是將它的坐標(biāo)賦值給初始按下的坐標(biāo),這導(dǎo)致的效果是一瞬間就回去了傅物,動(dòng)畫太過(guò)生硬夯辖,體驗(yàn)不是特別好董饰,接下來(lái)我們來(lái)優(yōu)化一下蒿褂,讓它慢慢的回去,有一個(gè)過(guò)渡時(shí)間卒暂。

上次的代碼是這樣做的啄栓,直接回到手指起始按下的那一個(gè)點(diǎn)位置:

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

這個(gè)肯定不行昙楚,要對(duì)它的值進(jìn)行修改,我們的思想是這個(gè)樣子的诈嘿,看圖

image

我們需要慢慢的改變大圓的半徑堪旧,就相當(dāng)于改變被我們拉出來(lái)的那個(gè)圓的 x 坐標(biāo)和 y 坐標(biāo),我們給它定一個(gè)時(shí)間段永淌,讓它們一起開始變化崎场,這個(gè)就得使用到屬性動(dòng)畫來(lái)處理了,我們把上部分的代碼做如下修改即可

            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;//在拖動(dòng)過(guò)程中李滴,小圓半徑一直在縮小
                        }
                    });

                    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) {
                            //動(dòng)畫結(jié)束時(shí)螃宙,隱藏小圓
                            mSmallCircleRaduis = 0;//每次手松開,小圓半徑規(guī) 0
                        }
                    });
                }
                break;

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

    @Override
    protected void onDraw(Canvas canvas) {
        mBezierPath = drawDragBezier();
        //兩個(gè)圓還有聯(lián)系
        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);
        }
    }

好了芹助,一起來(lái)看看效果吧堂湖。為了使效果更加明顯闲先,我特地把縮回來(lái)的動(dòng)畫改為 10S,足夠你看清楚了吧

image

我給它加了一個(gè)插值器无蜂,回來(lái)的時(shí)候有一個(gè)反彈的效果伺糠!彈彈彈,彈走魚尾紋斥季。训桶。。

不過(guò)呢酣倾,還有一個(gè)地方需要優(yōu)化的舵揭,就是拖斷掉的時(shí)候,再松開會(huì)有一個(gè)消失的效果躁锡,我就搞的簡(jiǎn)單一點(diǎn)午绳,讓它慢慢的消失就好了。不過(guò)也可以學(xué)那個(gè)爆炸效果稚铣,會(huì)比較炫酷一點(diǎn)箱叁,我找了一下那個(gè)爆炸的圖片,懶得圖改成透明顏色了惕医,需要的自己去查一查幀動(dòng)畫就好了耕漱。

下面是放快的效果

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 消息拖拽消失的效果(大圓:不會(huì)消失,且大小一致抬伺。小圓:與大圓的距離協(xié)調(diào)改變大忻弧)
 * @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;//代表兩個(gè)關(guān)聯(lián)
    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();
        //兩個(gè)圓還有聯(lián)系
        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:
                // 兩個(gè)圓關(guān)聯(lián)了
                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;//在拖動(dòng)過(guò)程中峡钓,小圓半徑一直在縮小

                if (mSmallCircleRaduis < mSmallHideRaduis) {//小圓的半徑如果太小了妓笙,不顯示了。
                    isAttached = false;//表示兩個(gè)圓沒有關(guān)聯(lián)了能岩,意味這線被拖斷了
                }
                break;
            case MotionEvent.ACTION_UP:
                if (!isAttached) { // 被扯斷了寞宫,兩圓沒有聯(lián)系了
                    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;//在拖動(dòng)過(guò)程中,小圓半徑一直在縮小
                        }
                    });

                    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) {
                            //動(dòng)畫結(jié)束時(shí)膏燕,隱藏小圓
                            mSmallCircleRaduis = 0;//每次手松開钥屈,小圓半徑規(guī) 0
                        }
                    });
                }
                break;
        }
        invalidate();
        return true;
    }

    // 兩點(diǎn)之間的距離公式 √(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;
    //控制點(diǎn)
    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 四個(gè)點(diǎn)坐標(biāo)的計(jì)算
     *
     * @return
     */
    private Path drawDragBezier() {
        if (mSmallCircleRaduis < mSmallHideRaduis || !isAttached) {
            return null;
        }

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

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

        //控制點(diǎn)的計(jì)算
        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;
    }

}

最后呢,給出本效果的全部代碼坝辫,期間由于隔了幾天再來(lái)繼續(xù)寫這個(gè)效果篷就,代碼的關(guān)鍵處也補(bǔ)了一點(diǎn)點(diǎn)注釋。哈哈近忙,隔了幾天沒去瞧一眼竭业,差點(diǎn)給我整懵逼了智润,還好,還好未辆。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末做鹰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子鼎姐,更是在濱河造成了極大的恐慌,老刑警劉巖更振,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件炕桨,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡肯腕,警方通過(guò)查閱死者的電腦和手機(jī)献宫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)实撒,“玉大人姊途,你說(shuō)我怎么就攤上這事≈” “怎么了捷兰?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)负敏。 經(jīng)常有香客問(wèn)我贡茅,道長(zhǎng),這世上最難降的妖魔是什么其做? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任顶考,我火速辦了婚禮,結(jié)果婚禮上妖泄,老公的妹妹穿的比我還像新娘驹沿。我一直安慰自己,他們只是感情好蹈胡,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布渊季。 她就那樣靜靜地躺著,像睡著了一般审残。 火紅的嫁衣襯著肌膚如雪梭域。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天搅轿,我揣著相機(jī)與錄音病涨,去河邊找鬼。 笑死璧坟,一個(gè)胖子當(dāng)著我的面吹牛既穆,可吹牛的內(nèi)容都是我干的赎懦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼幻工,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼励两!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起囊颅,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤当悔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后踢代,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盲憎,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年胳挎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了饼疙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡慕爬,死狀恐怖窑眯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情医窿,我是刑警寧澤磅甩,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站姥卢,受9級(jí)特大地震影響更胖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜隔显,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一却妨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧括眠,春花似錦彪标、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至当船,卻和暖如春题画,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背德频。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工苍息, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓竞思,卻偏偏與公主長(zhǎng)得像表谊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盖喷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容