DragDropTouchListener.java
|
/*
* 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.marshalchen.ultimaterecyclerview;
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.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.FrameLayout;
import android.widget.ImageView;
/**
* 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 RecyclerView#addOnItemTouchListener(RecyclerView.OnItemTouchListener)} method.
* Use something like this:
* 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);
*
* 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 = 150;
private RecyclerView recyclerView;
private Activity activity;
private Drawable dragHighlight;
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);
this.dragHighlight = recyclerView.getResources().getDrawable(R.drawable.drag_frame);
}
public DragDropTouchListener(RecyclerView recyclerView, Activity activity, Drawable dragHighlight) {
this(recyclerView, activity);
this.dragHighlight = dragHighlight;
}
@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.getChildPosition(viewUnder);
int[] viewRawCoords = getViewRawCoords(viewUnder);
mobileView = copyViewAsImage(viewUnder);
mobileView.setX(viewRawCoords[0]);
mobileView.setY(viewRawCoords[1]);
mobileViewStartY = viewRawCoords[1];
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();
if (aboveView != null && aboveView.getTop() > -1 && mobileViewY < aboveView.getTop()) {
Log.d(LOG_TAG, String.format("Got aboveView with top = %s, for position = %s, %s", aboveView.getTop(), abovePos, aboveView));
doSwitch(aboveView, pos, abovePos);
}
if (belowView != null && belowView.getTop() > -1 && mobileViewY > belowView.getTop()) {
Log.d(LOG_TAG, String.format("Got belowView with top = %s, for position = %s, %s", belowView.getTop(), belowPos, belowView));
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 view = getViewByPosition(mobileViewCurrentPos);
if (view != null && mobileView != null) {
float y = getViewRawCoords(view)[1];
mobileView.animate().y(y).setDuration(MOVE_DURATION).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.VISIBLE);
if (mobileView != null) {
ViewGroup parent = (ViewGroup) mobileView.getParent();
parent.removeView(mobileView);
mobileView = null;
}
}
});
}
dragging = false;
mobileViewStartY = -1;
mobileViewCurrentPos = -1;
}
private View getViewByPosition(int position) {
RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForPosition(position);
return viewHolder == null ? null : viewHolder.itemView;
}
private boolean scrollIfNeeded() {
int height = recyclerView.getHeight();
int hoverViewTop = (int) mobileView.getY();
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 ImageView copyViewAsImage(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.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
v.draw(canvas);
//Drag highlight, usually border
if (dragHighlight != null) {
dragHighlight.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
dragHighlight.draw(canvas);
}
ImageView imageView = new ImageView(recyclerView.getContext());
imageView.setImageBitmap(bitmap);
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 na
*/
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);
public void setCustomDragHighlight(Drawable dragHighlight) {
this.dragHighlight = dragHighlight;
}
}