今天偶然在apkbus上看到了以下欄目拖拽功能,我們也化繁為簡(jiǎn),一步一步來(lái)簡(jiǎn)單實(shí)現(xiàn)。
第一步,實(shí)現(xiàn)拖拽;
第二步,拖拽后的動(dòng)畫;
通過這篇文章可以了解的內(nèi)容:
第一,view的拖拽;
第二,屬性動(dòng)畫的執(zhí)行;
第三,view的位置計(jì)算(各種相對(duì)位置,比如相對(duì)屏幕,相對(duì)父view等等)
第四,拖拽影像的產(chǎn)生。
第一步實(shí)現(xiàn)拖拽
通過對(duì)其源碼的分析,實(shí)現(xiàn)拖拽的主要用法就是如下代碼:
private static final ClipData EMPTY_CLIP_DATA = ClipData.newPlainText("", "");
@Override
public boolean onLongClick(View v) {
mTvDrag.startDrag(EMPTY_CLIP_DATA,new View.DragShadowBuilder(),mTvDrag,0);
return false;
}
注意這里只是設(shè)置在某個(gè)事件后(比如這里長(zhǎng)按事件)開始拖拽,這以后,如果我們按著view進(jìn)行拖拽的話,那么拖拽監(jiān)聽將可以監(jiān)控到,雖然此時(shí)view不能拖動(dòng)。
那我們?nèi)绾卧O(shè)置監(jiān)聽呢?上代碼:
mTvDrag.setOnDragListener(this);
設(shè)置監(jiān)聽還有另外一種方式,就是調(diào)用了startDrag方法的view的父類中去實(shí)現(xiàn)onDragEvent()
方法,這樣也可以監(jiān)聽到view的拖拽。
注意點(diǎn):
誰(shuí)負(fù)責(zé)監(jiān)聽,那么這個(gè)監(jiān)聽的view就是可拖拽的范圍,這個(gè)很重要,因?yàn)檫@涉及到拖拽時(shí)坐標(biāo)的計(jì)算
@Override
public boolean onDragEvent(DragEvent event) {
int action = event.getAction();
// 拖拽點(diǎn)x和y坐標(biāo)
int eventX = (int) event.getX();
int eventY = (int) event.getY();
switch (action) {
// 拖拽開始監(jiān)聽
case DragEvent.ACTION_DRAG_STARTED:
break;
// 拖拽進(jìn)入時(shí)監(jiān)聽,可以開始進(jìn)行拖拽
// 和STARTED區(qū)別稍后講
case DragEvent.ACTION_DRAG_ENTERED:
Log.d("zp_test", "ENTERED " + event.getY());
break;
// 拖拽進(jìn)行中
case DragEvent.ACTION_DRAG_LOCATION:
break;
// 拖拽結(jié)束
case DragEvent.ACTION_DRAG_ENDED:
case DragEvent.ACTION_DRAG_EXITED:
break;
// 拖拽松開
case DragEvent.ACTION_DROP:
break;
}
return true;
}
也許你會(huì)講,直接用onTouchListener和layout方法也可以實(shí)現(xiàn)view跟隨手指一起滑動(dòng)啊!沒錯(cuò)是可以,但是用它來(lái)實(shí)現(xiàn)更為復(fù)雜的內(nèi)容時(shí),那將是場(chǎng)災(zāi)難,比如長(zhǎng)按后拖動(dòng),點(diǎn)擊事件不被屏蔽等等,實(shí)現(xiàn)起來(lái)就顯得稍微麻煩了。
第二步繪制影像陰影
細(xì)心的你一定能夠發(fā)現(xiàn),在拖拽的時(shí)候,拖拽的view是透明度比本身的view要低的一個(gè)影像。拖拽實(shí)現(xiàn)的原理其實(shí)是:在可拖拽區(qū)域放置一個(gè)framelayout,在framelayout中有一個(gè)隱藏的imageview,當(dāng)你長(zhǎng)按哪個(gè)可拖動(dòng)的view的時(shí)候,獲取到這個(gè)view的位置,然后通過復(fù)制這個(gè)view生成一個(gè)bitmap對(duì)象復(fù)制給framelayout中隱藏的imageview。然后根據(jù)手指位置來(lái)設(shè)置隱藏的imageview(此時(shí)顯示這個(gè)imageview)位置。
由此可見,所謂的拖拽其實(shí)是在拖拽一個(gè)影像,影像是最先設(shè)置在framelayout中的一個(gè)imageview。通過不斷更新imageview的setX和setY來(lái)進(jìn)行拖動(dòng)。
通過一個(gè)view來(lái)獲取其cache背景圖,從而生成一個(gè)跟其一模一樣的bitmap對(duì)象。
private Bitmap createDraggedChildBitmap(View view) {
view.setDrawingCacheEnabled(true);
final Bitmap cache = view.getDrawingCache();
Bitmap bitmap = null;
if (cache != null) {
try {
bitmap = cache.copy(Bitmap.Config.ARGB_8888, false);
} catch (final OutOfMemoryError e) {
Log.w("zp_test", "Failed to copy bitmap from Drawing cache", e);
bitmap = null;
}
}
view.destroyDrawingCache();
view.setDrawingCacheEnabled(false);
return bitmap;
}
至于計(jì)算當(dāng)前view的位置,那么就需要你先了解以下這些內(nèi)容:
第一點(diǎn)
getTop(),getLeft(),getRight(),getBottom()
這四個(gè)方法是指相對(duì)于父view的位置,并且一般情況下它的值是不會(huì)發(fā)生變化的,除非有屬性動(dòng)畫改變其位置,或者重新進(jìn)行l(wèi)ayout方法的調(diào)用。
第二點(diǎn)
在拖拽監(jiān)聽中int eventX = (int) event.getX();int eventY = (int) event.getY();
不同拖拽事件對(duì)應(yīng)的eventx和eventy是不同的,具體如下:STARTED事件下對(duì)應(yīng)是拖拽點(diǎn)與屏幕邊界的距離(包括狀態(tài)欄),ENTERED對(duì)應(yīng)是拖拽點(diǎn)與界面邊界的距離(不包括狀態(tài)欄)、LOCATION、DROP事件下對(duì)應(yīng)的是拖拽點(diǎn)和父view邊界的距離,END時(shí)為0。
第三點(diǎn)
當(dāng)進(jìn)行拖拽時(shí):
依次執(zhí)行started,entered,若干個(gè)location,然后drop,end這樣一個(gè)執(zhí)行順序。
如下圖:
接下來(lái)我們開工:
我們長(zhǎng)按一個(gè)view,然后在同樣的位置生成一個(gè)影像。這個(gè)工作適合在started事件中去完成。
case DragEvent.ACTION_DRAG_STARTED:
Log.d("zp_test", "STARTED " + event.getY() + " x: " + event.getX());
if (mDrayListener != null) {
// 7.0以下eventX,eventY是相對(duì)于屏幕邊界線的
View drag = getViewFromPositionRelativeScreen(eventX, eventY);
if (drag == null) {
Log.e("zp_test", "drag is null");
break;
}
int[] location = new int[2];
drag.getLocationInWindow(location);
mPointToBorderX = eventX - location[0];
mPointToBorderY = eventY - location[1];
mDrayListener.onDragStart(createDraggedChildBitmap(drag), drag);
}
break;
private int[] location = new int[2];
private View getViewFromPositionRelativeScreen(int x, int y) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
getLocationInWindow(location);
return getViewFromPositionRelativeFather(x - location[0], y - location[1]);
} else {
return getViewFromPositionRelativeFather(x, y);
}
}
private View getViewFromPositionRelativeFather(int x, int y) {
Log.d("zp_test", "x: " + x + " y: " + y);
int childCount = getChildCount();
Log.d("zp_test", "childCount: " + childCount);
if (childCount <= 0)
return null;
for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
Log.d("zp_test", "view l : " + view.getX()
+ " view r: " + view.getX() + view.getWidth()
+ " view t: " + view.getY()
+ " view b: " + view.getY() + view.getHeight());
// 根據(jù)點(diǎn)擊位置獲取view
if (y >= view.getY() && y <= view.getY() + view.getHeight()
&& x >= view.getX() && x <= view.getX() + view.getWidth())
return view;
}
return null;
}
上面兩個(gè)方法就可以從點(diǎn)擊點(diǎn)獲取到點(diǎn)擊在哪個(gè)view上。
注意以下兩句代碼很重要
mPointToBorderX = eventX - location[0]; mPointToBorderY = eventY - location[1];
這個(gè)是在求得點(diǎn)擊點(diǎn)和被拖拽的view邊界的距離,因?yàn)槲覀円L制影像的位置,就必須知道被拖拽的view到其父view的位置,這個(gè)就是影像的位置,但是我們從監(jiān)聽方法中只能知道點(diǎn)擊點(diǎn)的坐標(biāo)位置,我們還得求得這個(gè)view邊界到父view的位置,也就是得減去點(diǎn)擊點(diǎn)到拖拽view邊界的距離
影像y坐標(biāo) = (點(diǎn)擊點(diǎn)y坐標(biāo)) - (點(diǎn)擊點(diǎn)到view邊界的距離)
最開始我沒有做這個(gè)計(jì)算,所以導(dǎo)致每次從STARTED事件到ENTERED事件時(shí),都會(huì)跳動(dòng)一下,而這個(gè)跳動(dòng)的距離實(shí)際上就是mPointToBorderX 和mPointToBorderY 。
到這里,我們就實(shí)現(xiàn)了拖拽功能了,而后的功能在下篇進(jìn)行介紹,最后,因?yàn)橹挥袔讉€(gè)類就不分享到github了,給出百度鏈接:
拖拽功能code.zip
如有錯(cuò)誤,歡迎指出。(本人最近已離職,在上海如有工作推薦,麻煩各位留言或者私信我,謝謝大家!)