تنفيذ السحب والإفلات مع طرق العرض

يمكنك تنفيذ عملية السحب والإفلات في طرق العرض من خلال الاستجابة للأحداث التي قد تؤدي إلى بدء سحب والاستجابة لأحداث الإفلات واستخدامها.

بدء السحب

يبدأ المستخدم عملية السحب بإيماءة، عادةً عن طريق لمس عنصر يريد سحبه أو النقر عليه مع الاستمرار.

لمعالجة هذا الإجراء في 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() واستخدام الطُرق من نوع MIME في السمة ClipDescription المعروضة لمعرفة ما إذا كان بإمكان المستمع قبول البيانات التي يتم سحبها.

    إذا كانت عملية السحب والإفلات لا تمثل حركة البيانات، فقد يكون هذا غير ضروري.

  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_ENTERED أو ACTION_DRAG_LOCATION، يمكن للمستمع تغيير مظهر View للإشارة إلى أنّ العرض يمثّل استهداف انخفاض مُحتمَل.
  • يحتوي حدث من نوع الإجراء ACTION_DRAG_LOCATION على بيانات صالحة لكل من getX() وgetY() بما يتوافق مع الموقع الجغرافي لنقطة الاتصال. ويمكن للمستمع استخدام هذه المعلومات لتغيير مظهر View عند نقطة اللمس أو لتحديد الموضع الدقيق الذي يمكن للمستخدم فيه إزالة المحتوى.
  • استجابةً لـ ACTION_DRAG_EXITED، على المستمع إعادة ضبط أي تغييرات في المظهر يتم تطبيقها استجابةً للسمة ACTION_DRAG_ENTERED أو ACTION_DRAG_LOCATION. يشير ذلك إلى المستخدم أنّ View لم يعُد هدفًا قريبًا لانخفاض عدد الزيارات.

الردّ على الانخفاض المفاجئ

عندما يرفع المستخدم ظل السحب على View، أبلغت السمة View في السابق عن إمكانية قبول المحتوى الذي يتم سحبه، يرسل النظام حدث السحب إلى View بنوع الإجراء ACTION_DROP.

على أداة معالجة حدث السحب إجراء ما يلي:

  1. يمكنك طلب الإجراء getClipData() للحصول على العنصر ClipData الذي تم توفيره في الأصل في الطلب إلى startDragAndDrop()، ثم معالجة البيانات. إذا كانت عملية السحب والإفلات لا تمثل حركة البيانات، فلن يكون ذلك ضروريًا.

  2. اعرض القيمة المنطقية true للإشارة إلى أنّه تمت معالجة الانخفاض المفاجئ بنجاح، أو false إذا لم تتم معالجته. تصبح القيمة التي تعرضها هي القيمة التي تعرضها getResult() لحدث ACTION_DRAG_ENDED النهائي. إذا لم يرسل النظام حدث ACTION_DROP، ستكون القيمة التي يعرضها getResult() لحدث ACTION_DRAG_ENDED هي false.

بالنسبة إلى حدث ACTION_DROP، يستخدم getX() وgetY() نظام إحداثيات View الذي يتلقى الانخفاض لعرض الموضعين X وY لنقطة الاتصال في وقت الانخفاض.

على الرغم من قدرة المستخدم على إطلاق ظل السحب على 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);
    }
}