Skip to content
This repository has been archived by the owner on Dec 1, 2017. It is now read-only.

Implemented reordering (through drag and drop). #149

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.lucasr.twowayview.widget;

/**
* Listen for changes in a {@link TwoWayView}'s reordering state.
*/
public interface OnReorderingListener {
/**
* Called when a {@link TwoWayView} has started reordering.
*/
public void onStartReordering();

/**
* Called when a {@link TwoWayView} has stopped reordering.
*/
public void onStopReordering();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.lucasr.twowayview.widget;

/**
* Any {@link android.support.v7.widget.RecyclerView.Adapter} that is passed to one of
* {@link TwoWayView#setReorderableAdapter} or {@link TwoWayView#swapReorderableAdapter}
* should implement {@code ReorderableAdapter} (or its subinterfaces).
*/
public interface ReorderableAdapter {
/**
* Called when an item that was being dragged is dropped. Most implementations will want to
* remove the item from the dataset at {@code from} and reinsert it at {@code to}.
* @param from the position the item was dragged from
* @param to the position the item was dropped on
*/
public void onItemDropped(int from, int to);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.lucasr.twowayview.widget;

import android.view.View;

/**
* A {@link ReorderableAdapter} that allows for overriding the decoration applied to an item at a "drop position."
* If an item is dragged over another, that view will slide over 1 position, leaving an "empty space."
* The default implementation calls {@link View#setVisibility} with {@link View#INVISIBLE} for the {@code View}
* at the drop position, and then calls {@code View.setVisibility} with {@link View#VISIBLE} when it is no longer
* at the drop position. If an implementation uses a {@code View}'s visibility, or wants to apply custom animations
* to this transition, its {@link android.support.v7.widget.RecyclerView.Adapter} should implement
* this instead of {@link ReorderableAdapter}.
* @see org.lucasr.twowayview.widget.ReorderableAdapter
*/
public interface ReorderableAdapterViewDecorator extends ReorderableAdapter {
/**
* Alter the {@code View}'s appearance when its position is the "drop position."
* @param view the {@link View} that the dragged item is currently over
*/
public void applyDropPositionDecoration(View view);

/**
* Undo whatever alterations were made in {@link ReorderableAdapterViewDecorator#applyDropPositionDecoration}.
* This is called everytime its position is invalidated; not just after {@code applyDropPositionDecoration},
* so it may be best to check if the alterations were already undone.
* @param view a {@link View} that the dragged item is <b>not</b> currently over
*/
public void undoDropPositionDecoration(View view);
}
228 changes: 228 additions & 0 deletions layouts/src/main/java/org/lucasr/twowayview/widget/Reorderer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package org.lucasr.twowayview.widget;

import android.annotation.TargetApi;
import android.content.ClipData;
import android.content.ClipDescription;
import android.os.Build;
import android.os.Handler;
import android.support.v7.widget.RecyclerView;
import android.view.DragEvent;
import android.view.View;
import android.widget.AbsListView;
import org.lucasr.twowayview.TwoWayLayoutManager;

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
/*package*/ final class Reorderer implements View.OnDragListener {
/**
* Allows for system wide drag-drop compatability.
* http://developer.android.com/guide/topics/ui/drag-drop.html#HandleDrop
*/
/*package*/ final static String MIME_TYPE = "x-org-lucasr-twowayview-widget/item";

private final TwoWayView twv;

private boolean mIsReordering = false;

/**
* Allows for system wide drag-drop compatability.
* http://developer.android.com/guide/topics/ui/drag-drop.html#HandleDrop
*/
private ClipData mMyClipData;

/**
* Creates a new {@code Reorderer} for a given {@link TwoWayView}.
*/
/*package*/ Reorderer(TwoWayView twv) {
this.twv = twv;
this.twv.setOnDragListener(this);
this.mMyClipData = new ClipData(new ClipDescription("", new String[] {MIME_TYPE}), new ClipData.Item(""));
}

/**
* As per this bug - https://code.google.com/p/android/issues/detail?id=25073
*/
/*package*/ boolean dispatchDragEvent(DragEvent ev) {
boolean r = twv.superDispatchDragEvent(ev);
if (r && (ev.getAction() == DragEvent.ACTION_DRAG_STARTED
|| ev.getAction() == DragEvent.ACTION_DRAG_ENDED)){
// If we got a start or end and the return value is true, our
// onDragEvent wasn't called by ViewGroup.dispatchDragEvent
// So we do it here.
onDragEvent(ev);
return true;
}
else {
return true;
}
}

/*package*/ boolean onDragEvent(DragEvent ev) {
return onDrag(null, ev);
}

private Handler scrollHandler = new Handler();
private int scrollDistance;
private boolean isScrolling = false;
private Runnable scrollRunnable = new Runnable() {
@Override
public void run() {
if(isScrolling) {
if(twv.getOrientation() == TwoWayLayoutManager.Orientation.VERTICAL) {
twv.smoothScrollBy(0, scrollDistance);
}
else {
twv.smoothScrollBy(scrollDistance, 0);
}
scrollHandler.postDelayed(this, 250);
}
}
};

private int lastKnownPosition = TwoWayView.NO_POSITION;
@Override
public boolean onDrag(View v, DragEvent ev) {
ClipDescription desc = ev.getClipDescription();
// if this isn't our dragged item, ignore it
if(desc != null && !desc.hasMimeType(MIME_TYPE)) {
return false;
}

int x = (int) ev.getX();
int y = (int) ev.getY();

RecyclerView.ViewHolder viewHolderToSendToAdapter = null;

if(ev.getAction() == DragEvent.ACTION_DRAG_ENTERED) {
// reset our last known position for a new drag
lastKnownPosition = AbsListView.INVALID_POSITION;
}
else if(ev.getAction() == DragEvent.ACTION_DRAG_LOCATION) {
boolean isVertical = (twv.getOrientation() == TwoWayLayoutManager.Orientation.VERTICAL);
int pointer = (isVertical ? y : x);
int bottomBound = (isVertical ? twv.getHeight() : twv.getWidth());
int bottomOffset = bottomBound - pointer;

View firstVisibleView = twv.getChildAt(twv.getFirstVisiblePosition());
scrollDistance = bottomBound / 8;
int scrollThreshold = scrollDistance / 2;
if(firstVisibleView != null) {
int bound = (isVertical ? firstVisibleView.getHeight() : firstVisibleView.getWidth());
scrollDistance = ((bound * 2) + (bound / 2));
scrollThreshold = bound / 8;
}

if(pointer <= scrollThreshold && bottomOffset >= scrollThreshold) {
scrollDistance = -scrollDistance;
if(!isScrolling) {
scrollHandler.post(scrollRunnable);
isScrolling = true;
}
}
else if(pointer >= scrollThreshold && bottomOffset <= scrollThreshold) {
if(!isScrolling) {
scrollHandler.post(scrollRunnable);
isScrolling = true;
}
}
else {
scrollHandler.removeCallbacks(scrollRunnable);
isScrolling = false;
}

// get the view that the dragged item is over
View viewAtCurrentPosition = twv.findChildViewUnder(ev.getX(), ev.getY());
if(viewAtCurrentPosition != null) {
// and get its position
int currentPosition = twv.getChildPosition(viewAtCurrentPosition);
// this is an optimization so that we don't keep sending the same ViewHolder to the adapter
// for every pixel that it moves (which would be redundant anyway)
if(currentPosition != lastKnownPosition) {
// if it's a valid position, use it to get the ViewHolder to send to the adapter
if(currentPosition != TwoWayView.NO_POSITION) {
viewHolderToSendToAdapter = twv.findViewHolderForPosition(currentPosition);
}
// this position is now our known position
lastKnownPosition = currentPosition;
}
}
else { // if we can't get the view we're over, we don't have a known position
lastKnownPosition = TwoWayView.NO_POSITION;
}
}
else if(ev.getAction() == DragEvent.ACTION_DROP) {
// stop scrolling
scrollHandler.removeCallbacks(scrollRunnable);
isScrolling = false;

// get the view that the dragged item is over
View viewAtCurrentPosition = twv.findChildViewUnder(ev.getX(), ev.getY());
if(viewAtCurrentPosition != null) {
// and get its position
int currentPosition = twv.getChildPosition(viewAtCurrentPosition);
// if it's a valid position, use it to get the ViewHolder to send to the adapter
if(currentPosition != TwoWayView.NO_POSITION) {
viewHolderToSendToAdapter = twv.findViewHolderForPosition(currentPosition);
}
}
// reset our last known position since we dropped
lastKnownPosition = TwoWayView.NO_POSITION;
}
else if(ev.getAction() == DragEvent.ACTION_DRAG_ENDED) {
setIsReordering(false);

// stop scrolling
scrollHandler.removeCallbacks(scrollRunnable);
isScrolling = false;

// reset our last known position since we're done
lastKnownPosition = TwoWayView.NO_POSITION;
}

ReordererAdapterDecorator adapter = twv.getReordererAdapter();
return adapter != null && adapter.onDrag(viewHolderToSendToAdapter, ev);
}

/*package*/ boolean isReordering() {
return mIsReordering;
}

/*package*/ void setIsReordering(boolean isReordering) {
this.mIsReordering = isReordering;
OnReorderingListener reorderingListener = twv.getOnReorderingListener();
if(reorderingListener != null) {
if(isReordering) {
reorderingListener.onStartReordering();
}
else {
reorderingListener.onStopReordering();
}
}
}

/*package*/ final boolean startReorder(int position) {
if(isReordering()) {
throw new IllegalStateException("Cannot start reordering if a reordering operation is already in progress");
}
if(twv.getAdapter() == null) {
throw new IllegalStateException("Cannot start a reorder operation if there is no adapter set");
}
if(position < 0 || position >= twv.getAdapter().getItemCount()) {
throw new IndexOutOfBoundsException("Cannot start a reorder operation if the position is out of the bounds of the adapter");
}

// TODO: custom DragShadowBuilder

View.DragShadowBuilder dragShadowBuilder;
View viewAtReorderPosition = twv.findViewHolderForPosition(position).itemView;
dragShadowBuilder = new View.DragShadowBuilder(viewAtReorderPosition);

boolean success = twv.startDrag(mMyClipData, dragShadowBuilder, null, 0);

if(success) {
setIsReordering(true);
twv.getReordererAdapter().startReordering(position);
}

return success;
}
}
Loading