1/*
2 * Copyright (C) 2013 Google Inc. All Rights Reserved.
3 */
4
5package com.android.deskclock.widget.sgv;
6
7import android.graphics.Point;
8import android.util.Log;
9import android.view.View;
10
11import com.android.deskclock.widget.sgv.StaggeredGridView.LayoutParams;
12import 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 */
19public 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
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     */
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
96    public void handleDragCancelled(View draggedView) {
97        mReorderListener.onCancelDrag(draggedView);
98    }
99
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     */
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     */
148    public void enableUpdatesOnDrag(boolean enabled) {
149        mEnableUpdatesOnDrag = enabled;
150    }
151
152    /**
153     * Clear dragged over child info
154     */
155    public void clearDraggedOverChild() {
156        mCurrentDraggedOverChild = null;
157    }
158
159    /**
160     * Return if the currently dragged view is over a valid reordering area.
161     */
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     */
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     */
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     */
189    public View getDraggedChild() {
190        return mDraggedChild != null ? mDraggedChild.target : null;
191    }
192
193    /**
194     * Clear original dragged child info
195     */
196    public void clearDraggedChild() {
197        mDraggedChild = null;
198    }
199
200    // TODO: Consolidate clearDraggedChild() and 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     */
209    public int getDraggedChildPosition() {
210        return mDraggedChild != null ? mDraggedChild.position : INVALID_REORDER_POS;
211    }
212
213    public void updateDraggedChildView(View v) {
214        if (mDraggedChild != null && v != mDraggedChild.target) {
215            mDraggedChild.target = v;
216        }
217    }
218
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     */
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     */
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
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;
272        public ReorderView(View v, int pos, long i) {
273            target = v;
274            position = pos;
275            id = i;
276        }
277    }
278}