啟用拖曳架構

Android 拖曳架構可讓您在應用程式中加入互動式拖曳功能。透過拖曳功能,使用者可以將文字、圖片、物件和任何由 URI 代表的內容,從 URI 的 View 複製到應用程式的另一個,或多視窗模式的應用程式之間。

將文字字串和圖片拖曳到應用程式中。 在分割畫面模式中,將文字字串和圖片在應用程式之間拖曳。
圖 1.在應用程式中拖曳。
圖 2.在應用程式間拖曳。

架構包含拖曳事件類別、拖曳事件監聽器,以及輔助類別和方法。雖然主要用於啟用資料傳輸,但您也可以將此架構用於其他 UI 動作。舉例來說,您可以建立混色應用程式,使用者直接將某個顏色圖示拖曳到另一個圖示上方。不過,本文件的其餘部分會說明資料移轉中的拖曳架構。

總覽

當使用者做出 UI 手勢讓應用程式辨識到開始拖曳的信號時,拖曳作業就會開始。當回應時,應用程式會通知系統拖曳作業開始。系統會再呼叫應用程式,取得拖曳的資料表示法,稱為「拖曳陰影」

當使用者將拖曳陰影移動到應用程式版面配置上時,系統會傳送拖曳事件給拖曳事件監聽器,並傳送與版面配置中 View 物件相關聯的回呼方法。如果使用者在可接受資料的檢視畫面放開拖曳陰影 (放置目標),系統會將資料傳送至目標。當使用者放開拖曳陰影時,無論拖曳陰影是否在放置目標上,拖曳作業都會結束。

實作 View.OnDragListener 來建立拖曳事件監聽器。請使用 View 物件的 setOnDragListener() 方法設定放置目標的事件監聽器。版面配置中的每個檢視畫面都有 onDragEvent() 回呼方法。

應用程式會呼叫 startDragAndDrop() 方法來通知系統傳送拖曳事件,藉此通知系統開始執行拖曳作業。這個方法也會為系統提供使用者拖曳的資料,以及描述資料的中繼資料。您可以在目前版面配置的任何 View 上呼叫 startDragAndDrop()。系統只會使用 View 物件來取得版面配置中的全域設定。

在拖曳作業期間,系統會傳送拖曳事件給拖曳事件監聽器,或版面配置中 View 物件的回呼方法。監聽器或回呼方法會使用中繼資料來判斷在是否要接受拖放的資料。如果使用者將資料放置到放置目標 (可接受資料的 View),系統會將含有資料的拖曳事件物件,傳送至放置目標的拖曳事件監聽器或回呼方法。

拖曳事件監聽器和回呼方法

View 會收到拖曳事件,其中包含實作 View.OnDragListener 或檢視畫面的 onDragEvent() 回呼方法的拖曳事件監聽器。系統呼叫方法或事件監聽器時,會提供 DragEvent 引數。

在大部分情況下,使用監聽器時最好使用回呼方法。設計 UI 時,您通常不會將 View 類別設為子類別,不過如果您使用回呼方法,則必須建立子類別來覆寫此方法。相較之下,您可以導入一個事件監聽器類別,然後搭配多個不同的 View 物件使用。您也可以將其實作為匿名內嵌類別或 lambda 運算式。如要設定 View 物件的事件監聽器,請呼叫 setOnDragListener()

或者,您可以修改 onDragEvent() 的預設實作方式,而不必覆寫該方法。在檢視畫面中設定 OnReceiveContentListener;詳情請參閱 setOnReceiveContentListener()。接著,onDragEvent() 方法預設會執行下列操作:

  • 回傳 true 值以回應對 startDragAndDrop() 的呼叫。
  • 如果拖放資料在檢視畫面上放置,請呼叫 performReceiveContent()。資料會以 ContentInfo 物件的形式傳遞至方法。這個方法會叫用 OnReceiveContentListener

  • 如果拖放資料在檢視畫面上放置,且 OnReceiveContentListener 時會取用任何內容,則會回傳 true。

定義 OnReceiveContentListener,專門處理應用程式的資料。為了回溯相容到 API 級別 24,請使用 OnReceiveContentListener 的 Jetpack 版本。

您可以讓 View 物件有拖曳事件監聽器和回呼方法;在此情況下,系統會先呼叫事件監聽器。除非監聽器回傳 false,否則系統不會呼叫回呼方法。

onDragEvent() 方法和 View.OnDragListener 的組合類似於搭配觸控事件使用的 onTouchEvent()View.OnTouchListener 組合。

拖曳流程

拖曳程序包含四個步驟或狀態:已啟動、繼續、捨棄和已結束。

已開始

為回應使用者的拖曳手勢,應用程式會呼叫 startDragAndDrop() 以指示系統開始拖曳作業。方法的引數提供以下內容:

  • 要拖曳的資料。
  • 用於繪製拖曳陰影的回呼
  • 描述拖曳資料的中繼資料:系統會回應應用程式以獲得拖曳陰影。然後在裝置上顯示拖曳陰影。:接下來,系統會將動作類型為 ACTION_DRAG_STARTED 的拖曳事件傳送至目前版面配置中所有 View 物件的拖曳事件監聽器。如要繼續接收拖曳事件 (包括可能的放置事件),拖曳事件監聽器必須回傳 true。此指令會向系統註冊事件監聽器。只有已註冊的事件監聽器會繼續接收拖曳事件。此時,事件監聽器也可以變更放置目標 View 物件的外觀,以表示檢視畫面能夠接受放置事件。:如果拖曳事件監聽器回傳 false,則必須等到系統傳送動作類型為 ACTION_DRAG_ENDED 的拖曳事件,才能接收目前作業的拖曳事件。事件監聽器會藉由回傳 false,向系統表示不想參與拖曳作業及接受拖曳的資料。
繼續
使用者持續拖曳。如果拖曳陰影與放置目標的定界框交會,系統會傳送一或多個拖曳事件至目標的拖曳事件監聽器。事件監聽器可能會配合事件變更放置目標 View 的外觀。舉例來說,如果事件指出拖曳陰影進入放置目標的定界框 (動作類型 ACTION_DRAG_ENTERED),事件監聽器就會醒目顯示 View
已拖曳
使用者在放置目標的定界框內放開拖曳陰影。系統會將動作類型為 ACTION_DROP 的拖曳事件傳送給放置目標的事件監聽器。拖曳事件物件包含在開始作業的 startDragAndDrop() 呼叫中傳遞至系統的資料。如果事件監聽器成功處理放置的資料,預期事件監聽器會將布林值 true 回傳系統。:使用者必須將拖曳陰影拖曳到已經過註冊而得以接收拖曳事件的 View (放置目標) 定界框中,此步驟才會發生。如果使用者在任何其他情況下放開拖曳陰影,系統就不會傳送 ACTION_DROP 拖曳事件。
已結束

使用者放開拖曳陰影後,且系統傳送後

視需要傳送動作類型為 ACTION_DROP 的拖曳事件,以傳送動作類型為 ACTION_DRAG_ENDED 的拖曳事件,表示拖曳作業已結束。無論使用者放開拖曳陰影的位置為何。即使事件監聽器收到 ACTION_DROP 事件,系統仍會將事件傳送給每個註冊以接收拖曳事件的事件監聽器。

如要進一步瞭解這些步驟,請參閱「拖曳作業」一節。

拖曳事件

系統會以 DragEvent 物件的形式傳送拖曳事件。此物件包含描述拖曳過程的動作類型。視動作類型而定,物件中也可能包含其他資料。

拖曳事件監聽器會接收 DragEvent 物件。為了取得動作類型,事件監聽器會呼叫 DragEvent.getAction()DragEvent 類別中的常數定義了六個可能的值,如表 1 所述:

表 1. DragEvent 動作類型

動作類型 意義
ACTION_DRAG_STARTED 應用程式會呼叫 startDragAndDrop() 並取得拖曳陰影。如果事件監聽器要繼續接收此作業的拖曳事件,則必須將布林值 true 回傳系統。
ACTION_DRAG_ENTERED 拖曳陰影會進入拖曳事件監聽器 View 的定界框。這是拖曳陰影進入定界框時,事件監聽器收到的第一個事件動作類型。
ACTION_DRAG_LOCATION ACTION_DRAG_ENTERED 事件之後,拖曳陰影仍位於拖曳事件監聽器 View 的定界框內。
ACTION_DRAG_EXITED ACTION_DRAG_ENTERED 和至少一個 ACTION_DRAG_LOCATION 事件之後,拖曳陰影會在拖曳事件監聽器 View 的定界框外移動。
ACTION_DROP 拖曳陰影會在拖曳事件監聽器的 View 上放開。只有在事件監聽器回傳布林值 true 以回應 ACTION_DRAG_STARTED 拖曳事件時,系統才會將動作類型傳送至 View 物件的事件監聽器。如果使用者在未註冊事件監聽器的 View 上放開拖曳陰影,或在不屬於目前版面配置的任一處放開,就不會傳送此動作類型。

如果拖曳成功,事件監聽器會傳回布林值 true。否則,則必須傳回 false

ACTION_DRAG_ENDED 系統即將結束拖曳動作。動作類型不一定要在 ACTION_DROP 事件之前。如果系統傳送 ACTION_DROP,收到 ACTION_DRAG_ENDED 動作類型並不表示捨棄成功。事件監聽器必須呼叫 getResult() (如表 2 所示) 以取得回應 ACTION_DROP 時回傳的值。如未傳送 ACTION_DROP 事件,則 getResult() 會回傳 false

DragEvent 物件也會包含應用程式呼叫 startDragAndDrop() 時提供給系統的資料和中繼資料。有些資料僅適用於特定動作類型,如表 2 所列。如要進一步瞭解事件及相關資料,請參閱「拖曳作業」一節。

表 2. 各動作類型的有效 DragEvent 資料

getAction()
getClipDescription()
getLocalState()
getX()
getY()
getClipData()
getResult()
ACTION_DRAG_STARTED ✓ ✓ ✓ ✓    
ACTION_DRAG_ENTERED ✓ ✓        
ACTION_DRAG_LOCATION ✓ ✓ ✓ ✓    
ACTION_DRAG_EXITED ✓ ✓        
ACTION_DROP ✓ ✓ ✓ ✓ ✓  
ACTION_DRAG_ENDED   ✓       ✓

DragEvent 方法 getAction()describeContents()writeToParcel()toString() 一律傳回有效的資料。

如果方法未包含特定動作類型的有效資料,系統會根據其結果類型傳回 null 或 0。

拖曳陰影

在拖曳作業期間,系統會顯示使用者拖曳的圖片。若是移動資料,此圖片則代表拖曳的資料。若是其他作業,圖片則代表拖曳作業的某些部分。

此圖片稱為「拖曳陰影」。使用您為 View.DragShadowBuilder 物件宣告的方法建立。使用 startDragAndDrop() 啟動拖曳作業時,請將建構工具傳遞給系統。為回應 startDragAndDrop(),系統會叫用您在 View.DragShadowBuilder 中定義的回呼方法,以取得拖曳陰影。

View.DragShadowBuilder 類別有兩個建構函式:

View.DragShadowBuilder(View)

此建構函式接受應用程式的任何 View 物件。建構函式會將 View 物件儲存在 View.DragShadowBuilder 物件中,因此回呼可以存取該物件來建構拖曳陰影。檢視畫面不一定要是使用者選取以開始作業的 View

使用此建構函式時,您不必擴充 View.DragShadowBuilder 或覆寫其方法。根據預設,您取得的拖曳陰影外觀會與做為引數傳遞的 View 相同,位於使用者輕觸螢幕的位置下方。

View.DragShadowBuilder()

如果使用這個建構函式,View.DragShadowBuilder 物件就不會有 View 物件。欄位已設為 null。您必須擴充 View.DragShadowBuilder 並覆寫其方法,否則就會產生隱形的拖曳陰影。系統不會擲回錯誤。

View.DragShadowBuilder 類別有兩個方法一起建立拖曳陰影:

onProvideShadowMetrics()

當您呼叫 startDragAndDrop() 後,系統會立即呼叫此方法。此方法會將拖曳陰影的維度和觸控點傳送到系統。此方法有兩個參數:

outShadowSizePoint 物件。拖曳陰影寬度為 x,高度為 y

outShadowTouchPointPoint 物件。觸控點是指拖曳陰影內的位置,拖曳時必須位於使用者手指下方。其 X 位置為 xY 位置則為 y

onDrawShadow()

呼叫 onProvideShadowMetrics() 後,系統會立即呼叫 onDrawShadow() 來建立拖曳陰影。此方法有一個引數,即系統根據 onProvideShadowMetrics() 中提供的參數建構的 Canvas 物件。此方法會在提供的 Canvas 上繪製拖曳陰影。

為提升效能,請盡量使用較小的拖曳陰影。如果是單一項目,建議您使用圖示。如果需要選擇多個項目,建議您使用堆疊中的圖示,而不要將整個圖片蓋滿螢幕畫面。

拖曳作業

本節逐步說明如何開始拖曳、在拖曳期間回應事件、回應放置事件,以及結束拖曳作業。

開始拖曳

使用者用拖曳手勢 (通常是按住) 開始在 View 物件上開始拖曳。為回應,應用程式必須執行以下操作:

  1. 為要移動的資料建立 ClipData 物件和 ClipData.Item 物件。在 ClipData 中,提供在 ClipData 內儲存於 ClipDescription 物件的中繼資料。如果不是代表資料移動的拖曳作業,建議您使用 null,而非實際物件。

    例如,此程式碼片段說明如何建立包含 ImageView 標記 (或標籤) 的 ClipData 物件,藉此回應 ImageView 的按住手勢:

    Kotlin

    // Create a string for the ImageView label.
    val IMAGEVIEW_TAG = "icon bitmap"
    ...
    val imageView = ImageView(context).apply {
    // Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
    setImageBitmap(iconBitmap)
    tag = IMAGEVIEW_TAG
    setOnLongClickListener { v ->
            // Create a new ClipData. This is done in two steps to provide
            // clarity. The convenience method ClipData.newPlainText() can
            // create a plain text ClipData in one step.
    
            // Create a new ClipData.Item from the ImageView object's tag.
            val item = ClipData.Item(v.tag as? CharSequence)
    
            // Create a new ClipData using the tag as a label, the plain text
            // MIME type, and the already-created item. This creates a new
            // ClipDescription object within the ClipData and sets its MIME type
            // to "text/plain".
            val dragData = ClipData(
                v.tag as? CharSequence,
                arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
                item)
    
            // Instantiate the drag shadow builder.
            val myShadow = MyDragShadowBuilder(view: this)
    
            // Start the drag.
            v.startDragAndDrop(dragData,  // The data to be dragged.
                               myShadow,  // The drag shadow builder.
                               null,      // No need to use local data.
                               0          // Flags. Not currently used, set to 0.
            )
    
           // Indicate that the long-click is handled.
           true
    }
    }
    

    Java

    // Create a string for the ImageView label.
    private static final String IMAGEVIEW_TAG = "icon bitmap";
    ...
    // Create a new ImageView.
    ImageView imageView = new ImageView(context);
    
    // Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
    imageView.setImageBitmap(iconBitmap);
    
    // Set the tag.
    imageView.setTag(IMAGEVIEW_TAG);
    
    // Set a long-click listener for the ImageView using an anonymous listener
    // object that implements the OnLongClickListener interface.
    imageView.setOnLongClickListener( v -> {
    
    // Create a new ClipData. This is done in two steps to provide clarity. The
    // convenience method ClipData.newPlainText() can create a plain text
    // ClipData in one step.
    
    // Create a new ClipData.Item from the ImageView object's tag.
    ClipData.Item item = new ClipData.Item((CharSequence) v.getTag());
    
    // Create a new ClipData using the tag as a label, the plain text MIME type,
    // and the already-created item. This creates a new ClipDescription object
    // within the ClipData and sets its MIME type to "text/plain".
    ClipData dragData = new ClipData(
            (CharSequence) v.getTag(),
            new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN },
            item);
    
    // Instantiate the drag shadow builder.
    View.DragShadowBuilder myShadow = new MyDragShadowBuilder(imageView);
    
    // Start the drag.
    v.startDragAndDrop(dragData,  // The data to be dragged.
                           myShadow,  // The drag shadow builder.
                           null,      // No need to use local data.
                           0          // Flags. Not currently used, set to 0.
    );
    
    // Indicate that the long-click is handled.
    return true;
    });
    
  2. 覆寫 View.DragShadowBuilder 中的方法來定義 myDragShadowBuilder。下列程式碼片段會為 TextView 建立小型的矩形灰色拖曳陰影:

    Kotlin

    private class MyDragShadowBuilder(view: View) : View.DragShadowBuilder(view) {
    
    private val shadow = ColorDrawable(Color.LTGRAY)
    
    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    override fun onProvideShadowMetrics(size: Point, touch: Point) {
    
            // Set the width of the shadow to half the width of the original
            // View.
            val width: Int = view.width / 2
    
            // Set the height of the shadow to half the height of the original
            // View.
            val height: Int = view.height / 2
    
            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height)
    
            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height)
    
            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2)
    }
    
    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    override fun onDrawShadow(canvas: Canvas) {
    
            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas)
    }
    }
    

    Java

    private static class MyDragShadowBuilder extends View.DragShadowBuilder {
    
    // The drag shadow image, defined as a drawable object.
    private static Drawable shadow;
    
    // Constructor.
    public MyDragShadowBuilder(View view) {
    
            // Store the View parameter.
            super(view);
    
            // Create a draggable image that fills the Canvas provided by the
            // system.
            shadow = new ColorDrawable(Color.LTGRAY);
    }
    
    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    @Override
    public void onProvideShadowMetrics (Point size, Point touch) {
    
            // Define local variables.
            int width, height;
    
            // Set the width of the shadow to half the width of the original
            // View.
            width = getView().getWidth() / 2;
    
            // Set the height of the shadow to half the height of the original
            // View.
            height = getView().getHeight() / 2;
    
            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height);
    
            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height);
    
            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2);
    }
    
    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    @Override
    public void onDrawShadow(Canvas canvas) {
    
            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas);
    }
    }
    

回應拖曳開始事件

在拖曳作業中,系統會把拖曳事件分派到目前版面配置中 View 物件的拖曳事件監聽器。事件監聽器會呼叫 DragEvent.getAction() 來取得動作類型。拖曳開始時,此方法會傳回 ACTION_DRAG_STARTED

如要回應動作類型為 ACTION_DRAG_STARTED 的事件,拖曳事件監聽器必須執行以下操作:

  1. 呼叫 DragEvent.getClipDescription(),並在回傳的 ClipDescription 中使用 MIME 類型方法,確認事件監聽器是否能接受拖曳的資料。

    如果拖曳作業不是資料移動,就不需要進行此步驟。

  2. 如果拖曳事件監聽器可接受放置,則必須回傳 true 來指示系統繼續將拖曳事件傳送至事件監聽器。如果事件監聽器不接受放置,事件監聽器必須回傳 false,且系統會停止將拖曳事件傳送至事件監聽器,直到系統傳送 ACTION_DRAG_ENDED 以完成拖曳作業。

如果是 ACTION_DRAG_STARTED 事件,下列 DragEvent 方法無效:getClipData()getX()getY()getResult()

在拖曳期間處理事件

在拖曳過程中,回傳 true 以回應 ACTION_DRAG_STARTED 拖曳事件的拖曳事件監聽器,會繼續接收拖曳事件。事件監聽器在拖曳期間收到的拖曳事件類型,取決於拖曳陰影的位置和事件監聽器 View 的瀏覽權限。事件監聽器主要會使用拖曳事件來判斷是否需要變更 View 的外觀。

在拖曳過程中,DragEvent.getAction() 會回傳下列三個值的其中之一:

  • ACTION_DRAG_ENTERED:當觸控點 (使用者手指或滑鼠下方的遊標) 進入事件監聽器 View 的定界框時,事件監聽器會收到此事件動作類型。
  • ACTION_DRAG_LOCATION:事件監聽器收到 ACTION_DRAG_ENTERED 事件後,每次觸控點移動,直到收到 ACTION_DRAG_EXITED 事件為止,就會收到新的 ACTION_DRAG_LOCATION 事件。getX()getY() 方法會回傳觸控點的 X 和 Y 座標。
  • ACTION_DRAG_EXITED:此事件動作類型會傳送給先前接收 ACTION_DRAG_ENTERED 的事件監聽器。當拖曳陰影觸控點從事件監聽器 View 的定界框內移動至框外,就會傳送事件。

拖曳事件監聽器不需要回應上述任何動作類型。如果事件監聽器回傳該值,系統會忽略該值。

以下是回應各種動作類型的方針:

  • 回應 ACTION_DRAG_ENTEREDACTION_DRAG_LOCATION 時,事件監聽器可以變更 View 的外觀,以表示檢視畫面是潛在的放置目標。
  • 動作類型為 ACTION_DRAG_LOCATION 的事件包含 getX()getY() 的有效資料,對應至觸控點位置。事件監聽器可使用此資訊調整觸控點的 View 外觀,或決定使用者可放開拖曳陰影 (即放置資料) 的確切位置。
  • 為回應 ACTION_DRAG_EXITED,事件監聽器必須重設回應 ACTION_DRAG_ENTEREDACTION_DRAG_LOCATION 時套用的任何外觀變更。這會向使用者表明 View 不再是預期的放置目標。

回應放置

當使用者在 View 上放開拖曳陰影,且 View 先前回報可接受拖曳的內容時,系統就會將拖曳事件分派給動作類型 ACTION_DROPView

拖曳事件監聽器必須執行以下作業:

  1. 呼叫 getClipData() 即可取得原先在呼叫 startDragAndDrop() 中提供的 ClipData 物件,並處理資料。如果拖曳作業不是資料移動,就不需要進行此步驟。

  2. 回傳布林值 true,表示已成功處理放置;如未成功處理,則回傳 false。回傳的值會成為 getResult() 最終為 ACTION_DRAG_ENDED 事件回傳的值。如果系統未傳送 ACTION_DROP 事件,則 getResult()ACTION_DRAG_ENDED 事件回傳的值會是 false

如果是 ACTION_DROP 事件,getX()getY() 會使用所接收放置的 View 座標系統,在放置時回傳觸控點的 XY 位置。

系統允許使用者在拖曳事件監聽器未收到拖曳事件的 View 上放開拖曳陰影。使用者也可以在應用程式 UI 的空白區域或應用程式外的區域放開拖曳陰影。在這些情況下,雖然不會傳送動作類型為 ACTION_DROP 的事件,但會傳送 ACTION_DRAG_ENDED 事件。

回應拖曳結束事件

在使用者放開拖曳陰影後,系統會立即將動作類型為 ACTION_DRAG_ENDED 的拖曳事件傳送至應用程式中的所有拖曳事件監聽器。這表示拖曳作業已結束。

每個拖曳事件監聽器都必須執行以下動作:

  1. 如果事件監聽器在作業期間變更其 View 物件的外觀,事件監聽器必須將 View 重設為預設外觀。這是讓使用者知道作業已經結束的視覺指標。
  2. 事件監聽器可以選擇呼叫 getResult() 以進一步瞭解作業。如果事件監聽器回傳 true 以回應動作類型 ACTION_DROP 的事件,則 getResult() 會回傳布林值 true。在所有其他情況下,getResult() 都會回傳布林值 false,包括系統未傳送 ACTION_DROP 事件時。
  3. 如要表示拖曳作業成功完成,事件監聽器必須將布林值 true 回傳系統。

回應拖曳事件:範例

所有拖曳事件均由拖曳事件方法或事件監聽器接收。以下程式碼片段是回應拖曳事件的簡單範例:

Kotlin

val imageView = ImageView(this)

// Set the drag event listener for the View.
imageView.setOnDragListener { v, e ->

    // Handle each of the expected events.
    when (e.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determine whether this View can accept the dragged data.
            if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                (v as? ImageView)?.setColorFilter(Color.BLUE)

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate()

                // Return true to indicate that the View can accept the dragged
                // data.
                true
            } else {
                // Return false to indicate that, during the current drag and
                // drop operation, this View doesn't receive events again until
                // ACTION_DRAG_ENDED is sent.
                false
            }
        }
        DragEvent.ACTION_DRAG_ENTERED -> {
            // Apply a green tint to the View.
            (v as? ImageView)?.setColorFilter(Color.GREEN)

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate()

            // Return true. The value is ignored.
            true
        }

        DragEvent.ACTION_DRAG_LOCATION ->
            // Ignore the event.
            true
        DragEvent.ACTION_DRAG_EXITED -> {
            // Reset the color tint to blue.
            (v as? ImageView)?.setColorFilter(Color.BLUE)

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate()

            // Return true. The value is ignored.
            true
        }
        DragEvent.ACTION_DROP -> {
            // Get the item containing the dragged data.
            val item: ClipData.Item = e.clipData.getItemAt(0)

            // Get the text data from the item.
            val dragData = item.text

            // Display a message containing the dragged data.
            Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show()

            // Turn off color tints.
            (v as? ImageView)?.clearColorFilter()

            // Invalidate the view to force a redraw.
            v.invalidate()

            // Return true. DragEvent.getResult() returns true.
            true
        }

        DragEvent.ACTION_DRAG_ENDED -> {
            // Turn off color tinting.
            (v as? ImageView)?.clearColorFilter()

            // Invalidate the view to force a redraw.
            v.invalidate()

            // Do a getResult() and display what happens.
            when(e.result) {
                true ->
                    Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG)
                else ->
                    Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG)
            }.show()

            // Return true. The value is ignored.
            true
        }
        else -> {
            // An unknown action type is received.
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            false
        }
    }
}

Java

View imageView = new ImageView(this);

// Set the drag event listener for the View.
imageView.setOnDragListener( (v, e) -> {

    // Handle each of the expected events.
    switch(e.getAction()) {

        case DragEvent.ACTION_DRAG_STARTED:

            // Determine whether this View can accept the dragged data.
            if (e.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {

                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                ((ImageView)v).setColorFilter(Color.BLUE);

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate();

                // Return true to indicate that the View can accept the dragged
                // data.
                return true;

            }

            // Return false to indicate that, during the current drag and drop
            // operation, this View doesn't receive events again until
            // ACTION_DRAG_ENDED is sent.
            return false;

        case DragEvent.ACTION_DRAG_ENTERED:

            // Apply a green tint to the View.
            ((ImageView)v).setColorFilter(Color.GREEN);

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate();

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DRAG_LOCATION:

            // Ignore the event.
            return true;

        case DragEvent.ACTION_DRAG_EXITED:

            // Reset the color tint to blue.
            ((ImageView)v).setColorFilter(Color.BLUE);

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate();

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DROP:

            // Get the item containing the dragged data.
            ClipData.Item item = e.getClipData().getItemAt(0);

            // Get the text data from the item.
            CharSequence dragData = item.getText();

            // Display a message containing the dragged data.
            Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG).show();

            // Turn off color tints.
            ((ImageView)v).clearColorFilter();

            // Invalidate the view to force a redraw.
            v.invalidate();

            // Return true. DragEvent.getResult() returns true.
            return true;

        case DragEvent.ACTION_DRAG_ENDED:

            // Turn off color tinting.
            ((ImageView)v).clearColorFilter();

            // Invalidate the view to force a redraw.
            v.invalidate();

            // Do a getResult() and displays what happens.
            if (e.getResult()) {
                Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG).show();
            }

            // Return true. The value is ignored.
            return true;

        // An unknown action type is received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;

});

在多視窗模式下拖曳

搭載 Android 7.0 (API 級別 24) 以上版本的裝置支援多視窗模式,可讓使用者透過拖曳作業,在不同應用程式間移動資料。詳情請參閱「多視窗模式支援」。

拖曳作業開始的「來源應用程式」會提供資料。拖曳作業結束的目標應用程式會收到資料。

開始拖曳作業時,來源應用程式必須設定 DRAG_FLAG_GLOBAL 旗標,以表示使用者可將資料拖曳到其他應用程式。

由於資料會跨應用程式邊界移動,因此應用程式會使用內容 URI 共用資料的存取權。必要條件:

  • 視來源應用程式要授予目標應用程式的資料讀取或寫入權限而定,來源應用程式必須設定 DRAG_FLAG_GLOBAL_URI_READ 和/或 DRAG_FLAG_GLOBAL_URI_WRITE 標記。
  • 目標應用程式必須先呼叫 requestDragAndDropPermissions(),才能處理使用者拖曳至應用程式的資料。如果目標應用程式不再需要存取拖曳資料的權限,應用程式可以在從 requestDragAndDropPermissions() 回傳的物件上呼叫 release()。否則,當內含的活動刪除時,權限就會釋放。如果您的實作需要啟動新的活動來處理捨棄的 URI,您必須為新的 Activity 授予相同權限。您必須設定剪輯資料和標記:

    Kotlin

    intent.setClipData(clipData)
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    

    Java

    intent.setClipData(clipData);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    

下列程式碼片段示範如何在拖曳作業發生後,立即撤銷拖曳資料的唯讀存取權。如需更完整的範例,請參閱 GitHub 上的 DragAndDrop 範例

來源拖曳活動

Kotlin

// Drag a file stored in an images/ directory in internal storage.
val internalImagesDir = File(context.filesDir, "images")
val imageFile = File(internalImagesDir, imageFilename)
val uri = FileProvider.getUriForFile(context, contentAuthority, imageFile)

val listener = OnDragStartListener@{ view: View, _: DragStartHelper ->
    val clipData = ClipData(ClipDescription("Image Description",
                                            arrayOf("image/*")),
                            ClipData.Item(uri))
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    val flags = View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ
    return@OnDragStartListener view.startDragAndDrop(clipData,
                                                     View.DragShadowBuilder(view),
                                                     null,
                                                     flags)
}

// Container where the image originally appears in the source app.
val srcImageView = findViewById<ImageView>(R.id.imageView)

// Detect and start the drag event.
DragStartHelper(srcImageView, listener).apply {
    attach()
}

Java

// Drag a file stored in an images/ directory in internal storage.
File internalImagesDir = new File(context.getFilesDir(), "images");
File imageFile = new File(internalImagesDir, imageFilename);
final Uri uri = FileProvider.getUriForFile(context, contentAuthority, imageFile);

// Container where the image originally appears in the source app.
ImageView srcImageView = findViewById(R.id.imageView);

// Enable the view to detect and start the drag event.
new DragStartHelper(srcImageView, (view, helper) -> {
    ClipData clipData = new ClipData(new ClipDescription("Image Description",
                                                          new String[] {"image/*"}),
                                     new ClipData.Item(uri));
    // Must include DRAG_FLAG_GLOBAL to permit dragging data between apps.
    // This example provides read-only access to the data.
    int flags = View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ;
    return view.startDragAndDrop(clipData,
                                 new View.DragShadowBuilder(view),
                                 null,
                                 flags);
}).attach();

目標拖曳活動

Kotlin

// Container where the image is to be dropped in the target app.
val targetImageView = findViewById<ImageView>(R.id.imageView)

targetImageView.setOnDragListener { view, event ->

    when (event.action) {

        ACTION_DROP -> {
            val imageItem: ClipData.Item = event.clipData.getItemAt(0)
            val uri = imageItem.uri

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            val dropPermissions = requestDragAndDropPermissions(event)
            (view as ImageView).setImageURI(uri)

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release()
            return@setOnDragListener true
        }

        // Implement logic for other DragEvent cases here.

        // An unknown action type is received.
        else -> {
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            return@setOnDragListener false
        }

    }
}

Java

// Container where the image is to be dropped in the target app.
ImageView targetImageView = findViewById(R.id.imageView);

targetImageView.setOnDragListener( (view, event) -> {

    switch (event.getAction()) {

        case ACTION_DROP:
            ClipData.Item imageItem = event.getClipData().getItemAt(0);
            Uri uri = imageItem.getUri();

            // Request permission to access the image data being dragged into
            // the target activity's ImageView element.
            DragAndDropPermissions dropPermissions =
                requestDragAndDropPermissions(event);

            ((ImageView)view).setImageURI(uri);

            // Release the permission immediately afterward because it's no
            // longer needed.
            dropPermissions.release();

            return true;

        // Implement logic for other DragEvent cases here.

        // An unknown action type was received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;
});

使用 DropHelper 可簡化拖曳作業

DropHelper 類別簡化了拖曳功能的實作方式。DropHelper (隸屬於 Jetpack DragAndDrop 資源庫) 提供回溯至 API 級別 24 的相容性。

使用 DropHelper 來指定放置目標、自訂放置目標醒目顯示,以及定義系統如何處理放置的資料。

指定放置目標

DropHelper.configureView() 是靜態的超載方法,可讓您指定放置目標。其參數包括:

舉例來說,如要建立可接受圖片的放置目標,請使用下列任一方法呼叫:

Kotlin

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    options,
    onReceiveContentListener)

// or

configureView(
    myActivity,
    targetView,
    arrayOf("image/*"),
    onReceiveContentListener)

Java

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    options,
    onReceiveContentlistener);

// or

DropHelper.configureView(
    myActivity,
    targetView,
    new String[] {"image/*"},
    onReceiveContentlistener);

第二個呼叫會忽略放置目標設定選項,在此情況下,放置目標醒目顯示顏色會設為主題的次要 (或強調) 顏色,醒目顯示的圓角半徑設為 16 dp,EditText 元件清單則為空白。詳情請參閱下節說明。

設定放置目標

DropHelper.Options 內部類別可讓您設定放置目標。請將該類別的例項提供給 DropHelper.configureView(Activity, View, String[], Options, OnReceiveContentListener) 方法。詳情請參閱上一節的說明。

自訂放置目標醒目顯示

DropHelper 會設定放置目標,在使用者將內容拖曳到目標上方時顯示醒目效果。DropHelper 提供預設樣式,而 DropHelper.Options 可讓您設定醒目顯示的顏色,以及指定醒目顯示矩形的邊角半徑。

請使用 DropHelper.Options.Builder 類別建立 DropHelper.Options 執行個體並設定設定選項,如以下範例所示:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .setHighlightColor(getColor(R.color.purple_300))
                                      .setHighlightCornerRadiusPx(resources.getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .setHighlightColor(getColor(R.color.purple_300))
                                     .setHighlightCornerRadiusPx(getResources().getDimensionPixelSize(R.dimen.drop_target_corner_radius))
                                     .build();

處理放置目標中的 EditText 元件

當目標包含可編輯的文字欄位時,DropHelper 也會控制放置目標中的焦點。

放置目標可以是單一資料檢視或檢視區塊階層。如果放置目標檢視區塊階層包含一或多個 EditText 元件,請將元件清單提供給 DropHelper.Options.Builder.addInnerEditTexts(EditText...),以確保放置目標醒目顯示功能和文字資料處理功能都正常運作。

DropHelper 會防止放置目標檢視區塊階層中的 EditText 元件在拖曳互動期間,從所含檢視區塊竊取焦點。

此外,如果拖曳 ClipData 包含文字和 URI 資料,DropHelper 則會選取放置目標的其中一個 EditText 元件來處理文字資料。選取項目是以下列優先順序為基礎:

  1. 捨棄 ClipDataEditText
  2. 包含文字遊標 (插入點) 的 EditText
  3. 提供給 DropHelper.Options.Builder.addInnerEditTexts(EditText...) 呼叫的第一個 EditText

如要將 EditText 設為預設文字資料處理常式,請將 EditText 做為 DropHelper.Options.Builder.addInnerEditTexts(EditText...) 呼叫的第一個引數傳遞。舉例來說,如果您的放置目標會處理圖片,但包含可編輯的文字欄位 T1T2T3,請將 T2 設為預設值,如下所示:

Kotlin

val options: DropHelper.Options = DropHelper.Options.Builder()
                                      .addInnerEditTexts(T2, T1, T3)
                                      .build()

Java

DropHelper.Options options = new DropHelper.Options.Builder()
                                     .addInnerEditTexts(T2, T1, T3)
                                     .build();

處理放置目標中的資料

DropHelper.configureView() 方法會接受您為了處理拖曳 ClipData 而建立的 OnReceiveContentListener。拖曳資料會提供給 ContentInfoCompat 物件中的事件監聽器。物件中會顯示文字資料。媒體 (例如圖片) 是以 URI 表示。

使用 DropHelper.configureView() 來設定下列類型的檢視畫面時,OnReceiveContentListener 也會處理使用者互動 (例如複製和貼上) 提供給放置目標的資料:

  • 所有檢視 (如果使用者執行 Android 12 以上版本)。
  • AppCompatEditText (如果使用者搭載的 Android 版本低於 Android 7.0)。

MIME 類型、權限和內容驗證

DropHelper 的 MIME 類型檢查是以拖曳 ClipDescription 為根據,而這由提供拖曳資料的應用程式建立。請驗證 ClipDescription,確保 MIME 類型設定正確無誤。

DropHelper 會要求拖曳 ClipData 中包含的內容 URI 所有存取權。詳情請參閱 DragAndDropPermissions。這類權限可讓您在處理拖曳資料時解析內容 URI。

DropHelper 不會在解析放置資料中的 URI 時,驗證內容供應器回傳的資料。檢查是否有空值,並驗證所有解析資料是否正確無誤。