Android 實(shí)現(xiàn)仿QQ拖拽氣泡效果的示例
效果圖:

一、實(shí)現(xiàn)思路
在列表中默認(rèn)使用自定義的TextView控件來(lái)展示消息氣泡,在自定義的TextView控件中重寫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è)拖拽氣泡效果的核心部分,具體如下圖:

如圖,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)畫
*/
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)畫演示效果。http://inloop.github.io/interpolator/
氣泡消失狀態(tài):當(dāng)拖拽的移動(dòng)圓超出一定范圍時(shí),并且松開了手指后,此時(shí)進(jìn)入氣泡消失狀態(tài),此時(shí)我們需要實(shí)現(xiàn)一個(gè)爆炸的動(dòng)畫。
爆炸的動(dòng)畫通過(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)畫來(lái)改變
/**
* 氣泡爆炸動(dòng)畫
*/
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)了
源碼地址:
https://github.com/loren325/CustomerView
以上就是Android 實(shí)現(xiàn)仿QQ拖拽氣泡效果的示例的詳細(xì)內(nèi)容,更多關(guān)于Android 實(shí)現(xiàn)仿QQ拖拽氣泡效果的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android WebView與JS交互全面詳解(小結(jié))
本篇文章主要介紹了Android WebView與JS交互全面詳解(小結(jié)),實(shí)現(xiàn)了Android客戶端與Web網(wǎng)頁(yè)交互,具有一定的參考價(jià)值,有興趣的可以了解一下2017-11-11
Android獲取系統(tǒng)時(shí)間以及網(wǎng)絡(luò)時(shí)間
這篇文章主要為大家詳細(xì)介紹了Android獲取系統(tǒng)時(shí)間以及網(wǎng)絡(luò)時(shí)間的方法,感興趣的小伙伴們可以參考一下2016-07-07
輕松實(shí)現(xiàn)Android仿淘寶地區(qū)選擇功能
這篇文章主要介紹了輕松實(shí)現(xiàn)Android仿淘寶地區(qū)選擇功能的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-06-06
android自定義View實(shí)現(xiàn)圓環(huán)顏色選擇器
這篇文章主要介紹了android自定義View實(shí)現(xiàn)圓環(huán)顏色選擇器,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06
Android 在 res/layout 文件夾 下創(chuàng)建一個(gè) 子文件夾實(shí)例
這篇文章主要介紹了Android 在 res/layout 文件夾 下創(chuàng)建一個(gè) 子文件夾實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03
Android實(shí)現(xiàn)Activity水平和垂直滾動(dòng)條的方法
這篇文章主要介紹了Android實(shí)現(xiàn)Activity水平和垂直滾動(dòng)條的方法,涉及Activity的ScrollView設(shè)置相關(guān)技巧,需要的朋友可以參考下2016-07-07

