8

What I want to achieve:

I want to create a drag and drop functionality in Android. I'd like to use a specific layout (different from the dragged object itself) as a drag shadow.

What result I'm getting instead:

Neither of my approaches works as expected - I end up with no visible drag shadow at all (although the target does receive the drop).

What I tried:

I tried

  • inflating the drag_item layout in the activity, then passing it as an argument to the shadow builder's constructor

and

  • inflating the drag_item layout in the shadow builder's onDrawShadow method, then drawing it on the canvas

Layouts:

My activity layout:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:id="@+id/container"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              tools:context="com.example.app.DragDropTestActivity"
              tools:ignore="MergeRootFrame">
    <TextView
        android:id="@+id/tvReceiver"
        android:text="Drop here"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/btnDragged"
        android:layout_height="wrap_content"
        android:text="Drag me"
        android:layout_width="match_parent"/>
</LinearLayout>

The layout I want to use as a drag shadow:

dragged_item.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Dragged Item"/>
</LinearLayout>

Source code:

Here's the code with both approaches (represented by 1, BuilderOne and 2, BuilderTwo, respectively):

package com.example.app;

import android.graphics.Canvas;
import android.graphics.Point;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;

public class DragDropTestActivity extends ActionBarActivity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_drag_drop_test);
        Button dragged = (Button) findViewById(R.id.btnDragged);

        dragged.setOnTouchListener(
            new View.OnTouchListener()
            {
                @Override
                public boolean onTouch(View v, MotionEvent event)
                {
                    if (event.getAction() != MotionEvent.ACTION_DOWN) {
                        return false;
                    }
                    LayoutInflater inflater = getLayoutInflater();

                    int approach = 1;    
                    // both approaches fail
                    switch (approach) {
                        case 1: {
                            View draggedItem = inflater.inflate(R.layout.dragged_item, null);
                            BuilderOne builder = new BuilderOne(draggedItem);
                            v.startDrag(null, builder, null, 0);
                            break;
                        }
                        case 2: {
                            BuilderTwo builder = new BuilderTwo(inflater, v);
                            v.startDrag(null, builder, null, 0);
                            break;
                        }
                    }

                    return true;
                }
            });
    }

My BuilderOne class:

    public static class BuilderOne extends View.DragShadowBuilder
    {
        public BuilderOne(View view)
        {
            super(view);
        }

        @Override
        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint)
        {
            super.onProvideShadowMetrics(
                shadowSize,
                shadowTouchPoint);
        }
    }

And BuilderTwo class:

    public static class BuilderTwo extends View.DragShadowBuilder
    {
        final LayoutInflater inflater;

        public BuilderTwo(LayoutInflater inflater, View view)
        {
            super(view);
            this.inflater = inflater;
        }

        @Override
        public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint)
        {
            super.onProvideShadowMetrics(
                shadowSize,
                shadowTouchPoint);
        }

        @Override
        public void onDrawShadow(Canvas canvas)
        {
            final View draggedItem = inflater.inflate(R.layout.dragged_item, null);
            if (draggedItem != null) {
                draggedItem.draw(canvas);
            }
        }
    }
}

Question:

What do I do wrong?

Update:

Bounty added.

Konrad Morawski
  • 8,307
  • 7
  • 53
  • 91

2 Answers2

2

Kurty is correct in that you shouldn't need to subclass DragShadowBuilder in this case. My thought is that the view you're passing to the DragShadowBuilder doesn't actually exist in the layout, and therefore it doesn't render.

Rather than passing null as the second argument to inflater.inflate, try actually adding the inflated View to the hierarchy somewhere, and then passing it to a regular DragShadowBuilder:

View dragView = findViewById(R.id.dragged_item);
mDragShadowBuilder = new DragShadowBuilder(dragView);
v.startDrag(null, mDragShadowBuilder, null, 0);

EDIT

I'm aware that having the dragged_item view being rendered all the time isn't what you want, but if it works then at least we know where the problem is and can look for a solution to that instead!

Matt Rowland
  • 379
  • 3
  • 16
  • That's some idea. Maybe I could add it somewhere where it's not visible to the user (but still part of the layout)? It would still be a hack, but if it does the trick... I'll try that when I get home – Konrad Morawski Mar 31 '14 at 08:58
  • Another idea (and the way I'm currently doing my custom DropShadow) is to only use the `View` to set the dimensions of the `Canvas` used in `onDrawShadow`, and then reconstruct the appearance manually in an overridden `onDrawShadow`. You might be able to access the text stored in the inflated `View` with `CharSequence text = ((TextView) getView()).getText()` and then use the result of that in [Canvas.drawText()](http://developer.android.com/reference/android/graphics/Canvas.html) – Matt Rowland Mar 31 '14 at 10:01
  • Did this end up helping you out, @KonradMorawski? – Matt Rowland Apr 24 '14 at 13:24
  • Sorry. Well I didn't have time to dig deeper and I ended up with a compromise, taking out a *part* of the dragged object and using this as my shadow. Good enough, but not really a custom view. You're probably correct that the root problem is the shadow only exists in the abstract and thus never gets "actually" inflated = has no dimensions. I didn't try the trick with a tucked away view. I +1 your answer as it points in the right direction, but I can't accept it, because it doesn't answer the problem fully, it's bit of a hack. I still believe there's some proper way to do it. Thanks anyhow – Konrad Morawski Apr 25 '14 at 13:35
  • It seems like you could render the view as a bitmap, then use the bitmap to instantiate the DragShadowBuilder. Render a view as a bitmap looks something like: – dkneller May 10 '14 at 02:14
  • To finish this thought, here is a link to do that rendering: http://stackoverflow.com/questions/4346710/bitmap-from-view-not-displayed-on-android – dkneller May 10 '14 at 02:24
  • For the record, I tried this approach and got it working. I have a view (drag_shadow_base) sitting behind the view the user actually sees. drag_shadow_base is what I then pass in to the drag builder. There's overdraw here, but hey, it works. – Kurtis Nusbaum Mar 18 '15 at 19:59
-2

Simply put it, you only need this:

private final class TouchListener implements View.OnTouchListener {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            v.startDrag(ClipData.newPlainText("", ""), new View.DragShadowBuilder(v), v, 0);
        }
        return true;
    }
}

(You don't necessarily need the BuilderOne and BuilderTwo class)

Kurty
  • 475
  • 2
  • 16
  • Your solution misses the point. It will get me the default shadow (`btnDragged` itself). And not `dragged_item`, which I wanted to use instead. – Konrad Morawski Mar 08 '14 at 20:44