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

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

بدء سحب

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

لمعالجة هذا الأمر في 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);
    }
}