Android自定義View之仿QQ拖拽氣泡效果

話不多說(shuō)七嫌,先上效果圖:
aa.gif

一鹅士、實(shí)現(xiàn)思路

在列表中默認(rèn)使用自定義的TextView控件來(lái)展示消息氣泡捂敌,在自定義的TextView控件中重寫(xiě)onTouchEvent方法,然后在DOWN艰毒、MOVE筐高、UP事件中分別處理拖拽效果。
整個(gè)拖拽效果我們可以拆分成以下幾步來(lái)實(shí)現(xiàn):
1.默認(rèn)狀態(tài)
2.兩氣泡相連狀態(tài)
3.兩氣泡分離狀態(tài)
4.氣泡消失狀態(tài)

二丑瞧、功能實(shí)現(xiàn)

默認(rèn)狀態(tài):用來(lái)做一個(gè)狀態(tài)的標(biāo)識(shí)柑土,無(wú)需特別處理。
兩氣泡相連狀態(tài):繪制一個(gè)固定圓和一個(gè)移動(dòng)圓绊汹,使用兩條貝塞爾曲線來(lái)實(shí)現(xiàn)兩氣泡連接的曲線稽屏,兩條貝塞爾曲線共用同一個(gè)控制點(diǎn),然后根據(jù)MOVE事件中的坐標(biāo)不斷重繪移動(dòng)圓灸促。
實(shí)現(xiàn)兩氣泡連接的效果诫欠,需要先計(jì)算出一些點(diǎn)的坐標(biāo),這也是整個(gè)拖拽氣泡效果的核心部分浴栽,具體如下圖:

思路圖.png

如圖荒叼,A點(diǎn)到B點(diǎn)是一條二階貝塞爾曲線,C點(diǎn)到D點(diǎn)也是一條二階貝塞爾曲線典鸡,它們共用同一個(gè)控制點(diǎn)被廓,所以我們要計(jì)算出A點(diǎn)、B點(diǎn)萝玷、C點(diǎn)嫁乘、D點(diǎn)以及控制點(diǎn)的坐標(biāo)。
首先來(lái)計(jì)算控制點(diǎn)的坐標(biāo)球碉,控制點(diǎn)的坐標(biāo)和容易計(jì)算出蜓斧,也就是固定圓的x坐標(biāo)加上移動(dòng)圓的x坐標(biāo),再除以2睁冬,固定圓的y坐標(biāo)同理得出挎春。

int controlX = (int) ((mBubStillCenter.x + mBubMoveCenter.x) / 2);
int controlY = (int) ((mBubStillCenter.y + mBubMoveCenter.y) / 2);

根據(jù)圖中所標(biāo)注的信息得知,∠a=∠d豆拨,∠b=∠c直奋,∠a=∠θ,由此可知施禾,我們求出∠θ所在的直角三角形的sin和cos值脚线,就可以計(jì)算出A點(diǎn)、B點(diǎn)弥搞、C點(diǎn)邮绿、D點(diǎn)的坐標(biāo)渠旁。
sin值可以通過(guò)移動(dòng)圓的y坐標(biāo)減去固定圓的y坐標(biāo),再除以兩圓心的距離斯碌,也就是O1到O2的距離一死。
cos值可以通過(guò)移動(dòng)圓的x坐標(biāo)減去固定圓的x坐標(biāo)肛度,再除以兩圓心的距離傻唾。

float sin = (mBubMoveCenter.y - mBubStillCenter.y) / mDist;
float cos = (mBubMoveCenter.x - mBubStillCenter.x) / mDist;

有了sin和cos值,對(duì)應(yīng)的A點(diǎn)承耿、B點(diǎn)冠骄、C點(diǎn)、D點(diǎn)的坐標(biāo)就好計(jì)算了

// A點(diǎn)
float bubbleStillStartX = mBubStillCenter.x + mBubbleStillRadius * sin;
float bubbleStillStartY = mBubStillCenter.y - mBubbleStillRadius * cos;
// B點(diǎn)
float bubbleMoveStartX = mBubMoveCenter.x + mBubbleMoveRadius * sin;
float bubbleMoveStartY = mBubMoveCenter.y - mBubbleMoveRadius * cos;
// C點(diǎn)
float bubbleMoveEndX = mBubMoveCenter.x - mBubbleMoveRadius * sin;
float bubbleMoveEndY = mBubMoveCenter.y + mBubbleMoveRadius * cos;
// D點(diǎn)
float bubbleStillEndX = mBubStillCenter.x - mBubbleStillRadius * sin;
float bubbleStillEndY = mBubStillCenter.y + mBubbleStillRadius * cos;

接下來(lái)就是把這些貝塞爾曲線和直線連起來(lái)加袋,就實(shí)現(xiàn)了兩氣泡相連的效果凛辣。
兩氣泡分離狀態(tài):當(dāng)拖拽的移動(dòng)圓超出固定圓一定范圍時(shí),就進(jìn)入了兩氣泡分離狀態(tài)职烧,此時(shí)我們只需要繪制移動(dòng)圓即可扁誓。當(dāng)拖拽的移動(dòng)圓回到固定圓一定范圍時(shí),此時(shí)會(huì)進(jìn)入兩氣泡相連狀態(tài)蚀之,并且需要實(shí)現(xiàn)一個(gè)氣泡還原的效果蝗敢。(這里會(huì)有個(gè)難點(diǎn),就是移動(dòng)圓我們可以在屏幕上任意拖動(dòng)而不被遮擋足删,這里放到后面來(lái)實(shí)現(xiàn)寿谴。)

public void move(float curX, float curY) {
    mBubMoveCenter.x = curX;
    mBubMoveCenter.y = curY;
    mDist = (float) Math.hypot(curX - mBubStillCenter.x, curY - mBubStillCenter.y);
    if(mBubbleState == BUBBLE_STATE_CONNECT){
        if(mDist < mMaxDist - MOVE_OFFSET){
            mBubbleStillRadius = mBubbleRadius - mDist / 10;
        }else {
            mBubbleState = BUBBLE_STATE_APART;
        }
    }
    invalidate();
}

mDist就是兩圓心的距離。

/**
 * 氣泡還原動(dòng)畫(huà)
 */
private void startBubbleRestAnim() {
    mBubbleStillRadius = mBubbleRadius;
    ValueAnimator animator = ValueAnimator.ofObject(new PointEvaluator(), new PointF(mBubMoveCenter.x, mBubMoveCenter.y), new PointF(mBubStillCenter.x, mBubStillCenter.y));
    animator.setDuration(200);
    animator.setInterpolator(input -> {
        float factor = 0.4f;
        return (float) (Math.pow(2, -10 * factor) * Math.sin((input - factor / 4) * (2 * Math.PI) / factor) + 1);
    });
    animator.addUpdateListener(animation -> {
        mBubMoveCenter = (PointF) animation.getAnimatedValue();
        invalidate();
    });
    animator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            mBubbleState = BUBBLE_STATE_DEFAULT;
            removeDragView();
            if(mDragListener != null){
                mDragListener.onRestore();
            }
        }
    });
    animator.start();
}

分享一個(gè)可視化插值器的網(wǎng)站失受,其中內(nèi)置了一些插值器公式讶泰,還可以查看動(dòng)畫(huà)演示效果。http://inloop.github.io/interpolator/

氣泡消失狀態(tài):當(dāng)拖拽的移動(dòng)圓超出一定范圍時(shí)拂到,并且松開(kāi)了手指后痪署,此時(shí)進(jìn)入氣泡消失狀態(tài),此時(shí)我們需要實(shí)現(xiàn)一個(gè)爆炸的動(dòng)畫(huà)兄旬。
爆炸的動(dòng)畫(huà)通過(guò)繪制一組圖片來(lái)實(shí)現(xiàn)

if(mBubbleState == BUBBLE_STATE_DISMISS){
    if(mIsBurstAnimStart){
        mBurstRect.set((int)(mBubMoveCenter.x - mBubbleMoveRadius), (int)(mBubMoveCenter.y - mBubbleMoveRadius),
                (int)(mBubMoveCenter.x + mBubbleMoveRadius), (int)(mBubMoveCenter.y + mBubbleMoveRadius));
        canvas.drawBitmap(mBurstBitmapArray[mCurDrawableIndex], null, mBurstRect, mBurstPaint);
    }
}

mCurDrawableIndex是圖片的索引狼犯,是通過(guò)屬性動(dòng)畫(huà)來(lái)改變

/**
 * 氣泡爆炸動(dòng)畫(huà)
 */
private void startBubbleBurstAnim() {
    ValueAnimator animator = ValueAnimator.ofInt(0, mBurstDrawablesArray.length);
    animator.setInterpolator(new LinearInterpolator());
    animator.setDuration(1000);
    animator.addUpdateListener(animation -> {
        mCurDrawableIndex = (int) animator.getAnimatedValue();
        invalidate();
    });
    animator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            mIsBurstAnimStart = false;
            if(mDragListener != null){
                mDragListener.onDismiss();
            }
        }
    });
    animator.start();
}

三、全屏拖拽效果實(shí)現(xiàn)

首先在DOWN事件中獲取當(dāng)前觸摸位置在全屏所在位置辖试,然后將當(dāng)前view緩存為bitmap辜王,并把此bitmap添加到rootview中,拖動(dòng)的時(shí)候直接繪制此bitmap罐孝。

//獲得當(dāng)前View在屏幕上的位置
int[] cLocation = new int[2];
getLocationOnScreen(cLocation);

if(rootView instanceof ViewGroup){
    mDragDotView = new DragDotView(getContext());

    //設(shè)置固定圓和移動(dòng)圓的圓心坐標(biāo)
    mDragDotView.setDragPoint(cLocation[0] + mWidth / 2, cLocation[1] + mHeight / 2, mRawX, mRawY);

    Bitmap bitmap = getBitmapFromView(this);
    if(bitmap != null){
        mDragDotView.setCacheBitmap(bitmap);
        ((ViewGroup) rootView).addView(mDragDotView);
        setVisibility(INVISIBLE);
    }
}

/**
 * 將當(dāng)前view緩存為bitmap呐馆,拖動(dòng)的時(shí)候直接繪制此bitmap
 * @param view
 * @return
 */
public Bitmap getBitmapFromView(View view)
{
    Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    view.draw(canvas);
    return bitmap;
}

至此,整個(gè)消息氣泡拖拽效果的核心部分就實(shí)現(xiàn)了莲兢,完整代碼見(jiàn)https://github.com/loren325/CustomerView

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末汹来,一起剝皮案震驚了整個(gè)濱河市续膳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌收班,老刑警劉巖坟岔,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異摔桦,居然都是意外死亡社付,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén)邻耕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鸥咖,“玉大人,你說(shuō)我怎么就攤上這事兄世√淅保” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵御滩,是天一觀的道長(zhǎng)鸥拧。 經(jīng)常有香客問(wèn)我,道長(zhǎng)削解,這世上最難降的妖魔是什么富弦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮钠绍,結(jié)果婚禮上舆声,老公的妹妹穿的比我還像新娘。我一直安慰自己柳爽,他們只是感情好媳握,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著磷脯,像睡著了一般蛾找。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赵誓,一...
    開(kāi)封第一講書(shū)人閱讀 51,775評(píng)論 1 307
  • 那天打毛,我揣著相機(jī)與錄音,去河邊找鬼俩功。 笑死幻枉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诡蜓。 我是一名探鬼主播熬甫,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蔓罚!你這毒婦竟也來(lái)了椿肩?” 一聲冷哼從身側(cè)響起瞻颂,我...
    開(kāi)封第一講書(shū)人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎郑象,沒(méi)想到半個(gè)月后贡这,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡厂榛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年盖矫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片噪沙。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炼彪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出正歼,到底是詐尸還是另有隱情,我是刑警寧澤拷橘,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布局义,位于F島的核電站,受9級(jí)特大地震影響冗疮,放射性物質(zhì)發(fā)生泄漏萄唇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一术幔、第九天 我趴在偏房一處隱蔽的房頂上張望另萤。 院中可真熱鬧,春花似錦诅挑、人聲如沸四敞。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)忿危。三九已至,卻和暖如春没龙,著一層夾襖步出監(jiān)牢的瞬間铺厨,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工硬纤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留解滓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓筝家,卻偏偏與公主長(zhǎng)得像洼裤,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肛鹏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

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