Android拖拽詳解

胡奚冰發表於2018-03-09

Android中實現拖拽其實很簡單,系統早已經提供了api讓我使用,主要用到了View的startDrag(startDragAndDrop API24+) 方法以及OnDragListener

startDrag

先來看下方法介紹:

/**
     * Starts a drag and drop operation. When your application calls this method, it passes a
     * {@link android.view.View.DragShadowBuilder} object to the system. The
     * system calls this object's {@link DragShadowBuilder#onProvideShadowMetrics(Point, Point)}
     * to get metrics for the drag shadow, and then calls the object's
     * {@link DragShadowBuilder#onDrawShadow(Canvas)} to draw the drag shadow itself.
     * <p>
     *  Once the system has the drag shadow, it begins the drag and drop operation by sending
     *  drag events to all the View objects in your application that are currently visible. It does
     *  this either by calling the View object's drag listener (an implementation of
     *  {@link android.view.View.OnDragListener#onDrag(View,DragEvent) onDrag()} or by calling the
     *  View object's {@link android.view.View#onDragEvent(DragEvent) onDragEvent()} method.
     *  Both are passed a {@link android.view.DragEvent} object that has a
     *  {@link android.view.DragEvent#getAction()} value of
     *  {@link android.view.DragEvent#ACTION_DRAG_STARTED}.
     * </p>
     * <p>
     * Your application can invoke {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object,
     * int) startDragAndDrop()} on any attached View object. The View object does not need to be
     * the one used in {@link android.view.View.DragShadowBuilder}, nor does it need to be related
     * to the View the user selected for dragging.
     * </p>
     * @param data A {@link android.content.ClipData} object pointing to the data to be
     * transferred by the drag and drop operation.
     * @param shadowBuilder A {@link android.view.View.DragShadowBuilder} object for building the
     * drag shadow.
     * @param myLocalState An {@link java.lang.Object} containing local data about the drag and
     * drop operation. When dispatching drag events to views in the same activity this object
     * will be available through {@link android.view.DragEvent#getLocalState()}. Views in other
     * activities will not have access to this data ({@link android.view.DragEvent#getLocalState()}
     * will return null).
     * <p>
     * myLocalState is a lightweight mechanism for the sending information from the dragged View
     * to the target Views. For example, it can contain flags that differentiate between a
     * a copy operation and a move operation.
     * </p>
     * @param flags Flags that control the drag and drop operation. This can be set to 0 for no
     * flags, or any combination of the following:
     *     <ul>
     *         <li>{@link #DRAG_FLAG_GLOBAL}</li>
     *         <li>{@link #DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION}</li>
     *         <li>{@link #DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION}</li>
     *         <li>{@link #DRAG_FLAG_GLOBAL_URI_READ}</li>
     *         <li>{@link #DRAG_FLAG_GLOBAL_URI_WRITE}</li>
     *         <li>{@link #DRAG_FLAG_OPAQUE}</li>
     *     </ul>
     * @return {@code true} if the method completes successfully, or
     * {@code false} if it fails anywhere. Returning {@code false} means the system was unable to
     * do a drag, and so no drag operation is in progress.
     */
    public final boolean startDragAndDrop(ClipData data, DragShadowBuilder shadowBuilder,Object myLocalState, int flags) 
複製程式碼

看到英文就頭大?沒事,我來翻譯解釋一下。

啟動拖放操作。當應用程式呼叫此方法時,它將傳遞一個DragShadowBuilder物件到系統。系統呼叫此物件的onProvideShadowMetrics(Point, Point)方法獲取拖動陰影的引數指標,然後呼叫onDrawShadow(Canvas)來繪製陰影。一旦系統有了拖動陰影,它就開始拖拽操作,通過將拖拽事件傳送到當前可見的應用程式中的所有檢視物件。這些檢視可以通過設定OnDragListener在或者實現onDragEvent方法接受DragEvent(事件)來響應和拖拽事件。

可以看到有四個引數:

ClipData data

其實就是一個封裝資料的物件,通過拖放操作傳遞給接受者。該物件可以存放一個Item的集合,Item可以存放如下資料:

public static class Item {
        final CharSequence mText;
        final String mHtmlText;
        final Intent mIntent;
        Uri mUri;
}
複製程式碼

注意到可以存放Intent,因此,通常可以將引數存入intent,然後通過靜態方法直接建立ClipData物件:

ClipData clipData = ClipData.newIntent("label", intent);
複製程式碼

該資料可以在監聽的中的DragEvent獲取

ClipData clipData = event.getClipData();
複製程式碼

簡單點說就是可以將一些資料傳遞給拖拽的接受者,該拖拽其實可以跨Activity的,如果只是同一個Activity可以使用第三個引數傳遞資料。

DragShadowBuilder shadowBuilder

用於建立拖拽view是的陰影,也就是跟隨手指移動的檢視,通常直接使用預設即可生成與一個原始view相同,帶有透明度的陰影:

View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(v);
複製程式碼

Object myLocalState

當你的拖拽行為是在同一個Activity中進行時可以傳遞一個任意物件,在監聽中可以通過{@link android.view.DragEvent#getLocalState()}獲得。如果是跨Activity拖拽中無法訪問此資料,getLocalState()將返回null。

int flags

控制拖放操作的標誌。因為沒有標誌可以設定為0,flag標誌拖動是否可以跨越視窗以及一些訪問許可權(需要API24+)。

瞭解了方法引數含義,接下來就是啟用拖拽了,通常會通過長按來觸發拖拽:

iv.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(v);
                v.startDrag(null, shadowBuilder, null, 0);
                //震動反饋
                v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
                return true;
            }
        });
複製程式碼

開始拖拽後還要有來接受這些拖拽事件,這就需要OnDragListener了。

OnDragListener

OnDragListener是在View中定義的介面,用於響應拖拽事件,可以通過View的setOnDragListener 方法設定監聽,有點類似於點選事件。

public interface OnDragListener { 
        boolean onDrag(View v, DragEvent event);
}
複製程式碼

設定監聽,實現onDrag(View v, DragEvent event)方法,其中View是設定該監聽的view,DragEvent是拖拽事件,可以通過event.getAction() 獲取具體事件型別,這和TouchEvent非常類似,具體事件型別有如下幾種:

fl_blue.setOnDragListener(new View.OnDragListener() {
            @Override
            public boolean onDrag(View v, DragEvent event) {
                //v 永遠是設定該監聽的view,這裡即fl_blue
                String simpleName = v.getClass().getSimpleName();
                Log.w(BLUE, "view name:" + simpleName);
                
                //獲取事件
                int action = event.getAction();
                switch (action) {
                    case DragEvent.ACTION_DRAG_STARTED:
                        Log.i(BLUE, "開始拖拽");
                        break;
                    case DragEvent.ACTION_DRAG_ENDED:
                        Log.i(BLUE, "結束拖拽");
                        break;
                    case DragEvent.ACTION_DRAG_ENTERED:
                        Log.i(BLUE, "拖拽的view進入監聽的view時");
                        break;
                    case DragEvent.ACTION_DRAG_EXITED:
                        Log.i(BLUE, "拖拽的view離開監聽的view時");
                        break;
                    case DragEvent.ACTION_DRAG_LOCATION:
                        float x = event.getX();
                        float y = event.getY();
                        long l = SystemClock.currentThreadTimeMillis();
                        Log.i(BLUE, "拖拽的view在監聽view中的位置:x =" + x + ",y=" + y);
                        break;
                    case DragEvent.ACTION_DROP:
                        Log.i(BLUE, "釋放拖拽的view");
                        break;
                }
                //是否響應拖拽事件,true響應,返回false只能接受到ACTION_DRAG_STARTED事件,後續事件不會收到
                return true;
            }
        });
複製程式碼

此處通過event.getX(); event.getY(); 獲取的x,y是手指(也即是被拖拽view的中心點)在監聽view的位置。

釋放手指會觸發ACTION_DRAG_ENDED 事件,如果此時被拖拽的view正好在監聽的view中,則會先觸發ACTION_DROP 事件。

這裡寫圖片描述

可以同時有多個view設定拖拽監聽接受事件,我給紅色和藍色view都設定了OnDragListener,然後拖動Android圖片到藍色區域後釋放,可以看到日誌如下:

03-09 14:53:54.518 12937-12937/com.huburt.app.androiddrag I/RED: 開始拖拽
03-09 14:53:54.518 12937-12937/com.huburt.app.androiddrag I/BLUE: 開始拖拽
03-09 14:53:55.689 12937-12937/com.huburt.app.androiddrag I/BLUE: 拖拽的view進入監聽的view時
03-09 14:53:55.689 12937-12937/com.huburt.app.androiddrag I/BLUE: 拖拽的view在BLUE中的位置:x =111.0,y=2.0
03-09 14:53:55.870 12937-12937/com.huburt.app.androiddrag I/BLUE: 拖拽的view在BLUE中的位置:x =112.0,y=23.0
03-09 14:53:56.014 12937-12937/com.huburt.app.androiddrag I/BLUE: 釋放拖拽的view
03-09 14:53:56.017 12937-12937/com.huburt.app.androiddrag I/RED: 結束拖拽
03-09 14:53:56.017 12937-12937/com.huburt.app.androiddrag I/BLUE: 結束拖拽
複製程式碼

現在我們已經可以把Android圖片拖出來,但是還不能把它放入目標view,其實也挺簡單的,只需要在ACTION_DROP事件做一些處理即可:

			case DragEvent.ACTION_DROP:
                        Log.i(BLUE, "釋放拖拽的view");
                        ImageView localState = (ImageView) event.getLocalState();
                        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                        layoutParams.topMargin = (int) event.getY() - localState.getWidth() / 2;
                        layoutParams.leftMargin = (int) event.getX() - localState.getHeight() / 2;
                        ((ViewGroup) localState.getParent()).removeView(localState);
                        fl_blue.addView(localState, layoutParams);
                        break;
複製程式碼

這裡因為是在同一個Activity中,我是將拖拽的view直接傳遞過來了,當然也可以只傳遞圖片,然後在接收的view中重新new一個imageview現實圖片。

執行一下就可以看到view可以拖拽到目標位置了。

可能文字描述不是特別清楚,請看demo

相關文章