實作 View 的拖曳功能

您可以藉由回應事件,在檢視畫面中實作拖曳程序 可能觸發拖曳開始、回應和消耗放置事件。

開始拖曳

使用者以手勢開始拖曳,通常是輕觸或點選, 然後按住要拖曳的項目

如要在 View 中處理這種情況,請建立 ClipData 物件和 ClipData.Item 物件: 要移動的資料做為 ClipData 的一部分,請提供 儲存在 ClipDescription 物件 ClipData。適用於並不代表 資料移動時,建議您使用 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. We use this imageView object
        // to create the default builder.
        val myShadow = View.DragShadowBuilder(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. We use this imageView object
    // to create the default builder.
    View.DragShadowBuilder myShadow = new View.DragShadowBuilder(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;
});

回應拖曳開始事件

在拖曳作業中,系統會把拖曳事件分派到目前版面配置中 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_LOCATION每次接觸點移動至觸發事件為止 就會收到 ACTION_DRAG_EXITED 事件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_DROP 拖曳到 View

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

  1. 呼叫 getClipData() 即可取得最初的 ClipData 物件 所提供的參數 startDragAndDrop()敬上 以及處理資料如果拖曳作業不代表資料 這不需要的話

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

如果是 ACTION_DROP 事件,getX()getY() 會使用 接收捨棄值的 View,傳回 與主要接觸點相比。

使用者可以透過拖曳事件的 View 釋放拖曳陰影 事件監聽器未收到拖曳事件、應用程式使用者介面的空白區域, 在應用程式以外的區域,Android 不會傳送含有動作的事件 輸入 ACTION_DROP,且只會傳送 ACTION_DRAG_ENDED 事件。

回應拖曳結束事件

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

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

  1. 如果事件監聽器在作業期間變更外觀,應重設 重新顯示應用程式的預設外觀,以視覺化的方式告知使用者 作業完成。
  2. 事件監聽器可以選擇呼叫 getResult() 以進一步瞭解作業。如果事件監聽器回傳 true 以回應動作事件 輸入 ACTION_DROP,然後 getResult() 會傳回布林值 true。所有其他大小 getResult() 會傳回布林值 false,包括系統 但不會傳送 ACTION_DROP 事件。
  3. 如要表示放置作業成功完成,事件監聽器: 應將布林值 true 傳回系統。在未傳回 false 的情況下, 顯示投射陰影返回來源的視覺提示,可能會建議 使用者表示作業失敗。

回應拖曳事件:範例

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

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;

});

自訂拖曳陰影

您可以覆寫自訂 myDragShadowBuilder,方法是覆寫 View.DragShadowBuilder。下列程式碼片段會建立一個小型的 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);
    }
}