1 /*
2  * Copyright (C) 2013 Google Inc. All Rights Reserved.
3  */
4 
5 package com.android.deskclock.widget.sgv;
6 
7 import android.graphics.Point;
8 import android.util.Log;
9 import android.view.View;
10 
11 import com.android.deskclock.widget.sgv.StaggeredGridView.LayoutParams;
12 import com.android.deskclock.widget.sgv.StaggeredGridView.ReorderListener;
13 
14 
15 /**
16  * Helper class for doing reorder animations. Works out the logical position of
17  * where an item would be placed as the user drags it around.
18  */
19 public final class ReorderHelper {
20 
21     private static final String TAG = "DeskClock";
22 
23     /**
24      * Constant to indicate an unsupported reordering position.
25      */
26     public static final int INVALID_REORDER_POS = -2;
27 
28     private final ReorderListener mReorderListener;
29 
30     // Current {@link ReorderView} that is currently being dragged over.  If drag is released here,
31     // and this child supports reordering, the dragged view will be reordered to be next
32     // to this child.
33     private ReorderView mCurrentDraggedOverChild;
34 
35     // The current child that is being dragged for reordering.
36     private ReorderView mDraggedChild;
37 
38     // The id of mDraggedChild.
39     private long mDraggedChildId = -1;
40 
41     // The parent view group that dragged children are attached to.
42     private final StaggeredGridView mParentView;
43 
44     private boolean mEnableUpdatesOnDrag = true;
45 
ReorderHelper(ReorderListener listener, StaggeredGridView parentView)46     public ReorderHelper(ReorderListener listener, StaggeredGridView parentView) {
47         mReorderListener = listener;
48         mParentView = parentView;
49         if (listener == null) {
50             throw new IllegalArgumentException("ReorderListener cannot be null");
51         }
52 
53         if (parentView == null) {
54             throw new IllegalArgumentException("ParentView cannot be null");
55         }
56     }
57 
58     /**
59      * Handle dropping the dragged child.
60      * @return true if the drop results in a reordering, false otherwise.
61      */
handleDrop(Point p)62     public boolean handleDrop(Point p) {
63         View reorderTarget = null;
64         if (mCurrentDraggedOverChild != null) {
65             reorderTarget = getReorderableChildAtCoordinate(p);
66         } else {
67             Log.w(TAG, "Current dragged over child does not exist");
68         }
69 
70         // If reorder target is null, the drag coordinate is not over any
71         // reordering areas. Don't update dragged over child if its the same as
72         // it was before or is the same as the child's original item.
73         if (reorderTarget != null) {
74             final LayoutParams lp = (LayoutParams) reorderTarget.getLayoutParams();
75             // Ensure that target position is not the same as the original,
76             // since that's a no-op.
77             if (lp.position != mCurrentDraggedOverChild.position) {
78                 updateDraggedOverChild(reorderTarget);
79             }
80         }
81 
82         if (mCurrentDraggedOverChild != null &&
83                 mDraggedChild.position != mCurrentDraggedOverChild.position) {
84             return mReorderListener.onReorder(mDraggedChild.target, mDraggedChild.id,
85                     mDraggedChild.position,
86                     mCurrentDraggedOverChild.position);
87         } else {
88             // Even if the dragged child is not dropped in a reorder area, we
89             // would still need to notify the listener of the drop event.
90             mReorderListener.onDrop(mDraggedChild.target, mDraggedChild.position,
91                     mCurrentDraggedOverChild.position);
92             return false;
93         }
94     }
95 
handleDragCancelled(View draggedView)96     public void handleDragCancelled(View draggedView) {
97         mReorderListener.onCancelDrag(draggedView);
98     }
99 
handleDragStart(View view, int pos, long id, Point p)100     public void handleDragStart(View view, int pos, long id, Point p) {
101         mDraggedChild = new ReorderView(view, pos, id);
102         mDraggedChildId = id;
103         mCurrentDraggedOverChild = new ReorderView(view, pos, id);
104         mReorderListener.onPickedUp(mDraggedChild.target);
105     }
106 
107     /**
108      * Handles determining which child views should be moved out of the way to
109      * make space for a reordered item and updates the ReorderListener when a
110      * new child view's space is entered by the dragging view.
111      */
handleDrag(Point p)112     public void handleDrag(Point p) {
113         if (p == null || p.y < 0 && p.y > mParentView.getHeight()) {
114             // If the user drags off screen, DragEvent.ACTION_DRAG_ENDED, would be called, so we'll
115             // treat it as though the user has released drag.
116             handleDrop(p);
117             return;
118         }
119 
120         if (!mEnableUpdatesOnDrag) {
121             return;
122         }
123 
124         View reorderTarget = null;
125         if (mCurrentDraggedOverChild != null) {
126             reorderTarget = getReorderableChildAtCoordinate(p);
127         } else {
128             Log.w(TAG, "Current dragged over child does not exist");
129         }
130 
131         // If reorder target is null, the drag coordinate is not over any
132         // reordering areas. Don't update dragged over child if its the same as
133         // it was before or is the same as the child's original item.
134         if (reorderTarget != null) {
135             final LayoutParams lp = (LayoutParams) reorderTarget.getLayoutParams();
136             if (lp.position != mCurrentDraggedOverChild.position) {
137                 updateDraggedOverChild(reorderTarget);
138                 // Ensure that target position is not the same as the original,
139                 // since that's a no-op.
140                 mReorderListener.onEnterReorderArea(reorderTarget, lp.position);
141             }
142         }
143     }
144 
145     /**
146      * Enable updates on drag events. If set to false, handleDrag will not update place holder
147      */
enableUpdatesOnDrag(boolean enabled)148     public void enableUpdatesOnDrag(boolean enabled) {
149         mEnableUpdatesOnDrag = enabled;
150     }
151 
152     /**
153      * Clear dragged over child info
154      */
clearDraggedOverChild()155     public void clearDraggedOverChild() {
156         mCurrentDraggedOverChild = null;
157     }
158 
159     /**
160      * Return if the currently dragged view is over a valid reordering area.
161      */
isOverReorderingArea()162     public boolean isOverReorderingArea() {
163         return mCurrentDraggedOverChild != null;
164     }
165 
166     /**
167      * Get the position of the child that is being dragged over. If there isn't one, returns
168      * {@link #INVALID_REORDER_POS}
169      */
getCurrentDraggedOverChildPosition()170     public int getCurrentDraggedOverChildPosition() {
171         if (mCurrentDraggedOverChild != null) {
172             return mCurrentDraggedOverChild.position;
173         }
174 
175         return INVALID_REORDER_POS;
176     }
177 
178     /**
179      * Get the id of the child that is being dragged. If there isn't one, returns -1
180      */
getDraggedChildId()181     public long getDraggedChildId() {
182         return mDraggedChildId;
183     }
184 
185     /**
186      * Get the original view of the child that is being dragged. If there isn't
187      * one, returns null
188      */
getDraggedChild()189     public View getDraggedChild() {
190         return mDraggedChild != null ? mDraggedChild.target : null;
191     }
192 
193     /**
194      * Clear original dragged child info
195      */
clearDraggedChild()196     public void clearDraggedChild() {
197         mDraggedChild = null;
198     }
199 
200     // TODO: Consolidate clearDraggedChild() and clearDraggedChildId().
clearDraggedChildId()201     public void clearDraggedChildId() {
202         mDraggedChildId = -1;
203     }
204 
205     /**
206      * Get the original position of the child that is being dragged. If there isn't one, returns
207      * {@link #INVALID_REORDER_POS};
208      */
getDraggedChildPosition()209     public int getDraggedChildPosition() {
210         return mDraggedChild != null ? mDraggedChild.position : INVALID_REORDER_POS;
211     }
212 
updateDraggedChildView(View v)213     public void updateDraggedChildView(View v) {
214         if (mDraggedChild != null && v != mDraggedChild.target) {
215             mDraggedChild.target = v;
216         }
217     }
218 
updateDraggedOverChildView(View v)219     public void updateDraggedOverChildView(View v) {
220         if (mCurrentDraggedOverChild != null && v != mCurrentDraggedOverChild.target) {
221             mCurrentDraggedOverChild.target = v;
222         }
223     }
224 
225     /**
226      * Update the current view that is being dragged over, and clean up all drag and hover
227      * UI states from other sibling views.
228      * @param child The new child that is being dragged over.
229      */
updateDraggedOverChild(View child)230     private void updateDraggedOverChild(View child) {
231         final LayoutParams childLayoutParam = (LayoutParams) child.getLayoutParams();
232         mCurrentDraggedOverChild = new ReorderView(
233                 child, childLayoutParam.position, childLayoutParam.id);
234     }
235 
236     /**
237      * Return the child view specified by the coordinates if
238      * there exists a child there.
239      *
240      * @return the child in this StaggeredGridView at the coordinates, null otherwise.
241      */
getReorderableChildAtCoordinate(Point p)242     public View getReorderableChildAtCoordinate(Point p) {
243         if (p == null || p.y < 0) {
244             // TODO: If we've dragged off the screen, return null for now until we know what
245             // we'd like the experience to be like.
246             return null;
247         }
248 
249         final int count = mParentView.getChildCount();
250         for (int i = 0; i < count; i++) {
251             if (!mParentView.isChildReorderable(i)) {
252                 continue;
253             }
254             final View childView = mParentView.getChildAt(i);
255             if (p.x >= childView.getLeft() && p.x < childView.getRight()
256                     && p.y >= childView.getTop() && p.y < childView.getBottom()) {
257                 return childView;
258             }
259         }
260 
261         return null;
262     }
263 
hasReorderListener()264     public boolean hasReorderListener() {
265         return mReorderListener != null;
266     }
267 
268     private class ReorderView {
269         final long id;
270         final int position;
271         View target;
ReorderView(View v, int pos, long i)272         public ReorderView(View v, int pos, long i) {
273             target = v;
274             position = pos;
275             id = i;
276         }
277     }
278 }