blob: c8b6f8528272031aaa7f2b328610db05db854f83 [file] [log] [blame]
Steve McKay4b3a13c2015-06-11 10:10:49 -07001/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Steve McKayf8621552015-11-03 15:23:16 -080017package com.android.documentsui.dirlist;
Steve McKay4b3a13c2015-06-11 10:10:49 -070018
Steve McKay635b0942015-09-03 16:49:51 -070019import static com.android.documentsui.Shared.DEBUG;
Ben Kwa22f479d2016-01-20 15:07:53 -080020import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DIRECTORY;
21import static com.android.documentsui.dirlist.ModelBackedDocumentsAdapter.ITEM_TYPE_DOCUMENT;
Steve McKaye63dce72015-07-28 19:20:01 -070022import static com.android.internal.util.Preconditions.checkArgument;
Steve McKayb04b1642015-07-24 13:14:20 -070023import static com.android.internal.util.Preconditions.checkNotNull;
24import static com.android.internal.util.Preconditions.checkState;
25
Steve McKaye852d932016-02-08 19:09:42 -080026import android.annotation.IntDef;
Steve McKay635b0942015-09-03 16:49:51 -070027import android.graphics.Point;
28import android.graphics.Rect;
29import android.graphics.drawable.Drawable;
Steve McKaye852d932016-02-08 19:09:42 -080030import android.os.Parcel;
31import android.os.Parcelable;
Steve McKay635b0942015-09-03 16:49:51 -070032import android.support.annotation.Nullable;
33import android.support.annotation.VisibleForTesting;
Kyle Horimoto2c802572015-08-18 13:25:29 -070034import android.support.v7.widget.GridLayoutManager;
Steve McKay4b3a13c2015-06-11 10:10:49 -070035import android.support.v7.widget.RecyclerView;
Steve McKay4b3a13c2015-06-11 10:10:49 -070036import android.util.Log;
Kyle Horimoto786babe2015-08-25 21:03:38 -070037import android.util.SparseArray;
Ben Kwafcb54d82015-12-10 15:21:18 -080038import android.util.SparseBooleanArray;
39import android.util.SparseIntArray;
Steve McKay4b3a13c2015-06-11 10:10:49 -070040import android.view.MotionEvent;
41import android.view.View;
Steve McKay635b0942015-09-03 16:49:51 -070042
43import com.android.documentsui.Events.InputEvent;
44import com.android.documentsui.Events.MotionInputEvent;
Steve McKay57a93ba2015-11-11 16:26:59 +090045import com.android.documentsui.R;
Steve McKay4b3a13c2015-06-11 10:10:49 -070046
Steve McKaye852d932016-02-08 19:09:42 -080047import java.lang.annotation.Retention;
48import java.lang.annotation.RetentionPolicy;
Steve McKay4b3a13c2015-06-11 10:10:49 -070049import java.util.ArrayList;
Ben Kwafcb54d82015-12-10 15:21:18 -080050import java.util.Collection;
Kyle Horimoto2c802572015-08-18 13:25:29 -070051import java.util.Collections;
Ben Kwa743c7c22015-12-01 19:56:57 -080052import java.util.HashMap;
53import java.util.HashSet;
Steve McKay4b3a13c2015-06-11 10:10:49 -070054import java.util.List;
Ben Kwa743c7c22015-12-01 19:56:57 -080055import java.util.Map;
56import java.util.Set;
Steve McKay4b3a13c2015-06-11 10:10:49 -070057
58/**
Steve McKaydbec47a2015-08-12 14:48:34 -070059 * MultiSelectManager provides support traditional multi-item selection support to RecyclerView.
60 * Additionally it can be configured to restrict selection to a single element, @see
61 * #setSelectMode.
Steve McKay4b3a13c2015-06-11 10:10:49 -070062 */
Ben Kwa67924892016-01-27 09:58:36 -080063public final class MultiSelectManager {
Steve McKay4b3a13c2015-06-11 10:10:49 -070064
Steve McKaye852d932016-02-08 19:09:42 -080065 @IntDef(flag = true, value = {
66 MODE_MULTIPLE,
67 MODE_SINGLE
68 })
69 @Retention(RetentionPolicy.SOURCE)
70 public @interface SelectionMode {}
Steve McKaydbec47a2015-08-12 14:48:34 -070071 public static final int MODE_MULTIPLE = 0;
Steve McKaydbec47a2015-08-12 14:48:34 -070072 public static final int MODE_SINGLE = 1;
73
Steve McKay4b3a13c2015-06-11 10:10:49 -070074 private static final String TAG = "MultiSelectManager";
Steve McKay4b3a13c2015-06-11 10:10:49 -070075
76 private final Selection mSelection = new Selection();
Steve McKaye63dce72015-07-28 19:20:01 -070077
Steve McKay44408262016-01-05 15:27:17 -080078 private final SelectionEnvironment mEnvironment;
79 private final DocumentsAdapter mAdapter;
Steve McKay4b3a13c2015-06-11 10:10:49 -070080 private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);
81
Steve McKay44408262016-01-05 15:27:17 -080082 private Range mRanger;
Steve McKaydbec47a2015-08-12 14:48:34 -070083 private boolean mSingleSelect;
Steve McKaya698d632015-09-04 14:51:16 -070084
Steve McKayc3ef0d62015-09-08 17:15:25 -070085 @Nullable private BandController mBandManager;
Steve McKay4b3a13c2015-06-11 10:10:49 -070086
Steve McKay44408262016-01-05 15:27:17 -080087
Steve McKay4b3a13c2015-06-11 10:10:49 -070088 /**
Steve McKaye852d932016-02-08 19:09:42 -080089 * @param mode Selection single or multiple selection mode.
90 * @param initialSelection selection state probably preserved in external state.
Steve McKay4b3a13c2015-06-11 10:10:49 -070091 */
Steve McKay44408262016-01-05 15:27:17 -080092 public MultiSelectManager(
Steve McKaye852d932016-02-08 19:09:42 -080093 final RecyclerView recyclerView,
94 DocumentsAdapter adapter,
95 @SelectionMode int mode,
96 @Nullable Selection initialSelection) {
97
98 this(new RuntimeSelectionEnvironment(recyclerView), adapter, mode, initialSelection);
Ben Kwa8250db42015-10-07 14:15:12 -070099
Steve McKaya698d632015-09-04 14:51:16 -0700100 if (mode == MODE_MULTIPLE) {
Steve McKaye852d932016-02-08 19:09:42 -0800101 // TODO: Don't load this on low memory devices.
Steve McKay669ebe72015-10-19 12:04:21 -0700102 mBandManager = new BandController();
Steve McKaya698d632015-09-04 14:51:16 -0700103 }
Kyle Horimoto2c802572015-08-18 13:25:29 -0700104
Steve McKay4b3a13c2015-06-11 10:10:49 -0700105 recyclerView.addOnItemTouchListener(
106 new RecyclerView.OnItemTouchListener() {
Steve McKay635b0942015-09-03 16:49:51 -0700107 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -0700108 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
Steve McKay669ebe72015-10-19 12:04:21 -0700109 if (mBandManager != null) {
110 return mBandManager.handleEvent(new MotionInputEvent(e, recyclerView));
Steve McKaya698d632015-09-04 14:51:16 -0700111 }
Steve McKay669ebe72015-10-19 12:04:21 -0700112 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700113 }
Steve McKay635b0942015-09-03 16:49:51 -0700114
115 @Override
Kyle Horimoto2c802572015-08-18 13:25:29 -0700116 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
Steve McKay669ebe72015-10-19 12:04:21 -0700117 mBandManager.processInputEvent(
Steve McKay635b0942015-09-03 16:49:51 -0700118 new MotionInputEvent(e, recyclerView));
Kyle Horimoto2c802572015-08-18 13:25:29 -0700119 }
Steve McKay635b0942015-09-03 16:49:51 -0700120 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -0700121 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
122 });
123 }
124
Steve McKayb04b1642015-07-24 13:14:20 -0700125 /**
126 * Constructs a new instance with {@code adapter} and {@code helper}.
Steve McKay57a93ba2015-11-11 16:26:59 +0900127 * @param runtimeSelectionEnvironment
Steve McKayb04b1642015-07-24 13:14:20 -0700128 * @hide
129 */
130 @VisibleForTesting
Steve McKaye852d932016-02-08 19:09:42 -0800131 MultiSelectManager(
132 SelectionEnvironment environment,
133 DocumentsAdapter adapter,
134 @SelectionMode int mode,
135 @Nullable Selection initialSelection) {
136
Steve McKay57a93ba2015-11-11 16:26:59 +0900137 mEnvironment = checkNotNull(environment, "'environment' cannot be null.");
Steve McKay44408262016-01-05 15:27:17 -0800138 mAdapter = checkNotNull(adapter, "'adapter' cannot be null.");
Steve McKaydbec47a2015-08-12 14:48:34 -0700139 mSingleSelect = mode == MODE_SINGLE;
Steve McKaye852d932016-02-08 19:09:42 -0800140 if (initialSelection != null) {
141 mSelection.copyFrom(initialSelection);
142 }
Steve McKaydbec47a2015-08-12 14:48:34 -0700143
Steve McKay44408262016-01-05 15:27:17 -0800144 mAdapter.registerAdapterDataObserver(
Ben Kwa743c7c22015-12-01 19:56:57 -0800145 new RecyclerView.AdapterDataObserver() {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700146
Ben Kwafcb54d82015-12-10 15:21:18 -0800147 private List<String> mModelIds;
Ben Kwadb65cd52015-12-09 14:33:49 -0800148
Steve McKay4b3a13c2015-06-11 10:10:49 -0700149 @Override
150 public void onChanged() {
Steve McKay44408262016-01-05 15:27:17 -0800151 mModelIds = mAdapter.getModelIds();
Ben Kwad4d0e702016-02-01 09:11:55 -0800152
153 // Update the selection to remove any disappeared IDs.
154 mSelection.cancelProvisionalSelection();
155 mSelection.intersect(mModelIds);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700156 }
157
158 @Override
159 public void onItemRangeChanged(
Ben Kwa743c7c22015-12-01 19:56:57 -0800160 int startPosition, int itemCount, Object payload) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700161 // No change in position. Ignoring.
162 }
163
164 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800165 public void onItemRangeInserted(int startPosition, int itemCount) {
166 mSelection.cancelProvisionalSelection();
Steve McKay4b3a13c2015-06-11 10:10:49 -0700167 }
168
169 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800170 public void onItemRangeRemoved(int startPosition, int itemCount) {
171 checkState(startPosition >= 0);
172 checkState(itemCount > 0);
Ben Kwafcb54d82015-12-10 15:21:18 -0800173
Ben Kwa743c7c22015-12-01 19:56:57 -0800174 mSelection.cancelProvisionalSelection();
Ben Kwa743c7c22015-12-01 19:56:57 -0800175 // Remove any disappeared IDs from the selection.
Ben Kwafcb54d82015-12-10 15:21:18 -0800176 mSelection.intersect(mModelIds);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700177 }
178
179 @Override
180 public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
181 throw new UnsupportedOperationException();
182 }
183 });
184 }
185
Steve McKayb04b1642015-07-24 13:14:20 -0700186 /**
187 * Adds {@code callback} such that it will be notified when {@code MultiSelectManager}
188 * events occur.
189 *
190 * @param callback
191 */
Steve McKay4b3a13c2015-06-11 10:10:49 -0700192 public void addCallback(MultiSelectManager.Callback callback) {
193 mCallbacks.add(callback);
194 }
195
Ben Kwabd964562015-10-14 08:00:27 -0700196 public boolean hasSelection() {
197 return !mSelection.isEmpty();
198 }
199
Steve McKay4b3a13c2015-06-11 10:10:49 -0700200 /**
201 * Returns a Selection object that provides a live view
Ben Kwac64cb252015-08-27 16:04:46 -0700202 * on the current selection.
Steve McKay4b3a13c2015-06-11 10:10:49 -0700203 *
Ben Kwac64cb252015-08-27 16:04:46 -0700204 * @see #getSelection(Selection) on how to get a snapshot
Steve McKay4b3a13c2015-06-11 10:10:49 -0700205 * of the selection that will not reflect future changes
206 * to selection.
207 *
Ben Kwac64cb252015-08-27 16:04:46 -0700208 * @return The current selection.
Steve McKay4b3a13c2015-06-11 10:10:49 -0700209 */
210 public Selection getSelection() {
211 return mSelection;
212 }
213
214 /**
215 * Updates {@code dest} to reflect the current selection.
216 * @param dest
217 *
218 * @return The Selection instance passed in, for convenience.
219 */
220 public Selection getSelection(Selection dest) {
221 dest.copyFrom(mSelection);
222 return dest;
223 }
224
Steve McKayb04b1642015-07-24 13:14:20 -0700225 /**
Steve McKaye852d932016-02-08 19:09:42 -0800226 * Updates selection to include items in {@code selection}.
227 */
228 public void updateSelection(Selection selection) {
229 setItemsSelected(selection.toList(), true);
230 }
231
232 /**
Steve McKaydbec47a2015-08-12 14:48:34 -0700233 * Sets the selected state of the specified items. Note that the callback will NOT
234 * be consulted to see if an item can be selected.
235 *
Ben Kwa743c7c22015-12-01 19:56:57 -0800236 * @param ids
237 * @param selected
238 * @return
Steve McKayb04b1642015-07-24 13:14:20 -0700239 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800240 public boolean setItemsSelected(Iterable<String> ids, boolean selected) {
Steve McKayb04b1642015-07-24 13:14:20 -0700241 boolean changed = false;
Ben Kwa743c7c22015-12-01 19:56:57 -0800242 for (String id: ids) {
243 boolean itemChanged = selected ? mSelection.add(id) : mSelection.remove(id);
Steve McKaydbec47a2015-08-12 14:48:34 -0700244 if (itemChanged) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800245 notifyItemStateChanged(id, selected);
Steve McKaydbec47a2015-08-12 14:48:34 -0700246 }
247 changed |= itemChanged;
Steve McKayb04b1642015-07-24 13:14:20 -0700248 }
Steve McKaydbec47a2015-08-12 14:48:34 -0700249 notifySelectionChanged();
Steve McKayb04b1642015-07-24 13:14:20 -0700250 return changed;
251 }
252
253 /**
Steve McKaydbec47a2015-08-12 14:48:34 -0700254 * Clears the selection and notifies (even if nothing changes).
Steve McKayb04b1642015-07-24 13:14:20 -0700255 */
Steve McKay4b3a13c2015-06-11 10:10:49 -0700256 public void clearSelection() {
Steve McKaydbec47a2015-08-12 14:48:34 -0700257 clearSelectionQuietly();
258 notifySelectionChanged();
259 }
260
Kyle Horimotod017db02015-08-27 16:44:00 -0700261 public void handleLayoutChanged() {
Steve McKaya698d632015-09-04 14:51:16 -0700262 if (mBandManager != null) {
263 mBandManager.handleLayoutChanged();
264 }
Kyle Horimotod017db02015-08-27 16:44:00 -0700265 }
266
Steve McKaydbec47a2015-08-12 14:48:34 -0700267 /**
Ben Kwa67924892016-01-27 09:58:36 -0800268 * Clears the selection, without notifying selection listeners. UI elements still need to be
269 * notified about state changes so that they can update their appearance.
Steve McKaydbec47a2015-08-12 14:48:34 -0700270 */
271 private void clearSelectionQuietly() {
Steve McKaye63dce72015-07-28 19:20:01 -0700272 mRanger = null;
273
Ben Kwabd964562015-10-14 08:00:27 -0700274 if (!hasSelection()) {
Steve McKayb04b1642015-07-24 13:14:20 -0700275 return;
276 }
Ben Kwa743c7c22015-12-01 19:56:57 -0800277
Ben Kwa67924892016-01-27 09:58:36 -0800278 Selection oldSelection = getSelection(new Selection());
Steve McKay4b3a13c2015-06-11 10:10:49 -0700279 mSelection.clear();
280
Ben Kwa67924892016-01-27 09:58:36 -0800281 for (String id: oldSelection.getAll()) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800282 notifyItemStateChanged(id, false);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700283 }
284 }
285
Steve McKay635b0942015-09-03 16:49:51 -0700286 @VisibleForTesting
287 void onLongPress(InputEvent input) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700288 if (DEBUG) Log.d(TAG, "Handling long press event.");
289
Steve McKay635b0942015-09-03 16:49:51 -0700290 if (!input.isOverItem()) {
291 if (DEBUG) Log.i(TAG, "Cannot handle tap. No adapter position available.");
Steve McKay4b3a13c2015-06-11 10:10:49 -0700292 }
293
Steve McKay635b0942015-09-03 16:49:51 -0700294 handleAdapterEvent(input);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700295 }
296
Steve McKay4b3a13c2015-06-11 10:10:49 -0700297 @VisibleForTesting
Steve McKay635b0942015-09-03 16:49:51 -0700298 boolean onSingleTapUp(InputEvent input) {
299 if (DEBUG) Log.d(TAG, "Processing tap event.");
Ben Kwabd964562015-10-14 08:00:27 -0700300 if (!hasSelection()) {
Ben Kwa0436a752016-01-15 10:43:24 -0800301 // No selection active - do nothing.
Steve McKay5353a1e2015-07-30 12:27:44 -0700302 return false;
303 }
304
Steve McKay635b0942015-09-03 16:49:51 -0700305 if (!input.isOverItem()) {
306 if (DEBUG) Log.d(TAG, "Activity has no position. Canceling selection.");
Steve McKay5353a1e2015-07-30 12:27:44 -0700307 clearSelection();
308 return false;
309 }
310
Steve McKay635b0942015-09-03 16:49:51 -0700311 handleAdapterEvent(input);
Steve McKay6c86b192015-09-02 15:59:28 -0700312 return true;
Kyle Horimoto2c802572015-08-18 13:25:29 -0700313 }
314
315 /**
316 * Handles a change caused by a click on the item with the given position. If the Shift key is
317 * held down, this performs a range select; otherwise, it simply toggles the item's selection
318 * state.
319 */
Steve McKay635b0942015-09-03 16:49:51 -0700320 private void handleAdapterEvent(InputEvent input) {
321 if (mRanger != null && input.isShiftKeyDown()) {
322 mRanger.snapSelection(input.getItemPosition());
Steve McKay5353a1e2015-07-30 12:27:44 -0700323
Kyle Horimoto2c802572015-08-18 13:25:29 -0700324 // We're being lazy here notifying even when something might not have changed.
325 // To make this more correct, we'd need to update the Ranger class to return
326 // information about what has changed.
327 notifySelectionChanged();
Ben Kwabd7da4c2015-09-25 07:47:56 -0700328 } else {
Ben Kwa743c7c22015-12-01 19:56:57 -0800329 int position = input.getItemPosition();
330 toggleSelection(position);
331 setSelectionRangeBegin(position);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700332 }
Steve McKay5353a1e2015-07-30 12:27:44 -0700333 }
334
335 /**
Ben Kwa743c7c22015-12-01 19:56:57 -0800336 * A convenience method for toggling selection by adapter position.
Steve McKaye63dce72015-07-28 19:20:01 -0700337 *
Ben Kwa743c7c22015-12-01 19:56:57 -0800338 * @param position Adapter position to toggle.
Steve McKaye63dce72015-07-28 19:20:01 -0700339 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800340 private void toggleSelection(int position) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700341 // Position may be special "no position" during certain
342 // transitional phases. If so, skip handling of the event.
343 if (position == RecyclerView.NO_POSITION) {
344 if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position.");
Ben Kwabd7da4c2015-09-25 07:47:56 -0700345 return;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700346 }
Steve McKay44408262016-01-05 15:27:17 -0800347 String id = mAdapter.getModelId(position);
Steve McKayef16f5f2015-12-22 18:15:31 -0800348 if (id != null) {
349 toggleSelection(id);
350 }
Ben Kwa743c7c22015-12-01 19:56:57 -0800351 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700352
Ben Kwa743c7c22015-12-01 19:56:57 -0800353 /**
354 * Toggles selection on the item with the given model ID.
355 *
356 * @param modelId
357 */
358 public void toggleSelection(String modelId) {
Steve McKayef16f5f2015-12-22 18:15:31 -0800359 checkNotNull(modelId);
Steve McKaydbec47a2015-08-12 14:48:34 -0700360 boolean changed = false;
Ben Kwa743c7c22015-12-01 19:56:57 -0800361 if (mSelection.contains(modelId)) {
362 changed = attemptDeselect(modelId);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700363 } else {
Ben Kwa67924892016-01-27 09:58:36 -0800364 changed = attemptSelect(modelId);
Steve McKaye63dce72015-07-28 19:20:01 -0700365 }
Steve McKaydbec47a2015-08-12 14:48:34 -0700366
Ben Kwabd7da4c2015-09-25 07:47:56 -0700367 if (changed) {
368 notifySelectionChanged();
369 }
Steve McKaye63dce72015-07-28 19:20:01 -0700370 }
371
372 /**
Ben Kwa83df50f2016-02-10 14:01:19 -0800373 * Starts a range selection. If a range selection is already active, this will start a new range
374 * selection (which will reset the range anchor).
Ben Kwa67924892016-01-27 09:58:36 -0800375 *
Ben Kwa83df50f2016-02-10 14:01:19 -0800376 * @param pos The anchor position for the selection range.
Ben Kwa67924892016-01-27 09:58:36 -0800377 */
Ben Kwa83df50f2016-02-10 14:01:19 -0800378 void startRangeSelection(int pos) {
379 attemptSelect(mAdapter.getModelId(pos));
380 setSelectionRangeBegin(pos);
381 }
Ben Kwa67924892016-01-27 09:58:36 -0800382
Ben Kwa83df50f2016-02-10 14:01:19 -0800383 /**
384 * Sets the end point for the current range selection, started by a call to
385 * {@link #startRangeSelection(int)}. This function should only be called when a range selection
386 * is active (see {@link #isRangeSelectionActive()}. Items in the range [anchor, end] will be
387 * selected.
388 *
389 * @param pos The new end position for the selection range.
390 */
391 void snapRangeSelection(int pos) {
392 checkNotNull(mRanger);
393 mRanger.snapSelection(pos);
Ben Kwa67924892016-01-27 09:58:36 -0800394 notifySelectionChanged();
395 }
396
397 /**
Ben Kwa83df50f2016-02-10 14:01:19 -0800398 * Stops an in-progress range selection.
399 */
400 void endRangeSelection() {
401 mRanger = null;
402 }
403
404 /**
Ben Kwa67924892016-01-27 09:58:36 -0800405 * @return Whether or not there is a current range selection active.
406 */
Ben Kwa83df50f2016-02-10 14:01:19 -0800407 boolean isRangeSelectionActive() {
Ben Kwa67924892016-01-27 09:58:36 -0800408 return mRanger != null;
409 }
410
411 /**
412 * Sets the magic location at which a selection range begins (the selection anchor). This value
413 * is consulted when determining how to extend, and modify selection ranges. Calling this when a
414 * range selection is active will reset the range selection.
Steve McKay5353a1e2015-07-30 12:27:44 -0700415 *
416 * @throws IllegalStateException if {@code position} is not already be selected
417 * @param position
418 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800419 void setSelectionRangeBegin(int position) {
Ben Kwa20887032015-12-22 15:27:14 -0800420 if (position == RecyclerView.NO_POSITION) {
421 return;
422 }
423
Steve McKay44408262016-01-05 15:27:17 -0800424 if (mSelection.contains(mAdapter.getModelId(position))) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800425 mRanger = new Range(position);
426 }
Steve McKay5353a1e2015-07-30 12:27:44 -0700427 }
428
429 /**
Ben Kwa743c7c22015-12-01 19:56:57 -0800430 * Try to set selection state for all elements in range. Not that callbacks can cancel selection
431 * of specific items, so some or even all items may not reflect the desired state after the
432 * update is complete.
Steve McKaye63dce72015-07-28 19:20:01 -0700433 *
Ben Kwa743c7c22015-12-01 19:56:57 -0800434 * @param begin Adapter position for range start (inclusive).
435 * @param end Adapter position for range end (inclusive).
436 * @param selected New selection state.
Steve McKaye63dce72015-07-28 19:20:01 -0700437 */
438 private void updateRange(int begin, int end, boolean selected) {
439 checkState(end >= begin);
Steve McKaye63dce72015-07-28 19:20:01 -0700440 for (int i = begin; i <= end; i++) {
Steve McKay44408262016-01-05 15:27:17 -0800441 String id = mAdapter.getModelId(i);
Steve McKayef16f5f2015-12-22 18:15:31 -0800442 if (id == null) {
443 continue;
444 }
445
Steve McKaye63dce72015-07-28 19:20:01 -0700446 if (selected) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800447 boolean canSelect = notifyBeforeItemStateChange(id, true);
Steve McKaydbec47a2015-08-12 14:48:34 -0700448 if (canSelect) {
Ben Kwabd964562015-10-14 08:00:27 -0700449 if (mSingleSelect && hasSelection()) {
Steve McKaydbec47a2015-08-12 14:48:34 -0700450 clearSelectionQuietly();
451 }
Ben Kwa743c7c22015-12-01 19:56:57 -0800452 selectAndNotify(id);
Steve McKaydbec47a2015-08-12 14:48:34 -0700453 }
Steve McKaye63dce72015-07-28 19:20:01 -0700454 } else {
Ben Kwa743c7c22015-12-01 19:56:57 -0800455 attemptDeselect(id);
Steve McKaye63dce72015-07-28 19:20:01 -0700456 }
457 }
458 }
459
460 /**
Ben Kwa743c7c22015-12-01 19:56:57 -0800461 * @param modelId
Steve McKaye63dce72015-07-28 19:20:01 -0700462 * @return True if the update was applied.
463 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800464 private boolean selectAndNotify(String modelId) {
465 boolean changed = mSelection.add(modelId);
Steve McKaydbec47a2015-08-12 14:48:34 -0700466 if (changed) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800467 notifyItemStateChanged(modelId, true);
Steve McKaye63dce72015-07-28 19:20:01 -0700468 }
Steve McKaydbec47a2015-08-12 14:48:34 -0700469 return changed;
Steve McKaye63dce72015-07-28 19:20:01 -0700470 }
471
472 /**
Ben Kwa743c7c22015-12-01 19:56:57 -0800473 * @param id
Steve McKaye63dce72015-07-28 19:20:01 -0700474 * @return True if the update was applied.
475 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800476 private boolean attemptDeselect(String id) {
Steve McKayef16f5f2015-12-22 18:15:31 -0800477 checkArgument(id != null);
Ben Kwa743c7c22015-12-01 19:56:57 -0800478 if (notifyBeforeItemStateChange(id, false)) {
479 mSelection.remove(id);
480 notifyItemStateChanged(id, false);
Steve McKaye63dce72015-07-28 19:20:01 -0700481 if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection);
482 return true;
483 } else {
484 if (DEBUG) Log.d(TAG, "Select cancelled by listener.");
485 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700486 }
487 }
488
Ben Kwa67924892016-01-27 09:58:36 -0800489 /**
490 * @param id
491 * @return True if the update was applied.
492 */
493 private boolean attemptSelect(String id) {
494 checkArgument(id != null);
495 boolean canSelect = notifyBeforeItemStateChange(id, true);
496 if (!canSelect) {
497 return false;
498 }
499 if (mSingleSelect && hasSelection()) {
500 clearSelectionQuietly();
501 }
502
503 selectAndNotify(id);
504 return true;
505 }
506
Ben Kwa743c7c22015-12-01 19:56:57 -0800507 private boolean notifyBeforeItemStateChange(String id, boolean nextState) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700508 int lastListener = mCallbacks.size() - 1;
509 for (int i = lastListener; i > -1; i--) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800510 if (!mCallbacks.get(i).onBeforeItemStateChange(id, nextState)) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700511 return false;
512 }
513 }
514 return true;
515 }
516
517 /**
Steve McKaydbec47a2015-08-12 14:48:34 -0700518 * Notifies registered listeners when the selection status of a single item
519 * (identified by {@code position}) changes.
Steve McKay4b3a13c2015-06-11 10:10:49 -0700520 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800521 private void notifyItemStateChanged(String id, boolean selected) {
Steve McKayef16f5f2015-12-22 18:15:31 -0800522 checkArgument(id != null);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700523 int lastListener = mCallbacks.size() - 1;
524 for (int i = lastListener; i > -1; i--) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800525 mCallbacks.get(i).onItemStateChanged(id, selected);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700526 }
Steve McKay44408262016-01-05 15:27:17 -0800527 mAdapter.onItemSelectionChanged(id);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700528 }
529
530 /**
Steve McKaydbec47a2015-08-12 14:48:34 -0700531 * Notifies registered listeners when the selection has changed. This
532 * notification should be sent only once a full series of changes
533 * is complete, e.g. clearingSelection, or updating the single
534 * selection from one item to another.
535 */
536 private void notifySelectionChanged() {
537 int lastListener = mCallbacks.size() - 1;
538 for (int i = lastListener; i > -1; i--) {
539 mCallbacks.get(i).onSelectionChanged();
540 }
541 }
542
543 /**
Steve McKaye63dce72015-07-28 19:20:01 -0700544 * Class providing support for managing range selections.
545 */
Steve McKay5353a1e2015-07-30 12:27:44 -0700546 private final class Range {
Steve McKaye63dce72015-07-28 19:20:01 -0700547 private static final int UNDEFINED = -1;
548
549 final int mBegin;
550 int mEnd = UNDEFINED;
551
Steve McKay5353a1e2015-07-30 12:27:44 -0700552 public Range(int begin) {
Steve McKaydbec47a2015-08-12 14:48:34 -0700553 if (DEBUG) Log.d(TAG, "New Ranger created beginning @ " + begin);
Steve McKaye63dce72015-07-28 19:20:01 -0700554 mBegin = begin;
555 }
556
Steve McKay5353a1e2015-07-30 12:27:44 -0700557 private void snapSelection(int position) {
Steve McKaye63dce72015-07-28 19:20:01 -0700558 checkState(mRanger != null);
559 checkArgument(position != RecyclerView.NO_POSITION);
560
561 if (mEnd == UNDEFINED || mEnd == mBegin) {
562 // Reset mEnd so it can be established in establishRange.
563 mEnd = UNDEFINED;
564 establishRange(position);
565 } else {
566 reviseRange(position);
567 }
568 }
569
570 private void establishRange(int position) {
571 checkState(mRanger.mEnd == UNDEFINED);
572
573 if (position == mBegin) {
574 mEnd = position;
575 }
576
577 if (position > mBegin) {
578 updateRange(mBegin + 1, position, true);
579 } else if (position < mBegin) {
580 updateRange(position, mBegin - 1, true);
581 }
582
583 mEnd = position;
584 }
585
586 private void reviseRange(int position) {
587 checkState(mEnd != UNDEFINED);
588 checkState(mBegin != mEnd);
589
590 if (position == mEnd) {
591 if (DEBUG) Log.i(TAG, "Skipping no-op revision click on mEndRange.");
592 }
593
594 if (mEnd > mBegin) {
595 reviseAscendingRange(position);
596 } else if (mEnd < mBegin) {
597 reviseDescendingRange(position);
598 }
599 // the "else" case is covered by checkState at beginning of method.
600
601 mEnd = position;
602 }
603
604 /**
605 * Updates an existing ascending seleciton.
606 * @param position
607 */
608 private void reviseAscendingRange(int position) {
609 // Reducing or reversing the range....
610 if (position < mEnd) {
611 if (position < mBegin) {
612 updateRange(mBegin + 1, mEnd, false);
613 updateRange(position, mBegin -1, true);
614 } else {
615 updateRange(position + 1, mEnd, false);
616 }
617 }
618
619 // Extending the range...
620 else if (position > mEnd) {
621 updateRange(mEnd + 1, position, true);
622 }
623 }
624
625 private void reviseDescendingRange(int position) {
626 // Reducing or reversing the range....
627 if (position > mEnd) {
628 if (position > mBegin) {
629 updateRange(mEnd, mBegin - 1, false);
630 updateRange(mBegin + 1, position, true);
631 } else {
632 updateRange(mEnd, position - 1, false);
633 }
634 }
635
636 // Extending the range...
637 else if (position < mEnd) {
638 updateRange(position, mEnd - 1, true);
639 }
640 }
641 }
642
643 /**
Steve McKayb04b1642015-07-24 13:14:20 -0700644 * Object representing the current selection. Provides read only access
645 * public access, and private write access.
Steve McKay4b3a13c2015-06-11 10:10:49 -0700646 */
Steve McKaye852d932016-02-08 19:09:42 -0800647 public static final class Selection implements Parcelable {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700648
Ben Kwa743c7c22015-12-01 19:56:57 -0800649 // This class tracks selected items by managing two sets: the saved selection, and the total
650 // selection. Saved selections are those which have been completed by tapping an item or by
651 // completing a band select operation. Provisional selections are selections which have been
652 // temporarily created by an in-progress band select operation (once the user releases the
653 // mouse button during a band select operation, the selected items become saved). The total
654 // selection is the combination of both the saved selection and the provisional
655 // selection. Tracking both separately is necessary to ensure that saved selections do not
656 // become deselected when they are removed from the provisional selection; for example, if
657 // item A is tapped (and selected), then an in-progress band select covers A then uncovers
658 // A, A should still be selected as it has been saved. To ensure this behavior, the saved
659 // selection must be tracked separately.
Steve McKaye852d932016-02-08 19:09:42 -0800660 private Set<String> mSelection = new HashSet<>();
661 private Set<String> mProvisionalSelection = new HashSet<>();
662 private String mDirectoryKey;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700663
Steve McKayedd05752015-10-21 14:38:54 -0700664 @VisibleForTesting
Ben Kwa743c7c22015-12-01 19:56:57 -0800665 public Selection(String... ids) {
666 for (int i = 0; i < ids.length; i++) {
667 add(ids[i]);
Steve McKayedd05752015-10-21 14:38:54 -0700668 }
669 }
670
Steve McKay4b3a13c2015-06-11 10:10:49 -0700671 /**
Ben Kwa743c7c22015-12-01 19:56:57 -0800672 * @param id
Steve McKay4b3a13c2015-06-11 10:10:49 -0700673 * @return true if the position is currently selected.
674 */
Steve McKayef16f5f2015-12-22 18:15:31 -0800675 public boolean contains(@Nullable String id) {
Steve McKaye852d932016-02-08 19:09:42 -0800676 return mSelection.contains(id) || mProvisionalSelection.contains(id);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700677 }
678
679 /**
Steve McKayedd05752015-10-21 14:38:54 -0700680 * Returns an unordered array of selected positions.
681 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800682 public String[] getAll() {
Steve McKaye852d932016-02-08 19:09:42 -0800683 return toList().toArray(new String[0]);
684 }
685
686 /**
687 * Returns an unordered array of selected positions (including any
688 * provisional selections current in effect).
689 */
690 private List<String> toList() {
691 ArrayList<String> selection = new ArrayList<String>(mSelection);
692 selection.addAll(mProvisionalSelection);
693 return selection;
Steve McKayedd05752015-10-21 14:38:54 -0700694 }
695
696 /**
Steve McKay4b3a13c2015-06-11 10:10:49 -0700697 * @return size of the selection.
698 */
699 public int size() {
Steve McKaye852d932016-02-08 19:09:42 -0800700 return mSelection.size() + mProvisionalSelection.size();
Steve McKay4b3a13c2015-06-11 10:10:49 -0700701 }
702
Steve McKayb04b1642015-07-24 13:14:20 -0700703 /**
704 * @return true if the selection is empty.
705 */
706 public boolean isEmpty() {
Steve McKaye852d932016-02-08 19:09:42 -0800707 return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700708 }
709
710 /**
711 * Sets the provisional selection, which is a temporary selection that can be saved,
712 * canceled, or adjusted at a later time. When a new provision selection is applied, the old
713 * one (if it exists) is abandoned.
Steve McKaye852d932016-02-08 19:09:42 -0800714 * @return Map of ids added or removed. Added ids have a value of true, removed are false.
Kyle Horimoto2c802572015-08-18 13:25:29 -0700715 */
716 @VisibleForTesting
Steve McKaye852d932016-02-08 19:09:42 -0800717 protected Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800718 Map<String, Boolean> delta = new HashMap<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700719
Steve McKaye852d932016-02-08 19:09:42 -0800720 for (String id: mProvisionalSelection) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800721 // Mark each item that used to be in the selection but is unsaved and not in the new
722 // provisional selection.
Steve McKaye852d932016-02-08 19:09:42 -0800723 if (!newSelection.contains(id) && !mSelection.contains(id)) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800724 delta.put(id, false);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700725 }
726 }
727
Steve McKaye852d932016-02-08 19:09:42 -0800728 for (String id: mSelection) {
729 // Mark each item that used to be in the selection but is unsaved and not in the new
730 // provisional selection.
731 if (!newSelection.contains(id)) {
732 delta.put(id, false);
733 }
734 }
735
736 for (String id: newSelection) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800737 // Mark each item that was not previously in the selection but is in the new
738 // provisional selection.
Steve McKaye852d932016-02-08 19:09:42 -0800739 if (!mSelection.contains(id) && !mProvisionalSelection.contains(id)) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800740 delta.put(id, true);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700741 }
742 }
743
Ben Kwa743c7c22015-12-01 19:56:57 -0800744 // Now, iterate through the changes and actually add/remove them to/from the current
745 // selection. This could not be done in the previous loops because changing the size of
746 // the selection mid-iteration changes iteration order erroneously.
747 for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
748 String id = entry.getKey();
749 if (entry.getValue()) {
Steve McKaye852d932016-02-08 19:09:42 -0800750 mProvisionalSelection.add(id);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700751 } else {
Steve McKaye852d932016-02-08 19:09:42 -0800752 mProvisionalSelection.remove(id);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700753 }
754 }
755
756 return delta;
757 }
758
759 /**
760 * Saves the existing provisional selection. Once the provisional selection is saved,
761 * subsequent provisional selections which are different from this existing one cannot
762 * cause items in this existing provisional selection to become deselected.
763 */
764 @VisibleForTesting
765 protected void applyProvisionalSelection() {
Steve McKaye852d932016-02-08 19:09:42 -0800766 mSelection.addAll(mProvisionalSelection);
767 mProvisionalSelection.clear();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700768 }
769
770 /**
771 * Abandons the existing provisional selection so that all items provisionally selected are
772 * now deselected.
773 */
774 @VisibleForTesting
Ben Kwa743c7c22015-12-01 19:56:57 -0800775 void cancelProvisionalSelection() {
Steve McKaye852d932016-02-08 19:09:42 -0800776 mProvisionalSelection.clear();
Steve McKayb04b1642015-07-24 13:14:20 -0700777 }
778
Steve McKay4b3a13c2015-06-11 10:10:49 -0700779 /** @hide */
780 @VisibleForTesting
Ben Kwa743c7c22015-12-01 19:56:57 -0800781 boolean add(String id) {
Steve McKaye852d932016-02-08 19:09:42 -0800782 if (!mSelection.contains(id)) {
783 mSelection.add(id);
Steve McKayb04b1642015-07-24 13:14:20 -0700784 return true;
785 }
786 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700787 }
788
789 /** @hide */
790 @VisibleForTesting
Ben Kwa743c7c22015-12-01 19:56:57 -0800791 boolean remove(String id) {
Steve McKaye852d932016-02-08 19:09:42 -0800792 if (mSelection.contains(id)) {
793 mSelection.remove(id);
Steve McKayb04b1642015-07-24 13:14:20 -0700794 return true;
795 }
796 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700797 }
798
Steve McKayf8621552015-11-03 15:23:16 -0800799 public void clear() {
Steve McKaye852d932016-02-08 19:09:42 -0800800 mSelection.clear();
Steve McKay4b3a13c2015-06-11 10:10:49 -0700801 }
802
Ben Kwafcb54d82015-12-10 15:21:18 -0800803 /**
804 * Trims this selection to be the intersection of itself with the set of given IDs.
805 */
806 public void intersect(Collection<String> ids) {
Steve McKaye852d932016-02-08 19:09:42 -0800807 mSelection.retainAll(ids);
808 mProvisionalSelection.retainAll(ids);
Ben Kwafcb54d82015-12-10 15:21:18 -0800809 }
810
Steve McKay4b3a13c2015-06-11 10:10:49 -0700811 @VisibleForTesting
812 void copyFrom(Selection source) {
Steve McKaye852d932016-02-08 19:09:42 -0800813 mSelection = new HashSet<>(source.mSelection);
814 mProvisionalSelection = new HashSet<>(source.mProvisionalSelection);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700815 }
816
817 @Override
818 public String toString() {
819 if (size() <= 0) {
820 return "size=0, items=[]";
821 }
822
Steve McKaye852d932016-02-08 19:09:42 -0800823 StringBuilder buffer = new StringBuilder(size() * 28);
824 buffer.append("Selection{")
825 .append("applied{size=" + mSelection.size())
826 .append(", entries=" + mSelection)
827 .append("}, provisional{size=" + mProvisionalSelection.size())
828 .append(", entries=" + mProvisionalSelection)
829 .append("}}");
Steve McKay4b3a13c2015-06-11 10:10:49 -0700830 return buffer.toString();
831 }
832
833 @Override
Steve McKay3b409d02015-07-22 11:42:14 -0700834 public int hashCode() {
Steve McKaye852d932016-02-08 19:09:42 -0800835 return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
Steve McKay3b409d02015-07-22 11:42:14 -0700836 }
837
838 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -0700839 public boolean equals(Object that) {
840 if (this == that) {
841 return true;
842 }
843
Steve McKay3b409d02015-07-22 11:42:14 -0700844 if (!(that instanceof Selection)) {
845 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700846 }
Steve McKay3b409d02015-07-22 11:42:14 -0700847
Steve McKaye852d932016-02-08 19:09:42 -0800848 return mSelection.equals(((Selection) that).mSelection) &&
849 mProvisionalSelection.equals(((Selection) that).mProvisionalSelection);
850 }
851
852 /**
853 * Sets the state key for this selection, which allows us to match selections
854 * to particular states (of DirectoryFragment). Basically this lets us avoid
855 * loading a persisted selection in the wrong directory.
856 */
857 public void setDirectoryKey(String key) {
858 mDirectoryKey = key;
859 }
860
861 /**
862 * Sets the state key for this selection, which allows us to match selections
863 * to particular states (of DirectoryFragment). Basically this lets us avoid
864 * loading a persisted selection in the wrong directory.
865 */
866 public boolean hasDirectoryKey(String key) {
867 return key.equals(mDirectoryKey);
868 }
869
870 @Override
871 public int describeContents() {
872 return 0;
873 }
874
875 public void writeToParcel(Parcel dest, int flags) {
Steve McKaye852d932016-02-08 19:09:42 -0800876 dest.writeString(mDirectoryKey);
877 dest.writeList(new ArrayList<>(mSelection));
878 // We don't include provisional selection since it is
879 // typically coupled to some other runtime state (like a band).
Steve McKay4b3a13c2015-06-11 10:10:49 -0700880 }
881 }
882
Kyle Horimoto2c802572015-08-18 13:25:29 -0700883 /**
Steve McKayc3ef0d62015-09-08 17:15:25 -0700884 * Provides functionality for BandController. Exists primarily to tests that are
885 * fully isolated from RecyclerView.
Kyle Horimoto2c802572015-08-18 13:25:29 -0700886 */
Ben Kwa8250db42015-10-07 14:15:12 -0700887 interface SelectionEnvironment {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700888 void showBand(Rect rect);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700889 void hideBand();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700890 void addOnScrollListener(RecyclerView.OnScrollListener listener);
Steve McKayc3ef0d62015-09-08 17:15:25 -0700891 void removeOnScrollListener(RecyclerView.OnScrollListener listener);
892 void scrollBy(int dy);
893 int getHeight();
894 void invalidateView();
895 void runAtNextFrame(Runnable r);
896 void removeCallback(Runnable r);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700897 Point createAbsolutePoint(Point relativePoint);
898 Rect getAbsoluteRectForChildViewAt(int index);
Ben Kwafcb54d82015-12-10 15:21:18 -0800899 int getAdapterPositionAt(int index);
Steve McKayc3ef0d62015-09-08 17:15:25 -0700900 int getColumnCount();
901 int getRowCount();
902 int getChildCount();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700903 int getVisibleChildCount();
Ben Kwa22f479d2016-01-20 15:07:53 -0800904 /**
905 * Layout items are excluded from the GridModel.
906 */
907 boolean isLayoutItem(int adapterPosition);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700908 }
909
Steve McKay57a93ba2015-11-11 16:26:59 +0900910 /** Recycler view facade implementation backed by good ol' RecyclerView. */
Ben Kwa8250db42015-10-07 14:15:12 -0700911 private static final class RuntimeSelectionEnvironment implements SelectionEnvironment {
Kyle Horimoto2c802572015-08-18 13:25:29 -0700912
Steve McKayc3ef0d62015-09-08 17:15:25 -0700913 private final RecyclerView mView;
914 private final Drawable mBand;
Kyle Horimoto2c802572015-08-18 13:25:29 -0700915
916 private boolean mIsOverlayShown = false;
917
Steve McKay44408262016-01-05 15:27:17 -0800918 RuntimeSelectionEnvironment(RecyclerView view) {
919 mView = view;
Steve McKayc3ef0d62015-09-08 17:15:25 -0700920 mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700921 }
922
923 @Override
Ben Kwafcb54d82015-12-10 15:21:18 -0800924 public int getAdapterPositionAt(int index) {
Ben Kwa67924892016-01-27 09:58:36 -0800925 return mView.getChildAdapterPosition(mView.getChildAt(index));
Kyle Horimoto2c802572015-08-18 13:25:29 -0700926 }
927
928 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800929 public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700930 mView.addOnScrollListener(listener);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700931 }
932
933 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800934 public void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700935 mView.removeOnScrollListener(listener);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700936 }
937
938 @Override
939 public Point createAbsolutePoint(Point relativePoint) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700940 return new Point(relativePoint.x + mView.computeHorizontalScrollOffset(),
941 relativePoint.y + mView.computeVerticalScrollOffset());
Kyle Horimoto2c802572015-08-18 13:25:29 -0700942 }
943
944 @Override
945 public Rect getAbsoluteRectForChildViewAt(int index) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700946 final View child = mView.getChildAt(index);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700947 final Rect childRect = new Rect();
948 child.getHitRect(childRect);
Steve McKayc3ef0d62015-09-08 17:15:25 -0700949 childRect.left += mView.computeHorizontalScrollOffset();
950 childRect.right += mView.computeHorizontalScrollOffset();
951 childRect.top += mView.computeVerticalScrollOffset();
952 childRect.bottom += mView.computeVerticalScrollOffset();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700953 return childRect;
954 }
955
956 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -0700957 public int getChildCount() {
958 return mView.getAdapter().getItemCount();
959 }
960
961 @Override
Kyle Horimoto2c802572015-08-18 13:25:29 -0700962 public int getVisibleChildCount() {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700963 return mView.getChildCount();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700964 }
965
966 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -0700967 public int getColumnCount() {
Ben Kwa743c7c22015-12-01 19:56:57 -0800968 RecyclerView.LayoutManager layoutManager = mView.getLayoutManager();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700969 if (layoutManager instanceof GridLayoutManager) {
970 return ((GridLayoutManager) layoutManager).getSpanCount();
971 }
972
973 // Otherwise, it is a list with 1 column.
974 return 1;
975 }
976
977 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -0700978 public int getRowCount() {
979 int numFullColumns = getChildCount() / getColumnCount();
980 boolean hasPartiallyFullColumn = getChildCount() % getColumnCount() != 0;
Kyle Horimoto2c802572015-08-18 13:25:29 -0700981 return numFullColumns + (hasPartiallyFullColumn ? 1 : 0);
982 }
983
984 @Override
Kyle Horimoto2c802572015-08-18 13:25:29 -0700985 public int getHeight() {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700986 return mView.getHeight();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700987 }
988
989 @Override
990 public void invalidateView() {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700991 mView.invalidate();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700992 }
993
994 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -0700995 public void runAtNextFrame(Runnable r) {
996 mView.postOnAnimation(r);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700997 }
998
999 @Override
1000 public void removeCallback(Runnable r) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001001 mView.removeCallbacks(r);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001002 }
1003
1004 @Override
1005 public void scrollBy(int dy) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001006 mView.scrollBy(0, dy);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001007 }
1008
1009 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -07001010 public void showBand(Rect rect) {
1011 mBand.setBounds(rect);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001012
1013 if (!mIsOverlayShown) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001014 mView.getOverlay().add(mBand);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001015 }
1016 }
1017
1018 @Override
1019 public void hideBand() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001020 mView.getOverlay().remove(mBand);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001021 }
Ben Kwa8250db42015-10-07 14:15:12 -07001022
1023 @Override
Ben Kwa22f479d2016-01-20 15:07:53 -08001024 public boolean isLayoutItem(int pos) {
1025 // The band selection model only operates on documents and directories. Exclude other
1026 // types of adapter items (e.g. whitespace items like dividers).
1027 RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
1028 switch (vh.getItemViewType()) {
1029 case ITEM_TYPE_DOCUMENT:
1030 case ITEM_TYPE_DIRECTORY:
1031 return false;
1032 default:
1033 return true;
1034 }
1035 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001036 }
1037
Steve McKay4b3a13c2015-06-11 10:10:49 -07001038 public interface Callback {
1039 /**
1040 * Called when an item is selected or unselected while in selection mode.
1041 *
1042 * @param position Adapter position of the item that was checked or unchecked
1043 * @param selected <code>true</code> if the item is now selected, <code>false</code>
1044 * if the item is now unselected.
1045 */
Ben Kwa743c7c22015-12-01 19:56:57 -08001046 public void onItemStateChanged(String id, boolean selected);
Steve McKay4b3a13c2015-06-11 10:10:49 -07001047
1048 /**
Steve McKaydbec47a2015-08-12 14:48:34 -07001049 * Called prior to an item changing state. Callbacks can cancel
1050 * the change at {@code position} by returning {@code false}.
1051 *
Ben Kwa743c7c22015-12-01 19:56:57 -08001052 * @param id Adapter position of the item that was checked or unchecked
Steve McKaydbec47a2015-08-12 14:48:34 -07001053 * @param selected <code>true</code> if the item is to be selected, <code>false</code>
1054 * if the item is to be unselected.
Steve McKay4b3a13c2015-06-11 10:10:49 -07001055 */
Ben Kwa743c7c22015-12-01 19:56:57 -08001056 public boolean onBeforeItemStateChange(String id, boolean selected);
Steve McKaydbec47a2015-08-12 14:48:34 -07001057
1058 /**
1059 * Called immediately after completion of any set of changes.
1060 */
1061 public void onSelectionChanged();
Steve McKay4b3a13c2015-06-11 10:10:49 -07001062 }
1063
1064 /**
Kyle Horimoto2c802572015-08-18 13:25:29 -07001065 * Provides mouse driven band-select support when used in conjunction with {@link RecyclerView}
1066 * and {@link MultiSelectManager}. This class is responsible for rendering the band select
1067 * overlay and selecting overlaid items via MultiSelectManager.
1068 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001069 public class BandController extends RecyclerView.OnScrollListener
1070 implements GridModel.OnSelectionChangedListener {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001071
1072 private static final int NOT_SET = -1;
1073
Steve McKay635b0942015-09-03 16:49:51 -07001074 private final Runnable mModelBuilder;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001075
Steve McKay635b0942015-09-03 16:49:51 -07001076 @Nullable private Rect mBounds;
1077 @Nullable private Point mCurrentPosition;
1078 @Nullable private Point mOrigin;
Steve McKayc3ef0d62015-09-08 17:15:25 -07001079 @Nullable private GridModel mModel;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001080
1081 // The time at which the current band selection-induced scroll began. If no scroll is in
1082 // progress, the value is NOT_SET.
1083 private long mScrollStartTime = NOT_SET;
1084 private final Runnable mViewScroller = new ViewScroller();
1085
Steve McKay669ebe72015-10-19 12:04:21 -07001086 public BandController() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001087 mEnvironment.addOnScrollListener(this);
Steve McKay635b0942015-09-03 16:49:51 -07001088
1089 mModelBuilder = new Runnable() {
1090 @Override
1091 public void run() {
Steve McKay44408262016-01-05 15:27:17 -08001092 mModel = new GridModel(mEnvironment, mAdapter);
Steve McKayc3ef0d62015-09-08 17:15:25 -07001093 mModel.addOnSelectionChangedListener(BandController.this);
Steve McKay635b0942015-09-03 16:49:51 -07001094 }
1095 };
1096 }
1097
Steve McKay669ebe72015-10-19 12:04:21 -07001098 public boolean handleEvent(MotionInputEvent e) {
1099 // b/23793622 notes the fact that we *never* receive ACTION_DOWN
1100 // events in onTouchEvent. Where it not for this issue, we'd
1101 // push start handling down into handleInputEvent.
1102 if (mBandManager.shouldStart(e)) {
1103 // endBandSelect is handled in handleInputEvent.
1104 mBandManager.startBandSelect(e.getOrigin());
1105 } else if (mBandManager.isActive()
1106 && e.isMouseEvent()
1107 && e.isActionUp()) {
1108 // Same issue here w b/23793622. The ACTION_UP event
1109 // is only evert dispatched to onTouchEvent when
1110 // there is some associated motion. If a user taps
1111 // mouse, but doesn't move, then band select gets
1112 // started BUT not ended. Causing phantom
1113 // bands to appear when the user later clicks to start
1114 // band select.
1115 mBandManager.processInputEvent(e);
1116 }
1117
1118 return isActive();
1119 }
1120
Steve McKay635b0942015-09-03 16:49:51 -07001121 private boolean isActive() {
1122 return mModel != null;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001123 }
1124
1125 /**
Kyle Horimotod017db02015-08-27 16:44:00 -07001126 * Handle a change in layout by cleaning up and getting rid of the old model and creating
1127 * a new model which will track the new layout.
1128 */
1129 public void handleLayoutChanged() {
Steve McKay635b0942015-09-03 16:49:51 -07001130 if (mModel != null) {
1131 mModel.removeOnSelectionChangedListener(this);
1132 mModel.stopListening();
Kyle Horimotod017db02015-08-27 16:44:00 -07001133
Steve McKay635b0942015-09-03 16:49:51 -07001134 // build a new model, all fresh and happy.
1135 mModelBuilder.run();
1136 }
1137 }
1138
Steve McKay669ebe72015-10-19 12:04:21 -07001139 boolean shouldStart(MotionInputEvent e) {
Steve McKay635b0942015-09-03 16:49:51 -07001140 return !isActive()
Steve McKay669ebe72015-10-19 12:04:21 -07001141 && e.isMouseEvent() // a mouse
1142 && e.isActionDown() // the initial button press
Steve McKay44408262016-01-05 15:27:17 -08001143 && mAdapter.getItemCount() > 0
Steve McKay669ebe72015-10-19 12:04:21 -07001144 && e.getItemPosition() == RecyclerView.NO_ID; // in empty space
Steve McKay635b0942015-09-03 16:49:51 -07001145 }
1146
1147 boolean shouldStop(InputEvent input) {
1148 return isActive()
1149 && input.isMouseEvent()
1150 && input.isActionUp();
Kyle Horimotod017db02015-08-27 16:44:00 -07001151 }
1152
1153 /**
Kyle Horimoto2c802572015-08-18 13:25:29 -07001154 * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
Steve McKay635b0942015-09-03 16:49:51 -07001155 * @param input
Kyle Horimoto2c802572015-08-18 13:25:29 -07001156 */
Steve McKay669ebe72015-10-19 12:04:21 -07001157 private void processInputEvent(InputEvent input) {
Steve McKay635b0942015-09-03 16:49:51 -07001158 checkArgument(input.isMouseEvent());
1159
1160 if (shouldStop(input)) {
Steve McKaya698d632015-09-04 14:51:16 -07001161 endBandSelect();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001162 return;
1163 }
1164
Steve McKay635b0942015-09-03 16:49:51 -07001165 // We shouldn't get any events in this method when band select is not active,
1166 // but it turns some guests show up late to the party.
1167 if (!isActive()) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001168 return;
1169 }
1170
Steve McKay635b0942015-09-03 16:49:51 -07001171 mCurrentPosition = input.getOrigin();
1172 mModel.resizeSelection(input.getOrigin());
Kyle Horimoto2c802572015-08-18 13:25:29 -07001173 scrollViewIfNecessary();
1174 resizeBandSelectRectangle();
1175 }
1176
1177 /**
1178 * Starts band select by adding the drawable to the RecyclerView's overlay.
1179 */
Steve McKay635b0942015-09-03 16:49:51 -07001180 private void startBandSelect(Point origin) {
1181 if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
1182
1183 mOrigin = origin;
1184 mModelBuilder.run(); // Creates a new selection model.
Kyle Horimoto2c802572015-08-18 13:25:29 -07001185 mModel.startSelection(mOrigin);
1186 }
1187
1188 /**
1189 * Scrolls the view if necessary.
1190 */
1191 private void scrollViewIfNecessary() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001192 mEnvironment.removeCallback(mViewScroller);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001193 mViewScroller.run();
Steve McKayc3ef0d62015-09-08 17:15:25 -07001194 mEnvironment.invalidateView();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001195 }
1196
1197 /**
1198 * Resizes the band select rectangle by using the origin and the current pointer position as
1199 * two opposite corners of the selection.
1200 */
1201 private void resizeBandSelectRectangle() {
Steve McKay635b0942015-09-03 16:49:51 -07001202 mBounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
1203 Math.min(mOrigin.y, mCurrentPosition.y),
1204 Math.max(mOrigin.x, mCurrentPosition.x),
1205 Math.max(mOrigin.y, mCurrentPosition.y));
Steve McKayc3ef0d62015-09-08 17:15:25 -07001206 mEnvironment.showBand(mBounds);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001207 }
1208
1209 /**
1210 * Ends band select by removing the overlay.
1211 */
1212 private void endBandSelect() {
1213 if (DEBUG) Log.d(TAG, "Ending band select.");
Steve McKay635b0942015-09-03 16:49:51 -07001214
Steve McKayc3ef0d62015-09-08 17:15:25 -07001215 mEnvironment.hideBand();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001216 mSelection.applyProvisionalSelection();
1217 mModel.endSelection();
Ben Kwafcb54d82015-12-10 15:21:18 -08001218 int firstSelected = mModel.getPositionNearestOrigin();
Ben Kwa20887032015-12-22 15:27:14 -08001219 if (firstSelected != NOT_SET) {
Steve McKay44408262016-01-05 15:27:17 -08001220 if (mSelection.contains(mAdapter.getModelId(firstSelected))) {
Ben Kwa20887032015-12-22 15:27:14 -08001221 // TODO: firstSelected should really be lastSelected, we want to anchor the item
1222 // where the mouse-up occurred.
1223 setSelectionRangeBegin(firstSelected);
1224 } else {
1225 // TODO: Check if this is really happening.
1226 Log.w(TAG, "First selected by band is NOT in selection!");
1227 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001228 }
Steve McKay635b0942015-09-03 16:49:51 -07001229
1230 mModel = null;
1231 mOrigin = null;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001232 }
1233
1234 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -08001235 public void onSelectionChanged(Set<String> updatedSelection) {
1236 Map<String, Boolean> delta = mSelection.setProvisionalSelection(updatedSelection);
1237 for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
1238 notifyItemStateChanged(entry.getKey(), entry.getValue());
Kyle Horimoto2c802572015-08-18 13:25:29 -07001239 }
1240 notifySelectionChanged();
1241 }
1242
1243 private class ViewScroller implements Runnable {
1244 /**
1245 * The number of milliseconds of scrolling at which scroll speed continues to increase.
1246 * At first, the scroll starts slowly; then, the rate of scrolling increases until it
1247 * reaches its maximum value at after this many milliseconds.
1248 */
1249 private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
1250
1251 @Override
1252 public void run() {
1253 // Compute the number of pixels the pointer's y-coordinate is past the view.
1254 // Negative values mean the pointer is at or before the top of the view, and
1255 // positive values mean that the pointer is at or after the bottom of the view. Note
1256 // that one additional pixel is added here so that the view still scrolls when the
1257 // pointer is exactly at the top or bottom.
1258 int pixelsPastView = 0;
Steve McKay635b0942015-09-03 16:49:51 -07001259 if (mCurrentPosition.y <= 0) {
1260 pixelsPastView = mCurrentPosition.y - 1;
Steve McKayc3ef0d62015-09-08 17:15:25 -07001261 } else if (mCurrentPosition.y >= mEnvironment.getHeight() - 1) {
1262 pixelsPastView = mCurrentPosition.y - mEnvironment.getHeight() + 1;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001263 }
1264
Steve McKay635b0942015-09-03 16:49:51 -07001265 if (!isActive() || pixelsPastView == 0) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001266 // If band selection is inactive, or if it is active but not at the edge of the
1267 // view, no scrolling is necessary.
1268 mScrollStartTime = NOT_SET;
1269 return;
1270 }
1271
1272 if (mScrollStartTime == NOT_SET) {
1273 // If the pointer was previously not at the edge of the view but now is, set the
1274 // start time for the scroll.
1275 mScrollStartTime = System.currentTimeMillis();
1276 }
1277
1278 // Compute the number of pixels to scroll, and scroll that many pixels.
1279 final int numPixels = computeScrollDistance(
1280 pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
Steve McKayc3ef0d62015-09-08 17:15:25 -07001281 mEnvironment.scrollBy(numPixels);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001282
Steve McKayc3ef0d62015-09-08 17:15:25 -07001283 mEnvironment.removeCallback(mViewScroller);
1284 mEnvironment.runAtNextFrame(this);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001285 }
1286
1287 /**
1288 * Computes the number of pixels to scroll based on how far the pointer is past the end
1289 * of the view and how long it has been there. Roughly based on ItemTouchHelper's
1290 * algorithm for computing the number of pixels to scroll when an item is dragged to the
1291 * end of a {@link RecyclerView}.
1292 * @param pixelsPastView
1293 * @param scrollDuration
1294 * @return
1295 */
1296 private int computeScrollDistance(int pixelsPastView, long scrollDuration) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001297 final int maxScrollStep = mEnvironment.getHeight();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001298 final int direction = (int) Math.signum(pixelsPastView);
1299 final int absPastView = Math.abs(pixelsPastView);
1300
1301 // Calculate the ratio of how far out of the view the pointer currently resides to
1302 // the entire height of the view.
1303 final float outOfBoundsRatio = Math.min(
Steve McKayc3ef0d62015-09-08 17:15:25 -07001304 1.0f, (float) absPastView / mEnvironment.getHeight());
Kyle Horimoto2c802572015-08-18 13:25:29 -07001305 // Interpolate this ratio and use it to compute the maximum scroll that should be
1306 // possible for this step.
1307 final float cappedScrollStep =
1308 direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);
1309
1310 // Likewise, calculate the ratio of the time spent in the scroll to the limit.
1311 final float timeRatio = Math.min(
1312 1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
1313 // Interpolate this ratio and use it to compute the final number of pixels to
1314 // scroll.
1315 final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));
1316
1317 // If the final number of pixels to scroll ends up being 0, the view should still
1318 // scroll at least one pixel.
1319 return numPixels != 0 ? numPixels : direction;
1320 }
1321
1322 /**
1323 * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
1324 * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
1325 * drags that are at the edge or barely past the edge of the view still cause sufficient
1326 * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if
1327 * needed.
1328 * @param ratio A ratio which is in the range [0, 1].
1329 * @return A "smoothed" value, also in the range [0, 1].
1330 */
1331 private float smoothOutOfBoundsRatio(float ratio) {
1332 return (float) Math.pow(ratio - 1.0f, 5) + 1.0f;
1333 }
1334
1335 /**
1336 * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1)
1337 * and stays close to 0 for most input values except those very close to 1. This ensures
1338 * that scrolls start out very slowly but speed up drastically after the scroll has been
1339 * in progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used,
1340 * but this could also be tweaked if needed.
1341 * @param ratio A ratio which is in the range [0, 1].
1342 * @return A "smoothed" value, also in the range [0, 1].
1343 */
1344 private float smoothTimeRatio(float ratio) {
1345 return (float) Math.pow(ratio, 5);
1346 }
1347 };
Kyle Horimotoea910a12015-08-25 17:12:42 -07001348
1349 @Override
1350 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
Steve McKay635b0942015-09-03 16:49:51 -07001351 if (!isActive()) {
Kyle Horimotoea910a12015-08-25 17:12:42 -07001352 return;
1353 }
1354
1355 // Adjust the y-coordinate of the origin the opposite number of pixels so that the
1356 // origin remains in the same place relative to the view's items.
1357 mOrigin.y -= dy;
1358 resizeBandSelectRectangle();
1359 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001360 }
1361
1362 /**
1363 * Provides a band selection item model for views within a RecyclerView. This class queries the
1364 * RecyclerView to determine where its items are placed; then, once band selection is underway,
1365 * it alerts listeners of which items are covered by the selections.
1366 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001367 public static final class GridModel extends RecyclerView.OnScrollListener {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001368
1369 public static final int NOT_SET = -1;
1370
1371 // Enum values used to determine the corner at which the origin is located within the
1372 private static final int UPPER = 0x00;
1373 private static final int LOWER = 0x01;
1374 private static final int LEFT = 0x00;
1375 private static final int RIGHT = 0x02;
1376 private static final int UPPER_LEFT = UPPER | LEFT;
1377 private static final int UPPER_RIGHT = UPPER | RIGHT;
1378 private static final int LOWER_LEFT = LOWER | LEFT;
1379 private static final int LOWER_RIGHT = LOWER | RIGHT;
1380
Ben Kwa8250db42015-10-07 14:15:12 -07001381 private final SelectionEnvironment mHelper;
Steve McKay44408262016-01-05 15:27:17 -08001382 private final DocumentsAdapter mAdapter;
1383
Steve McKayc3ef0d62015-09-08 17:15:25 -07001384 private final List<OnSelectionChangedListener> mOnSelectionChangedListeners =
1385 new ArrayList<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001386
Kyle Horimoto786babe2015-08-25 21:03:38 -07001387 // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed
1388 // by their y-offset. For example, if the first column of the view starts at an x-value of 5,
1389 // mColumns.get(5) would return an array of positions in that column. Within that array, the
1390 // value for key y is the adapter position for the item whose y-offset is y.
Ben Kwafcb54d82015-12-10 15:21:18 -08001391 private final SparseArray<SparseIntArray> mColumns = new SparseArray<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001392
Steve McKayc3ef0d62015-09-08 17:15:25 -07001393 // List of limits along the x-axis (columns).
1394 // This list is sorted from furthest left to furthest right.
1395 private final List<Limits> mColumnBounds = new ArrayList<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001396
Steve McKayc3ef0d62015-09-08 17:15:25 -07001397 // List of limits along the y-axis (rows). Note that this list only contains items which
1398 // have been in the viewport.
1399 private final List<Limits> mRowBounds = new ArrayList<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001400
1401 // The adapter positions which have been recorded so far.
Ben Kwafcb54d82015-12-10 15:21:18 -08001402 private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001403
1404 // Array passed to registered OnSelectionChangedListeners. One array is created and reused
1405 // throughout the lifetime of the object.
Ben Kwa743c7c22015-12-01 19:56:57 -08001406 private final Set<String> mSelection = new HashSet<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001407
1408 // The current pointer (in absolute positioning from the top of the view).
1409 private Point mPointer = null;
1410
1411 // The bounds of the band selection.
1412 private RelativePoint mRelativeOrigin;
1413 private RelativePoint mRelativePointer;
1414
1415 private boolean mIsActive;
1416
1417 // Tracks where the band select originated from. This is used to determine where selections
1418 // should expand from when Shift+click is used.
Ben Kwafcb54d82015-12-10 15:21:18 -08001419 private int mPositionNearestOrigin = NOT_SET;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001420
Steve McKay44408262016-01-05 15:27:17 -08001421 GridModel(SelectionEnvironment helper, DocumentsAdapter adapter) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001422 mHelper = helper;
Steve McKay44408262016-01-05 15:27:17 -08001423 mAdapter = adapter;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001424 mHelper.addOnScrollListener(this);
1425 }
1426
1427 /**
1428 * Stops listening to the view's scrolls. Call this function before discarding a
1429 * BandSelecModel object to prevent memory leaks.
1430 */
1431 void stopListening() {
1432 mHelper.removeOnScrollListener(this);
1433 }
1434
1435 /**
1436 * Start a band select operation at the given point.
1437 * @param relativeOrigin The origin of the band select operation, relative to the viewport.
1438 * For example, if the view is scrolled to the bottom, the top-left of the viewport
1439 * would have a relative origin of (0, 0), even though its absolute point has a higher
1440 * y-value.
1441 */
1442 void startSelection(Point relativeOrigin) {
1443 mIsActive = true;
1444 mPointer = mHelper.createAbsolutePoint(relativeOrigin);
1445
1446 recordVisibleChildren();
1447 mRelativeOrigin = new RelativePoint(mPointer);
1448 mRelativePointer = new RelativePoint(mPointer);
1449 computeCurrentSelection();
1450 notifyListeners();
1451 }
1452
1453 /**
1454 * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
1455 * opposite the origin.
1456 * @param relativePointer The pointer (opposite of the origin) of the band select operation,
1457 * relative to the viewport. For example, if the view is scrolled to the bottom, the
1458 * top-left of the viewport would have a relative origin of (0, 0), even though its
1459 * absolute point has a higher y-value.
1460 */
1461 void resizeSelection(Point relativePointer) {
1462 mPointer = mHelper.createAbsolutePoint(relativePointer);
1463 updateModel();
1464 }
1465
1466 /**
1467 * Ends the band selection.
1468 */
1469 void endSelection() {
1470 mIsActive = false;
1471 }
1472
1473 /**
1474 * @return The adapter position for the item nearest the origin corresponding to the latest
1475 * band select operation, or NOT_SET if the selection did not cover any items.
1476 */
Ben Kwafcb54d82015-12-10 15:21:18 -08001477 int getPositionNearestOrigin() {
1478 return mPositionNearestOrigin;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001479 }
1480
1481 @Override
1482 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
1483 if (!mIsActive) {
1484 return;
1485 }
1486
1487 mPointer.x += dx;
1488 mPointer.y += dy;
1489 recordVisibleChildren();
1490 updateModel();
1491 }
1492
1493 /**
1494 * Queries the view for all children and records their location metadata.
1495 */
1496 private void recordVisibleChildren() {
1497 for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001498 int adapterPosition = mHelper.getAdapterPositionAt(i);
Ben Kwa22f479d2016-01-20 15:07:53 -08001499 if (!mHelper.isLayoutItem(adapterPosition) &&
1500 !mKnownPositions.get(adapterPosition)) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001501 mKnownPositions.put(adapterPosition, true);
1502 recordItemData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001503 }
1504 }
1505 }
1506
1507 /**
1508 * Updates the limits lists and column map with the given item metadata.
1509 * @param absoluteChildRect The absolute rectangle for the child view being processed.
1510 * @param adapterPosition The position of the child view being processed.
1511 */
Ben Kwafcb54d82015-12-10 15:21:18 -08001512 private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001513 if (mColumnBounds.size() != mHelper.getColumnCount()) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001514 // If not all x-limits have been recorded, record this one.
1515 recordLimits(
Steve McKayc3ef0d62015-09-08 17:15:25 -07001516 mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right));
Kyle Horimoto2c802572015-08-18 13:25:29 -07001517 }
1518
Steve McKayc3ef0d62015-09-08 17:15:25 -07001519 if (mRowBounds.size() != mHelper.getRowCount()) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001520 // If not all y-limits have been recorded, record this one.
1521 recordLimits(
Steve McKayc3ef0d62015-09-08 17:15:25 -07001522 mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
Kyle Horimoto2c802572015-08-18 13:25:29 -07001523 }
1524
Ben Kwafcb54d82015-12-10 15:21:18 -08001525 SparseIntArray columnList = mColumns.get(absoluteChildRect.left);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001526 if (columnList == null) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001527 columnList = new SparseIntArray();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001528 mColumns.put(absoluteChildRect.left, columnList);
1529 }
Ben Kwafcb54d82015-12-10 15:21:18 -08001530 columnList.put(absoluteChildRect.top, adapterPosition);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001531 }
1532
1533 /**
1534 * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
1535 * does not exist.
1536 */
1537 private void recordLimits(List<Limits> limitsList, Limits limits) {
1538 int index = Collections.binarySearch(limitsList, limits);
1539 if (index < 0) {
1540 limitsList.add(~index, limits);
1541 }
1542 }
1543
1544 /**
1545 * Handles a moved pointer; this function determines whether the pointer movement resulted
1546 * in a selection change and, if it has, notifies listeners of this change.
1547 */
1548 private void updateModel() {
1549 RelativePoint old = mRelativePointer;
1550 mRelativePointer = new RelativePoint(mPointer);
1551 if (old != null && mRelativePointer.equals(old)) {
1552 return;
1553 }
1554
1555 computeCurrentSelection();
1556 notifyListeners();
1557 }
1558
1559 /**
1560 * Computes the currently-selected items.
1561 */
1562 private void computeCurrentSelection() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001563 if (areItemsCoveredByBand(mRelativePointer, mRelativeOrigin)) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001564 updateSelection(computeBounds());
1565 } else {
1566 mSelection.clear();
Ben Kwafcb54d82015-12-10 15:21:18 -08001567 mPositionNearestOrigin = NOT_SET;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001568 }
1569 }
1570
1571 /**
1572 * Notifies all listeners of a selection change. Note that this function simply passes
1573 * mSelection, so computeCurrentSelection() should be called before this
1574 * function.
1575 */
1576 private void notifyListeners() {
1577 for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
1578 listener.onSelectionChanged(mSelection);
1579 }
1580 }
1581
1582 /**
1583 * @param rect Rectangle including all covered items.
1584 */
1585 private void updateSelection(Rect rect) {
Ben Kwa22f479d2016-01-20 15:07:53 -08001586 int columnStart =
Steve McKayc3ef0d62015-09-08 17:15:25 -07001587 Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
Ben Kwa22f479d2016-01-20 15:07:53 -08001588 checkState(columnStart >= 0);
1589 int columnEnd = columnStart;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001590
Ben Kwa22f479d2016-01-20 15:07:53 -08001591 for (int i = columnStart; i < mColumnBounds.size()
Steve McKayc3ef0d62015-09-08 17:15:25 -07001592 && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
Ben Kwa22f479d2016-01-20 15:07:53 -08001593 columnEnd = i;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001594 }
1595
Ben Kwa22f479d2016-01-20 15:07:53 -08001596 int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
1597 if (rowStart < 0) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001598 mPositionNearestOrigin = NOT_SET;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001599 return;
1600 }
1601
Ben Kwa22f479d2016-01-20 15:07:53 -08001602 int rowEnd = rowStart;
1603 for (int i = rowStart; i < mRowBounds.size()
1604 && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
1605 rowEnd = i;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001606 }
1607
Ben Kwa22f479d2016-01-20 15:07:53 -08001608 updateSelection(columnStart, columnEnd, rowStart, rowEnd);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001609 }
1610
1611 /**
1612 * Computes the selection given the previously-computed start- and end-indices for each
1613 * row and column.
1614 */
1615 private void updateSelection(
1616 int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
Ben Kwa22f479d2016-01-20 15:07:53 -08001617 if (DEBUG) Log.d(TAG, String.format("updateSelection: %d, %d, %d, %d",
1618 columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
1619
Kyle Horimoto2c802572015-08-18 13:25:29 -07001620 mSelection.clear();
1621 for (int column = columnStartIndex; column <= columnEndIndex; column++) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001622 SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001623 for (int row = rowStartIndex; row <= rowEndIndex; row++) {
Ben Kwa20887032015-12-22 15:27:14 -08001624 // The default return value for SparseIntArray.get is 0, which is a valid
1625 // position. Use a sentry value to prevent erroneously selecting item 0.
Ben Kwa22f479d2016-01-20 15:07:53 -08001626 final int rowKey = mRowBounds.get(row).lowerLimit;
1627 int position = items.get(rowKey, NOT_SET);
Ben Kwa20887032015-12-22 15:27:14 -08001628 if (position != NOT_SET) {
Steve McKay44408262016-01-05 15:27:17 -08001629 String id = mAdapter.getModelId(position);
Ben Kwa20887032015-12-22 15:27:14 -08001630 if (id != null) {
1631 // The adapter inserts items for UI layout purposes that aren't associated
1632 // with files. Those will have a null model ID. Don't select them.
1633 mSelection.add(id);
1634 }
1635 if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
1636 row, rowStartIndex, rowEndIndex)) {
1637 // If this is the position nearest the origin, record it now so that it
1638 // can be returned by endSelection() later.
1639 mPositionNearestOrigin = position;
1640 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001641 }
1642 }
1643 }
1644 }
1645
1646 /**
1647 * @return Returns true if the position is the nearest to the origin, or, in the case of the
1648 * lower-right corner, whether it is possible that the position is the nearest to the
1649 * origin. See comment below for reasoning for this special case.
1650 */
1651 private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
1652 int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
1653 int corner = computeCornerNearestOrigin();
1654 switch (corner) {
1655 case UPPER_LEFT:
1656 return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
1657 case UPPER_RIGHT:
1658 return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
1659 case LOWER_LEFT:
1660 return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
1661 case LOWER_RIGHT:
1662 // Note that in some cases, the last row will not have as many items as there
1663 // are columns (e.g., if there are 4 items and 3 columns, the second row will
1664 // only have one item in the first column). This function is invoked for each
1665 // position from left to right, so return true for any position in the bottom
1666 // row and only the right-most position in the bottom row will be recorded.
1667 return rowIndex == rowEndIndex;
1668 default:
1669 throw new RuntimeException("Invalid corner type.");
1670 }
1671 }
1672
1673 /**
1674 * Listener for changes in which items have been band selected.
1675 */
1676 static interface OnSelectionChangedListener {
Ben Kwa743c7c22015-12-01 19:56:57 -08001677 public void onSelectionChanged(Set<String> updatedSelection);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001678 }
1679
1680 void addOnSelectionChangedListener(OnSelectionChangedListener listener) {
1681 mOnSelectionChangedListeners.add(listener);
1682 }
1683
1684 void removeOnSelectionChangedListener(OnSelectionChangedListener listener) {
1685 mOnSelectionChangedListeners.remove(listener);
1686 }
1687
1688 /**
Kyle Horimoto2c802572015-08-18 13:25:29 -07001689 * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
1690 * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
1691 * of item columns and the top- and bottom sides of item rows so that it can be determined
1692 * whether the pointer is located within the bounds of an item.
1693 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001694 private static class Limits implements Comparable<Limits> {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001695 int lowerLimit;
1696 int upperLimit;
1697
1698 Limits(int lowerLimit, int upperLimit) {
1699 this.lowerLimit = lowerLimit;
1700 this.upperLimit = upperLimit;
1701 }
1702
1703 @Override
1704 public int compareTo(Limits other) {
1705 return lowerLimit - other.lowerLimit;
1706 }
1707
1708 @Override
1709 public boolean equals(Object other) {
1710 if (!(other instanceof Limits)) {
1711 return false;
1712 }
1713
1714 return ((Limits) other).lowerLimit == lowerLimit &&
1715 ((Limits) other).upperLimit == upperLimit;
1716 }
1717 }
1718
1719 /**
1720 * The location of a coordinate relative to items. This class represents a general area of the
1721 * view as it relates to band selection rather than an explicit point. For example, two
1722 * different points within an item are considered to have the same "location" because band
1723 * selection originating within the item would select the same items no matter which point
1724 * was used. Same goes for points between items as well as those at the very beginning or end
1725 * of the view.
1726 *
1727 * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
1728 * advantage of tying the value to the Limits of items along that axis. This allows easy
1729 * selection of items within those Limits as opposed to a search through every item to see if a
1730 * given coordinate value falls within those Limits.
1731 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001732 private static class RelativeCoordinate
Kyle Horimoto2c802572015-08-18 13:25:29 -07001733 implements Comparable<RelativeCoordinate> {
1734 /**
1735 * Location describing points after the last known item.
1736 */
1737 static final int AFTER_LAST_ITEM = 0;
1738
1739 /**
1740 * Location describing points before the first known item.
1741 */
1742 static final int BEFORE_FIRST_ITEM = 1;
1743
1744 /**
1745 * Location describing points between two items.
1746 */
1747 static final int BETWEEN_TWO_ITEMS = 2;
1748
1749 /**
1750 * Location describing points within the limits of one item.
1751 */
1752 static final int WITHIN_LIMITS = 3;
1753
1754 /**
1755 * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
1756 * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
1757 */
1758 final int type;
1759
1760 /**
1761 * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
1762 * BETWEEN_TWO_ITEMS.
1763 */
1764 Limits limitsBeforeCoordinate;
1765
1766 /**
1767 * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
1768 */
1769 Limits limitsAfterCoordinate;
1770
1771 // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
1772 Limits mFirstKnownItem;
1773 // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
1774 Limits mLastKnownItem;
1775
1776 /**
1777 * @param limitsList The sorted limits list for the coordinate type. If this
1778 * CoordinateLocation is an x-value, mXLimitsList should be passed; otherwise,
1779 * mYLimitsList should be pased.
1780 * @param value The coordinate value.
1781 */
1782 RelativeCoordinate(List<Limits> limitsList, int value) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001783 int index = Collections.binarySearch(limitsList, new Limits(value, value));
Kyle Horimoto2c802572015-08-18 13:25:29 -07001784
1785 if (index >= 0) {
1786 this.type = WITHIN_LIMITS;
1787 this.limitsBeforeCoordinate = limitsList.get(index);
1788 } else if (~index == 0) {
1789 this.type = BEFORE_FIRST_ITEM;
1790 this.mFirstKnownItem = limitsList.get(0);
1791 } else if (~index == limitsList.size()) {
1792 Limits lastLimits = limitsList.get(limitsList.size() - 1);
1793 if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
1794 this.type = WITHIN_LIMITS;
1795 this.limitsBeforeCoordinate = lastLimits;
1796 } else {
1797 this.type = AFTER_LAST_ITEM;
1798 this.mLastKnownItem = lastLimits;
1799 }
1800 } else {
1801 Limits limitsBeforeIndex = limitsList.get(~index - 1);
1802 if (limitsBeforeIndex.lowerLimit <= value && value <= limitsBeforeIndex.upperLimit) {
1803 this.type = WITHIN_LIMITS;
1804 this.limitsBeforeCoordinate = limitsList.get(~index - 1);
1805 } else {
1806 this.type = BETWEEN_TWO_ITEMS;
1807 this.limitsBeforeCoordinate = limitsList.get(~index - 1);
1808 this.limitsAfterCoordinate = limitsList.get(~index);
1809 }
1810 }
1811 }
1812
1813 int toComparisonValue() {
1814 if (type == BEFORE_FIRST_ITEM) {
1815 return mFirstKnownItem.lowerLimit - 1;
1816 } else if (type == AFTER_LAST_ITEM) {
1817 return mLastKnownItem.upperLimit + 1;
1818 } else if (type == BETWEEN_TWO_ITEMS) {
1819 return limitsBeforeCoordinate.upperLimit + 1;
1820 } else {
1821 return limitsBeforeCoordinate.lowerLimit;
1822 }
1823 }
1824
1825 @Override
1826 public boolean equals(Object other) {
1827 if (!(other instanceof RelativeCoordinate)) {
1828 return false;
1829 }
1830
1831 RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
1832 return toComparisonValue() == otherCoordinate.toComparisonValue();
1833 }
1834
1835 @Override
1836 public int compareTo(RelativeCoordinate other) {
1837 return toComparisonValue() - other.toComparisonValue();
1838 }
1839 }
1840
1841 /**
1842 * The location of a point relative to the Limits of nearby items; consists of both an x- and
1843 * y-RelativeCoordinateLocation.
1844 */
1845 private class RelativePoint {
1846 final RelativeCoordinate xLocation;
1847 final RelativeCoordinate yLocation;
1848
1849 RelativePoint(Point point) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001850 this.xLocation = new RelativeCoordinate(mColumnBounds, point.x);
1851 this.yLocation = new RelativeCoordinate(mRowBounds, point.y);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001852 }
1853
1854 @Override
1855 public boolean equals(Object other) {
1856 if (!(other instanceof RelativePoint)) {
1857 return false;
1858 }
1859
1860 RelativePoint otherPoint = (RelativePoint) other;
1861 return xLocation.equals(otherPoint.xLocation) && yLocation.equals(otherPoint.yLocation);
1862 }
1863 }
1864
1865 /**
1866 * Generates a rectangle which contains the items selected by the pointer and origin.
1867 * @return The rectangle, or null if no items were selected.
1868 */
1869 private Rect computeBounds() {
1870 Rect rect = new Rect();
1871 rect.left = getCoordinateValue(
1872 min(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001873 mColumnBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001874 true);
1875 rect.right = getCoordinateValue(
1876 max(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001877 mColumnBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001878 false);
1879 rect.top = getCoordinateValue(
1880 min(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001881 mRowBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001882 true);
1883 rect.bottom = getCoordinateValue(
1884 max(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001885 mRowBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001886 false);
1887 return rect;
1888 }
1889
1890 /**
1891 * Computes the corner of the selection nearest the origin.
1892 * @return
1893 */
1894 private int computeCornerNearestOrigin() {
1895 int cornerValue = 0;
1896
1897 if (mRelativeOrigin.yLocation ==
1898 min(mRelativeOrigin.yLocation, mRelativePointer.yLocation)) {
1899 cornerValue |= UPPER;
1900 } else {
1901 cornerValue |= LOWER;
1902 }
1903
1904 if (mRelativeOrigin.xLocation ==
1905 min(mRelativeOrigin.xLocation, mRelativePointer.xLocation)) {
1906 cornerValue |= LEFT;
1907 } else {
1908 cornerValue |= RIGHT;
1909 }
1910
1911 return cornerValue;
1912 }
1913
1914 private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) {
1915 return first.compareTo(second) < 0 ? first : second;
1916 }
1917
1918 private RelativeCoordinate max(RelativeCoordinate first, RelativeCoordinate second) {
1919 return first.compareTo(second) > 0 ? first : second;
1920 }
1921
1922 /**
1923 * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
1924 * coordinate.
1925 */
1926 private int getCoordinateValue(RelativeCoordinate coordinate,
1927 List<Limits> limitsList, boolean isStartOfRange) {
1928 switch (coordinate.type) {
1929 case RelativeCoordinate.BEFORE_FIRST_ITEM:
1930 return limitsList.get(0).lowerLimit;
1931 case RelativeCoordinate.AFTER_LAST_ITEM:
1932 return limitsList.get(limitsList.size() - 1).upperLimit;
1933 case RelativeCoordinate.BETWEEN_TWO_ITEMS:
1934 if (isStartOfRange) {
1935 return coordinate.limitsAfterCoordinate.lowerLimit;
1936 } else {
1937 return coordinate.limitsBeforeCoordinate.upperLimit;
1938 }
1939 case RelativeCoordinate.WITHIN_LIMITS:
1940 return coordinate.limitsBeforeCoordinate.lowerLimit;
1941 }
1942
1943 throw new RuntimeException("Invalid coordinate value.");
1944 }
1945
Steve McKayc3ef0d62015-09-08 17:15:25 -07001946 private boolean areItemsCoveredByBand(
Kyle Horimoto2c802572015-08-18 13:25:29 -07001947 RelativePoint first, RelativePoint second) {
1948 return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) &&
1949 doesCoordinateLocationCoverItems(first.yLocation, second.yLocation);
1950 }
1951
1952 private boolean doesCoordinateLocationCoverItems(
1953 RelativeCoordinate pointerCoordinate,
1954 RelativeCoordinate originCoordinate) {
1955 if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM &&
1956 originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
1957 return false;
1958 }
1959
1960 if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM &&
1961 originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
1962 return false;
1963 }
1964
1965 if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
1966 originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
1967 pointerCoordinate.limitsBeforeCoordinate.equals(
1968 originCoordinate.limitsBeforeCoordinate) &&
1969 pointerCoordinate.limitsAfterCoordinate.equals(
1970 originCoordinate.limitsAfterCoordinate)) {
1971 return false;
1972 }
1973
1974 return true;
1975 }
1976 }
Steve McKay4b3a13c2015-06-11 10:10:49 -07001977}