仿網易新聞添加欄目和拖拽欄目效果(一)

今天偶然在apkbus上看到了以下欄目拖拽功能,我們也化繁為簡,一步一步來簡單實現。
第一步,實現拖拽;
第二步,拖拽后的動畫;
通過這篇文章可以了解的內容:
第一,view的拖拽;
第二,屬性動畫的執行;
第三,view的位置計算(各種相對位置,比如相對屏幕,相對父view等等)
第四,拖拽影像的產生。

introduce.gif

第一步實現拖拽

通過對其源碼的分析,實現拖拽的主要用法就是如下代碼:

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;
}

注意這里只是設置在某個事件后(比如這里長按事件)開始拖拽,這以后,如果我們按著view進行拖拽的話,那么拖拽監聽將可以監控到,雖然此時view不能拖動。
那我們如何設置監聽呢?上代碼:

mTvDrag.setOnDragListener(this);

設置監聽還有另外一種方式,就是調用了startDrag方法的view的父類中去實現onDragEvent()方法,這樣也可以監聽到view的拖拽。
注意點:
誰負責監聽,那么這個監聽的view就是可拖拽的范圍,這個很重要,因為這涉及到拖拽時坐標的計算

@Override
public boolean onDragEvent(DragEvent event) {
    int action = event.getAction();
    // 拖拽點x和y坐標
    int eventX = (int) event.getX();
    int eventY = (int) event.getY();

    switch (action) {
        // 拖拽開始監聽
        case DragEvent.ACTION_DRAG_STARTED:
            break;
        // 拖拽進入時監聽,可以開始進行拖拽
        // 和STARTED區別稍后講
        case DragEvent.ACTION_DRAG_ENTERED:
            Log.d("zp_test", "ENTERED " + event.getY());
            break;
        // 拖拽進行中
        case DragEvent.ACTION_DRAG_LOCATION:
            break;
        // 拖拽結束
        case DragEvent.ACTION_DRAG_ENDED:
        case DragEvent.ACTION_DRAG_EXITED:
            break;
        // 拖拽松開
        case DragEvent.ACTION_DROP:
            break;
    }
    return true;
}

也許你會講,直接用onTouchListener和layout方法也可以實現view跟隨手指一起滑動啊!沒錯是可以,但是用它來實現更為復雜的內容時,那將是場災難,比如長按后拖動,點擊事件不被屏蔽等等,實現起來就顯得稍微麻煩了。

第二步繪制影像陰影

細心的你一定能夠發現,在拖拽的時候,拖拽的view是透明度比本身的view要低的一個影像。拖拽實現的原理其實是:在可拖拽區域放置一個framelayout,在framelayout中有一個隱藏的imageview,當你長按哪個可拖動的view的時候,獲取到這個view的位置,然后通過復制這個view生成一個bitmap對象復制給framelayout中隱藏的imageview。然后根據手指位置來設置隱藏的imageview(此時顯示這個imageview)位置。
由此可見,所謂的拖拽其實是在拖拽一個影像,影像是最先設置在framelayout中的一個imageview。通過不斷更新imageview的setX和setY來進行拖動。

通過一個view來獲取其cache背景圖,從而生成一個跟其一模一樣的bitmap對象。

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;
}

至于計算當前view的位置,那么就需要你先了解以下這些內容:

第一點
getTop(),getLeft(),getRight(),getBottom()
這四個方法是指相對于父view的位置,并且一般情況下它的值是不會發生變化的,除非有屬性動畫改變其位置,或者重新進行layout方法的調用。

第二點
在拖拽監聽中int eventX = (int) event.getX();int eventY = (int) event.getY();
不同拖拽事件對應的eventx和eventy是不同的,具體如下:STARTED事件下對應是拖拽點與屏幕邊界的距離(包括狀態欄),ENTERED對應是拖拽點與界面邊界的距離(不包括狀態欄)、LOCATION、DROP事件下對應的是拖拽點和父view邊界的距離,END時為0。

第三點
當進行拖拽時:
依次執行started,entered,若干個location,然后drop,end這樣一個執行順序。

如下圖:

第二點示意圖1.jpg

接下來我們開工:
我們長按一個view,然后在同樣的位置生成一個影像。這個工作適合在started事件中去完成。

case DragEvent.ACTION_DRAG_STARTED:
    Log.d("zp_test", "STARTED " + event.getY() + " x: " + event.getX());
    if (mDrayListener != null) {
        // 7.0以下eventX,eventY是相對于屏幕邊界線的
        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());
        // 根據點擊位置獲取view
        if (y >= view.getY() && y <= view.getY() + view.getHeight()
                && x >= view.getX() && x <= view.getX() + view.getWidth())
            return view;
    }

    return null;
}

上面兩個方法就可以從點擊點獲取到點擊在哪個view上。
注意以下兩句代碼很重要
mPointToBorderX = eventX - location[0]; mPointToBorderY = eventY - location[1];
這個是在求得點擊點和被拖拽的view邊界的距離,因為我們要繪制影像的位置,就必須知道被拖拽的view到其父view的位置,這個就是影像的位置,但是我們從監聽方法中只能知道點擊點的坐標位置,我們還得求得這個view邊界到父view的位置,也就是得減去點擊點到拖拽view邊界的距離
影像y坐標 = (點擊點y坐標) - (點擊點到view邊界的距離)

示意圖2.jpg

最開始我沒有做這個計算,所以導致每次從STARTED事件到ENTERED事件時,都會跳動一下,而這個跳動的距離實際上就是mPointToBorderX 和mPointToBorderY 。

拖拽示意圖.gif

到這里,我們就實現了拖拽功能了,而后的功能在下篇進行介紹,最后,因為只有幾個類就不分享到github了,給出百度鏈接:
拖拽功能code.zip

如有錯誤,歡迎指出。(本人最近已離職,在上海如有工作推薦,麻煩各位留言或者私信我,謝謝大家!)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,560評論 25 708
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,257評論 4 61
  • 激蕩三十年讀后感 第一次聽說激蕩三十年這本書是在大三上學期管理學老師那聽說的,老師布置了一個任務,把這兩冊書讀完,...
    解小太陽閱讀 1,627評論 0 5
  • 神也無法壓制我的勃勃雄心壯志 對啊對啊又開始寫plan了乛乛 雖然每次放假都會寫然后開學時總是發現[em]e400...
    埋下胡楊閱讀 321評論 0 0
  • 每個人最后的依靠都只有自己. 別盲信任何人. 你和他,不會走到最后.因為,你看不透他,他,跟你是同一類人,同一類人...
    Sabrina667閱讀 167評論 0 0