Java tutorial
/* * 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); }