From 13d50ce7a8270b4709f1698c9b38961d92119009 Mon Sep 17 00:00:00 2001 From: Eliezer Graber Date: Wed, 29 Oct 2014 03:40:54 -0400 Subject: [PATCH 1/2] Implemented reordering (through drag and drop). --- .../widget/OnReorderingListener.java | 16 ++ .../twowayview/widget/ReorderableAdapter.java | 16 ++ .../ReorderableAdapterViewDecorator.java | 29 +++ .../lucasr/twowayview/widget/Reorderer.java | 223 ++++++++++++++++ .../widget/ReordererAdapterDecorator.java | 237 ++++++++++++++++++ .../lucasr/twowayview/widget/TwoWayView.java | 152 ++++++++++- .../twowayview/sample/LayoutAdapter.java | 12 +- .../twowayview/sample/LayoutFragment.java | 14 +- 8 files changed, 687 insertions(+), 12 deletions(-) create mode 100644 layouts/src/main/java/org/lucasr/twowayview/widget/OnReorderingListener.java create mode 100644 layouts/src/main/java/org/lucasr/twowayview/widget/ReorderableAdapter.java create mode 100644 layouts/src/main/java/org/lucasr/twowayview/widget/ReorderableAdapterViewDecorator.java create mode 100644 layouts/src/main/java/org/lucasr/twowayview/widget/Reorderer.java create mode 100644 layouts/src/main/java/org/lucasr/twowayview/widget/ReordererAdapterDecorator.java diff --git a/layouts/src/main/java/org/lucasr/twowayview/widget/OnReorderingListener.java b/layouts/src/main/java/org/lucasr/twowayview/widget/OnReorderingListener.java new file mode 100644 index 0000000..288846f --- /dev/null +++ b/layouts/src/main/java/org/lucasr/twowayview/widget/OnReorderingListener.java @@ -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(); +} diff --git a/layouts/src/main/java/org/lucasr/twowayview/widget/ReorderableAdapter.java b/layouts/src/main/java/org/lucasr/twowayview/widget/ReorderableAdapter.java new file mode 100644 index 0000000..16fc9f1 --- /dev/null +++ b/layouts/src/main/java/org/lucasr/twowayview/widget/ReorderableAdapter.java @@ -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); +} diff --git a/layouts/src/main/java/org/lucasr/twowayview/widget/ReorderableAdapterViewDecorator.java b/layouts/src/main/java/org/lucasr/twowayview/widget/ReorderableAdapterViewDecorator.java new file mode 100644 index 0000000..e54f7c5 --- /dev/null +++ b/layouts/src/main/java/org/lucasr/twowayview/widget/ReorderableAdapterViewDecorator.java @@ -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 not currently over + */ + public void undoDropPositionDecoration(View view); +} diff --git a/layouts/src/main/java/org/lucasr/twowayview/widget/Reorderer.java b/layouts/src/main/java/org/lucasr/twowayview/widget/Reorderer.java new file mode 100644 index 0000000..fd8850d --- /dev/null +++ b/layouts/src/main/java/org/lucasr/twowayview/widget/Reorderer.java @@ -0,0 +1,223 @@ +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; + +@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) { + twv.smoothScrollBy(scrollDistance, 500); + 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) { + // TODO: fix scrolling with the dragged item - this is semi FUBAR - need to handle orientation as well + int twvHeight = twv.getHeight(); + int[] globalCoords = new int[2]; + twv.getLocationInWindow(globalCoords); + int bottomOffset = twvHeight - y; + + View firstVisibleView = twv.getChildAt(0); + scrollDistance = twvHeight / 8; + int scrollThreshold = scrollDistance / 2; + if(firstVisibleView != null) { + int height = firstVisibleView.getHeight(); + scrollDistance = ((height * 2) + (height / 2)); + scrollThreshold = height / 8; + } + + if(y <= scrollThreshold && bottomOffset >= scrollThreshold) { + scrollDistance = -scrollDistance; + if(!isScrolling) { + scrollHandler.post(scrollRunnable); + isScrolling = true; + } + } + else if(y >= 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; + } +} diff --git a/layouts/src/main/java/org/lucasr/twowayview/widget/ReordererAdapterDecorator.java b/layouts/src/main/java/org/lucasr/twowayview/widget/ReordererAdapterDecorator.java new file mode 100644 index 0000000..7461a83 --- /dev/null +++ b/layouts/src/main/java/org/lucasr/twowayview/widget/ReordererAdapterDecorator.java @@ -0,0 +1,237 @@ +package org.lucasr.twowayview.widget; + +import android.annotation.TargetApi; +import android.content.ClipDescription; +import android.os.Build; +import android.support.v7.widget.RecyclerView; +import android.view.DragEvent; +import android.view.View; +import android.view.ViewGroup; + +/** + * A decorator for {@link android.support.v7.widget.RecyclerView.Adapter}. It adds reordering capabilities. + * @param the type of ViewHolder the wrapped {@code Adapter} holds + * (inferred from {@link ReordererAdapterDecorator#decorateAdapter}) + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB) +/*package*/ final class ReordererAdapterDecorator extends RecyclerView.Adapter { + private static final int INVALID = -1; + + // the Adapter that we're decorating + private final RecyclerView.Adapter decoratedAdapter; + + private int mReorderPosition = INVALID; + private int mDropPosition = INVALID; + + private ReordererAdapterDecorator(final RecyclerView.Adapter adapterToDecorate) { + this.decoratedAdapter = adapterToDecorate; + } + + /** + * Returns a {@code ReordererAdapterDecorator} that decorates the + * {@link android.support.v7.widget.RecyclerView.Adapter}. + * {@code adapterToDecorate} cannot be {@code null} and must be-a {@link ReorderableAdapter}. + * @param adapterToDecorate the {@code Adapter} to decorate + * @param the type of ViewHolder that {@code adapterToDecorate} holds + * @throws java.lang.NullPointerException if {@code adapterToDecorate} is {@code null} + * @throws java.lang.IllegalArgumentException if {@code adapterToDecorate} is not a {@code ReorderableAdapter} + */ + /*package*/ static ReordererAdapterDecorator + decorateAdapter(final RecyclerView.Adapter adapterToDecorate) { + if(adapterToDecorate == null) { + throw new NullPointerException("reorderable adapter cannot be null"); + } + if(!(adapterToDecorate instanceof ReorderableAdapter)) { + throw new IllegalArgumentException("reorderable adapter must implement ReorderableAdapter"); + } + return new ReordererAdapterDecorator(adapterToDecorate); + } + + /** + * Called when a reordering operation is started on the {@link TwoWayView}. + * @param position the position that the reordering operation was started on + */ + /*package*/ void startReordering(int position) { + mReorderPosition = position; + mDropPosition = position; + decoratedAdapter.notifyItemChanged(position); + } + + private boolean isReordering() { + return mReorderPosition != INVALID; + } + + /** + * Notifies any observers about the range of items that were changed. + */ + private void notifyPositionRangeChanged(int initialRangePosition, int currentRangePosition) { + if(currentRangePosition == INVALID) { + return; + } + + if(initialRangePosition == INVALID) { + initialRangePosition = currentRangePosition; + } + + if(initialRangePosition == currentRangePosition) { + decoratedAdapter.notifyItemChanged(currentRangePosition); + return; + } + + int max = Math.max(initialRangePosition, currentRangePosition); + int min = Math.min(initialRangePosition, currentRangePosition); + + int changedCount = (max + 1) - min; + decoratedAdapter.notifyItemRangeChanged(min, changedCount); + } + + /** + * Receive drag events from {@link Reorderer#onDrag}. + * @param holder the {@link android.support.v7.widget.RecyclerView.ViewHolder} at this drag position, or null + * @param e the {@link DragEvent} + * @param the type of {@code ViewHolder} + * @return whether the drag event was handled + */ + /*package*/ boolean onDrag(T holder, DragEvent e) { + ClipDescription desc = e.getClipDescription(); + // if this isn't our dragged item, ignore it + if(desc != null && !desc.hasMimeType(Reorderer.MIME_TYPE)) { + return false; + } + + if(e.getAction() == DragEvent.ACTION_DRAG_LOCATION) { + int oldDropPosition = mDropPosition; + if(holder != null) { + mDropPosition = holder.getPosition(); + } + + notifyPositionRangeChanged(oldDropPosition, mDropPosition); + } + else if(e.getAction() == DragEvent.ACTION_DROP) { + int oldDropPosition = mDropPosition; + if(holder != null) { + mDropPosition = holder.getPosition(); + } + + notifyPositionRangeChanged(oldDropPosition, mDropPosition); + if(mDropPosition != INVALID) { + ((ReorderableAdapter) decoratedAdapter).onItemDropped(mReorderPosition, mDropPosition); + notifyPositionRangeChanged(mReorderPosition, mDropPosition); + } + mReorderPosition = INVALID; + mDropPosition = INVALID; + } + else if(e.getAction() == DragEvent.ACTION_DRAG_ENDED) { + notifyPositionRangeChanged(mReorderPosition, mDropPosition); + mReorderPosition = INVALID; + mDropPosition = INVALID; + } + return true; + } + + /*package*/ RecyclerView.Adapter getDecoratedAdapter() { + return decoratedAdapter; + } + + @Override + public int getItemCount() { + return decoratedAdapter.getItemCount(); + } + + @Override + public long getItemId(int position) { + return decoratedAdapter.getItemId(position); + } + + @Override + public int getItemViewType(int position) { + return decoratedAdapter.getItemViewType(position); + } + + @Override + public void onBindViewHolder(VH holder, int position) { + if(!isReordering()) { //if we're not reordering, don't do anything to the view (unless we have to undo the drop position decoration) + decoratedAdapter.onBindViewHolder(holder, position); + if(decoratedAdapter instanceof ReorderableAdapterViewDecorator) { + ((ReorderableAdapterViewDecorator) decoratedAdapter).undoDropPositionDecoration(holder.itemView); + } + else { + if(holder.itemView.getVisibility() == View.INVISIBLE) { + holder.itemView.setVisibility(View.VISIBLE); + } + } + } + else if(holder.getPosition() == mDropPosition) { // if it's the drop position, apply the drop position decoration + decoratedAdapter.onBindViewHolder(holder, position); + if(decoratedAdapter instanceof ReorderableAdapterViewDecorator) { + ((ReorderableAdapterViewDecorator) decoratedAdapter).applyDropPositionDecoration(holder.itemView); + } + else { + holder.itemView.setVisibility(View.INVISIBLE); + } + } + else { //if we're reordering but it's not the drop position, resolve the position (and undo the drop position decoration if needed) + decoratedAdapter.onBindViewHolder(holder, resolvePosition(position)); + if(decoratedAdapter instanceof ReorderableAdapterViewDecorator) { + ((ReorderableAdapterViewDecorator) decoratedAdapter).undoDropPositionDecoration(holder.itemView); + } + else { + if(holder.itemView.getVisibility() == View.INVISIBLE) { + holder.itemView.setVisibility(View.VISIBLE); + } + } + } + } + + /** + * Resolve the position to where it needs to be (shifted +/- 1 because of drop position, or not affected). + * @param position the position to resolve + * @return the resolved position + */ + private int resolvePosition(final int position) { + if(mDropPosition < mReorderPosition && position > mDropPosition && position <= mReorderPosition) { + return position - 1; + } + else if(mDropPosition > mReorderPosition && position < mDropPosition && position >= mReorderPosition) { + return position + 1; + } + else { + return position; + } + } + + @Override + public VH onCreateViewHolder(ViewGroup parent, int viewType) { + return decoratedAdapter.onCreateViewHolder(parent, viewType); + } + + @Override + public void onViewAttachedToWindow(VH holder) { + decoratedAdapter.onViewAttachedToWindow(holder); + } + + @Override + public void onViewDetachedFromWindow(VH holder) { + decoratedAdapter.onViewDetachedFromWindow(holder); + } + + @Override + public void onViewRecycled(VH holder) { + decoratedAdapter.onViewRecycled(holder); + } + + @Override + public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { + decoratedAdapter.registerAdapterDataObserver(observer); + } + + @Override + public void setHasStableIds(boolean hasStableIds) { + decoratedAdapter.setHasStableIds(hasStableIds); + } + + @Override + public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { + decoratedAdapter.unregisterAdapterDataObserver(observer); + } +} diff --git a/layouts/src/main/java/org/lucasr/twowayview/widget/TwoWayView.java b/layouts/src/main/java/org/lucasr/twowayview/widget/TwoWayView.java index cc71d3b..5c44c19 100644 --- a/layouts/src/main/java/org/lucasr/twowayview/widget/TwoWayView.java +++ b/layouts/src/main/java/org/lucasr/twowayview/widget/TwoWayView.java @@ -16,12 +16,14 @@ package org.lucasr.twowayview.widget; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; +import android.os.Build; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.util.AttributeSet; - +import android.view.DragEvent; import org.lucasr.twowayview.TwoWayLayoutManager; import org.lucasr.twowayview.TwoWayLayoutManager.Orientation; @@ -35,6 +37,9 @@ public class TwoWayView extends RecyclerView { final Object[] sConstructorArgs = new Object[2]; + private Reorderer mReorderer; + private OnReorderingListener mReorderingListener; + public TwoWayView(Context context) { this(context, null); } @@ -112,4 +117,149 @@ public int getLastVisiblePosition() { TwoWayLayoutManager layout = (TwoWayLayoutManager) getLayoutManager(); return layout.getLastVisiblePosition(); } + + /* + The following methods all relate to reordering, and are only applicable to API 11+ + */ + + /** + * Returns whether this {@code TwoWayView} is in middle of reordering. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public boolean isReordering() { + return mReorderer.isReordering(); + } + + @Override + public Adapter getAdapter() { + if(mReorderer == null) { + return super.getAdapter(); + } + else { + return ((ReordererAdapterDecorator) super.getAdapter()).getDecoratedAdapter(); + } + } + + /*pacakge*/ ReordererAdapterDecorator getReordererAdapter() { + if(mReorderer != null) { + return (ReordererAdapterDecorator) super.getAdapter(); + } + else { + return null; + } + } + + /** + * Should only be used for API 11+.
+ * Set a new adapter to provide child views on demand. + * When adapter is changed, all existing views are recycled back to the pool. + * If the pool has only one adapter, it will be cleared. + * {@code adapter} must be a {@link ReorderableAdapter}. + * @param adapter the new adapter to set + * @throws java.lang.IllegalArgumentException if {@code adapter} is not a {@code ReorderableAdapter} + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void setReorderableAdapter(Adapter adapter) { + // if its null, we can't go into reorderable mode + if(adapter == null) { + setAdapter(null); + return; + } + + super.setAdapter(ReordererAdapterDecorator.decorateAdapter(adapter)); + mReorderer = new Reorderer(this); + } + + @Override + public void setAdapter(Adapter adapter) { + if(mReorderer != null) { + mReorderer = null; + } + super.setAdapter(adapter); + } + + /** + * Should only be used for API 11+.
+ * Swaps the current adapter with the provided one. It is similar to setAdapter(Adapter) but assumes + * existing adapter and the new adapter uses the same RecyclerView.ViewHolder and does not clear the RecycledViewPool. + *
Note that it still calls onAdapterChanged callbacks. + * {@code adapter} must be a {@link ReorderableAdapter}. + * @param adapter the new adapter to set + * @throws java.lang.NullPointerException if {@code adapter} is {@code null} + * @throws java.lang.IllegalArgumentException if {@code adapter} is not a {@code ReorderableAdapter} + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void swapReorderableAdapter(Adapter adapter, boolean removeAndRecycleExistingViews) { + //we don't check for null here because we want to propogate the NPE from ReordererAdapterDecorator + super.swapAdapter(ReordererAdapterDecorator.decorateAdapter(adapter), removeAndRecycleExistingViews); + mReorderer = new Reorderer(this); + } + + @Override + public void swapAdapter(Adapter adapter, boolean removeAndRecycleExistingViews) { + if(mReorderer != null) { + mReorderer = null; + } + super.swapAdapter(adapter, removeAndRecycleExistingViews); + } + + /** + * Sets a {@link OnReorderingListener}. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void setOnReorderingListener(OnReorderingListener onReorderingListener) { + this.mReorderingListener = onReorderingListener; + } + + /*package*/ OnReorderingListener getOnReorderingListener() { + return mReorderingListener; + } + + /** + * Should only be used for API 11+.
+ * Start a reordering operation. + * {@link TwoWayView#setReorderableAdapter} or {@link TwoWayView#swapReorderableAdapter} must be called at some + * point before this. + * @param position the position to drag + * @return whether the reordering operation was started successfully + * @throws java.lang.IllegalStateException if {@code setReorderableAdapter} or + * {@code swapReorderableAdapter} has not been called + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public final boolean startReorder(int position) { + if(mReorderer == null) { + throw new IllegalStateException("Cannot call startReorder if setReorderableAdapter or " + + "swapReorderableAdapter has not been called"); + } + + return mReorderer.startReorder(position); + } + + // As per this bug - https://code.google.com/p/android/issues/detail?id=25073 + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public boolean dispatchDragEvent(DragEvent ev) { + if(mReorderer == null) { + return super.dispatchDragEvent(ev); + } + else { + return mReorderer.dispatchDragEvent(ev); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + /*package*/ boolean superDispatchDragEvent(DragEvent ev) { + return super.dispatchDragEvent(ev); + } + + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public boolean onDragEvent(DragEvent ev) { + if(mReorderer == null) { + return super.onDragEvent(ev); + } + else { + return mReorderer.onDragEvent(ev); + } + } } diff --git a/sample/src/main/java/org/lucasr/twowayview/sample/LayoutAdapter.java b/sample/src/main/java/org/lucasr/twowayview/sample/LayoutAdapter.java index baabd25..2d0e4f8 100644 --- a/sample/src/main/java/org/lucasr/twowayview/sample/LayoutAdapter.java +++ b/sample/src/main/java/org/lucasr/twowayview/sample/LayoutAdapter.java @@ -22,16 +22,16 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; - import org.lucasr.twowayview.TwoWayLayoutManager; -import org.lucasr.twowayview.widget.TwoWayView; +import org.lucasr.twowayview.widget.ReorderableAdapter; import org.lucasr.twowayview.widget.SpannableGridLayoutManager; import org.lucasr.twowayview.widget.StaggeredGridLayoutManager; +import org.lucasr.twowayview.widget.TwoWayView; import java.util.ArrayList; import java.util.List; -public class LayoutAdapter extends RecyclerView.Adapter { +public class LayoutAdapter extends RecyclerView.Adapter implements ReorderableAdapter { private static final int COUNT = 100; private final Context mContext; @@ -40,6 +40,12 @@ public class LayoutAdapter extends RecyclerView.Adapter Date: Sun, 2 Nov 2014 01:31:03 -0400 Subject: [PATCH 2/2] Fixed scrolling. --- .../lucasr/twowayview/widget/Reorderer.java | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/layouts/src/main/java/org/lucasr/twowayview/widget/Reorderer.java b/layouts/src/main/java/org/lucasr/twowayview/widget/Reorderer.java index fd8850d..7ab17d0 100644 --- a/layouts/src/main/java/org/lucasr/twowayview/widget/Reorderer.java +++ b/layouts/src/main/java/org/lucasr/twowayview/widget/Reorderer.java @@ -9,6 +9,7 @@ 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 { @@ -66,7 +67,12 @@ @Override public void run() { if(isScrolling) { - twv.smoothScrollBy(scrollDistance, 500); + if(twv.getOrientation() == TwoWayLayoutManager.Orientation.VERTICAL) { + twv.smoothScrollBy(0, scrollDistance); + } + else { + twv.smoothScrollBy(scrollDistance, 0); + } scrollHandler.postDelayed(this, 250); } } @@ -91,29 +97,28 @@ public boolean onDrag(View v, DragEvent ev) { lastKnownPosition = AbsListView.INVALID_POSITION; } else if(ev.getAction() == DragEvent.ACTION_DRAG_LOCATION) { - // TODO: fix scrolling with the dragged item - this is semi FUBAR - need to handle orientation as well - int twvHeight = twv.getHeight(); - int[] globalCoords = new int[2]; - twv.getLocationInWindow(globalCoords); - int bottomOffset = twvHeight - y; - - View firstVisibleView = twv.getChildAt(0); - scrollDistance = twvHeight / 8; + 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 height = firstVisibleView.getHeight(); - scrollDistance = ((height * 2) + (height / 2)); - scrollThreshold = height / 8; + int bound = (isVertical ? firstVisibleView.getHeight() : firstVisibleView.getWidth()); + scrollDistance = ((bound * 2) + (bound / 2)); + scrollThreshold = bound / 8; } - if(y <= scrollThreshold && bottomOffset >= scrollThreshold) { + if(pointer <= scrollThreshold && bottomOffset >= scrollThreshold) { scrollDistance = -scrollDistance; if(!isScrolling) { scrollHandler.post(scrollRunnable); isScrolling = true; } } - else if(y >= scrollThreshold && bottomOffset <= scrollThreshold) { + else if(pointer >= scrollThreshold && bottomOffset <= scrollThreshold) { if(!isScrolling) { scrollHandler.post(scrollRunnable); isScrolling = true;