com.tsoliveira.android.listeners.DragDropTouchListener.java Source code

Java tutorial

Introduction

Here is the source code for com.tsoliveira.android.listeners.DragDropTouchListener.java

Source

/*
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package com.tsoliveira.android.listeners;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.media.Image;
import android.os.Build;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.support.v4.view.ViewPropertyAnimatorListener;
import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.FrameLayout;
import android.widget.ImageView;

import com.tsoliveira.draggablerecyclerview.R;

/**
 * Implementation of RecyclerView.OnItemTouchListener that allows reordering items in RecyclerView by dragging and dropping.
 * Instance of this class should be added to RecylcerView using {@link android.support.v7.widget.RecyclerView#addOnItemTouchListener(android.support.v7.widget.RecyclerView.OnItemTouchListener)} method.
 * <p/>
 * <p/>
 * Use something like this:
 * <pre>
 *  {@code
 * dragDropTouchListener = new DragDropTouchListener(recyclerView, this) {
 *       @Override
 *       protected void onItemSwitch(RecyclerView recyclerView, int from, int to) {
 *           adapter.swapPositions(from, to);
 *           adapter.notifyItemChanged(to);
 *           adapter.notifyItemChanged(from);
 *
 *        @Override
 *        protected void onItemDrop(RecyclerView recyclerView, int position) {
 *       }
 *  };
 *  }
 *   recyclerView.addOnItemTouchListener(dragDropTouchListener);
 * }
 * </pre>
 * <p/>
 * Actual drag is started by calling {@link #startDrag()} somewhere later, for eg. in long touch listener
 */
public abstract class DragDropTouchListener implements RecyclerView.OnItemTouchListener {
    private static final String LOG_TAG = "DRAG-DROP";
    private static final int MOVE_DURATION = 300;

    private RecyclerView recyclerView;
    private Activity activity;
    private DisplayMetrics displayMetrics;

    private final int scrollAmount;
    private int downY = -1;
    private int downX = -1;
    private View mobileView;
    private int mobileViewStartY = -1;
    private int mobileViewCurrentPos = -1;
    private int activePointerId;
    private boolean dragging;
    private boolean enabled = true;

    public DragDropTouchListener(RecyclerView recyclerView, Activity activity) {
        this.recyclerView = recyclerView;
        this.activity = activity;
        this.displayMetrics = recyclerView.getResources().getDisplayMetrics();
        this.scrollAmount = (int) (50 / displayMetrics.density);

    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
        if (!enabled)
            return false;

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            return down(event);

        case MotionEvent.ACTION_MOVE:
            return dragging && move(event);

        case MotionEvent.ACTION_UP:
            return up(event);

        case MotionEvent.ACTION_CANCEL:
            return cancel(event);

        }
        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView view, MotionEvent event) {
        if (!dragging)
            return;

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE:
            move(event);
            break;

        case MotionEvent.ACTION_UP:
            up(event);
            break;

        case MotionEvent.ACTION_CANCEL:
            cancel(event);
            break;

        }
    }

    /**
     * Call this to indicate drag start
     */
    public void startDrag() {
        View viewUnder = recyclerView.findChildViewUnder(downX, downY);
        if (viewUnder == null)
            return;
        dragging = true;

        mobileViewCurrentPos = recyclerView.getChildLayoutPosition(viewUnder);

        int[] viewRawCoords = getViewRawCoords(viewUnder);
        mobileView = copyViewAsImageView(viewUnder);
        mobileView.setX(viewRawCoords[0]);
        mobileView.setY(viewRawCoords[1]);
        mobileViewStartY = viewRawCoords[1];

        // No ViewCompat.setZ() method exists yet, but it might someday.
        // http://developer.android.com/tools/support-library/index.html
        ViewCompat.setTranslationZ(mobileView, ViewCompat.getZ(viewUnder));

        // http://stackoverflow.com/a/27518160 -- Look for "4. [EDIT]"
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
            mobileView.setBackgroundColor(activity.getResources().getColor(android.R.color.white));

        ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
        activity.addContentView(mobileView, lp);
        mobileView.bringToFront();

        viewUnder.setVisibility(View.INVISIBLE);

    }

    private boolean down(MotionEvent event) {
        activePointerId = event.getPointerId(0);
        downY = (int) event.getY();
        downX = (int) event.getX();

        return false;
    }

    private boolean move(MotionEvent event) {
        if (activePointerId == -1) {
            return false;
        }

        int pointerIndex = event.findPointerIndex(activePointerId);
        int currentY = (int) event.getY(pointerIndex);
        int deltaY = currentY - downY;
        int mobileViewY = mobileViewStartY + deltaY;
        mobileView.setY(mobileViewY);

        switchViewsIfNeeded();
        scrollIfNeeded();
        return true;
    }

    private void switchViewsIfNeeded() {
        int pos = mobileViewCurrentPos;
        int abovePos = pos - 1;
        int belowPos = pos + 1;

        View aboveView = getViewByPosition(abovePos);
        View belowView = getViewByPosition(belowPos);

        int mobileViewY = (int) mobileView.getY() - getViewRawCoords(recyclerView)[1];

        if (aboveView != null && aboveView.getTop() > -1 && mobileViewY < aboveView.getTop()) {
            doSwitch(aboveView, pos, abovePos);
        }
        if (belowView != null && belowView.getTop() > -1 && mobileViewY > belowView.getTop()) {
            doSwitch(belowView, pos, belowPos);
        }

    }

    private void doSwitch(final View switchView, final int originalViewPos, final int switchViewPos) {
        View originalView = getViewByPosition(originalViewPos);
        int switchViewTop = switchView.getTop();
        int originalViewTop = originalView.getTop();
        int delta = originalViewTop - switchViewTop;

        onItemSwitch(recyclerView, originalViewPos, switchViewPos);

        switchView.setVisibility(View.INVISIBLE);
        originalView.setVisibility(View.VISIBLE);

        originalView.setTranslationY(-delta);
        originalView.animate().translationYBy(delta).setDuration(MOVE_DURATION);

        mobileViewCurrentPos = switchViewPos;

    }

    private boolean up(MotionEvent event) {
        if (dragging) {
            onItemDrop(recyclerView, mobileViewCurrentPos);
        }
        reset();
        return false;
    }

    private boolean cancel(MotionEvent event) {
        reset();
        return false;
    }

    private void reset() {
        //Animate mobile view back to original position
        final View originalView = getViewByPosition(mobileViewCurrentPos);
        if (originalView != null && mobileView != null) {
            float y = getViewRawCoords(originalView)[1];
            mobileView.animate().y(y).setDuration(MOVE_DURATION).setListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {

                    if (mobileView != null) {
                        ViewCompat.animate(mobileView).z(ViewCompat.getZ(originalView)).setDuration(MOVE_DURATION)
                                .setListener(new ViewPropertyAnimatorListener() {
                                    @Override
                                    public void onAnimationStart(View view) {
                                    }

                                    @Override
                                    public void onAnimationEnd(View view) {
                                        originalView.setVisibility(View.VISIBLE);
                                        ViewGroup parent = (ViewGroup) mobileView.getParent();
                                        parent.removeView(mobileView);
                                        mobileView = null;
                                    }

                                    @Override
                                    public void onAnimationCancel(View view) {
                                    }
                                }).start();
                    }
                }
            });

        }

        dragging = false;
        mobileViewStartY = -1;
        mobileViewCurrentPos = -1;

    }

    private View getViewByPosition(int position) {
        RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForLayoutPosition(position);
        return viewHolder == null ? null : viewHolder.itemView;
    }

    private boolean scrollIfNeeded() {

        int height = recyclerView.getHeight();
        int hoverViewTop = (int) mobileView.getY() - getViewRawCoords(recyclerView)[1];
        int hoverHeight = mobileView.getHeight();

        if (hoverViewTop <= 0) {
            recyclerView.scrollBy(0, -scrollAmount);
            return true;
        }

        if (hoverViewTop + hoverHeight >= height) {
            recyclerView.scrollBy(0, scrollAmount);
            return true;
        }

        return false;
    }

    //Creates screenshot of a view
    private Bitmap copyViewAsBitmap(View v) {
        //Clear ripple effect to not get into screenshot,
        // need something more clever here
        if (v instanceof FrameLayout) {
            FrameLayout frameLayout = (FrameLayout) v;
            Drawable foreground = frameLayout.getForeground();
            if (foreground != null)
                foreground.setVisible(false, false);
        } else {
            if (v.getBackground() != null)
                v.getBackground().setVisible(false, false);
        }

        Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        v.draw(canvas);

        return bitmap;
    }

    private ImageView copyViewAsImageView(View v) {
        ImageView imageView = new ImageView(activity);
        imageView.setImageBitmap(copyViewAsBitmap(v));

        return imageView;
    }

    private int[] getViewRawCoords(View locateView) {
        View globalView = activity.findViewById(android.R.id.content);
        int topOffset = displayMetrics.heightPixels - globalView.getMeasuredHeight();
        int[] loc = new int[2];
        locateView.getLocationOnScreen(loc);
        loc[1] = loc[1] - topOffset;
        return loc;
    }

    /**
     * Enable/disable drag/drop
     *
     * @param enabled
     */
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    /**
     * Implementation usually do 2 things: change positions of items in RecyclerView.Adapter and notify it about changes
     *
     * @param recyclerView view the item is being dragged in
     * @param from         original (start) drag position within adapter
     * @param to           new drag position withing adapter
     */
    protected abstract void onItemSwitch(RecyclerView recyclerView, int from, int to);

    /**
     * Item is dropped at given position
     *
     * @param recyclerView view the item is being dropped in
     * @param position     position of a drop within adapter
     */
    protected abstract void onItemDrop(RecyclerView recyclerView, int position);

}