blob: b0cc09a134f9e41465f721e88571623dc8440e0a [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 McKayb04b1642015-07-24 13:14:20 -070022
Steve McKaye852d932016-02-08 19:09:42 -080023import android.annotation.IntDef;
Steve McKay635b0942015-09-03 16:49:51 -070024import android.graphics.Point;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
Steve McKaye852d932016-02-08 19:09:42 -080027import android.os.Parcel;
28import android.os.Parcelable;
Steve McKay635b0942015-09-03 16:49:51 -070029import android.support.annotation.Nullable;
30import android.support.annotation.VisibleForTesting;
Kyle Horimoto2c802572015-08-18 13:25:29 -070031import android.support.v7.widget.GridLayoutManager;
Steve McKay4b3a13c2015-06-11 10:10:49 -070032import android.support.v7.widget.RecyclerView;
Steve McKay4b3a13c2015-06-11 10:10:49 -070033import android.util.Log;
Kyle Horimoto786babe2015-08-25 21:03:38 -070034import android.util.SparseArray;
Ben Kwafcb54d82015-12-10 15:21:18 -080035import android.util.SparseBooleanArray;
36import android.util.SparseIntArray;
Steve McKay4b3a13c2015-06-11 10:10:49 -070037import android.view.MotionEvent;
38import android.view.View;
Steve McKay635b0942015-09-03 16:49:51 -070039
40import com.android.documentsui.Events.InputEvent;
41import com.android.documentsui.Events.MotionInputEvent;
Steve McKay57a93ba2015-11-11 16:26:59 +090042import com.android.documentsui.R;
Steve McKay4b3a13c2015-06-11 10:10:49 -070043
Steve McKaye852d932016-02-08 19:09:42 -080044import java.lang.annotation.Retention;
45import java.lang.annotation.RetentionPolicy;
Steve McKay4b3a13c2015-06-11 10:10:49 -070046import java.util.ArrayList;
Ben Kwafcb54d82015-12-10 15:21:18 -080047import java.util.Collection;
Kyle Horimoto2c802572015-08-18 13:25:29 -070048import java.util.Collections;
Ben Kwa743c7c22015-12-01 19:56:57 -080049import java.util.HashMap;
50import java.util.HashSet;
Steve McKay4b3a13c2015-06-11 10:10:49 -070051import java.util.List;
Ben Kwa743c7c22015-12-01 19:56:57 -080052import java.util.Map;
53import java.util.Set;
Steve McKay4b3a13c2015-06-11 10:10:49 -070054
55/**
Steve McKaydbec47a2015-08-12 14:48:34 -070056 * MultiSelectManager provides support traditional multi-item selection support to RecyclerView.
57 * Additionally it can be configured to restrict selection to a single element, @see
58 * #setSelectMode.
Steve McKay4b3a13c2015-06-11 10:10:49 -070059 */
Ben Kwa67924892016-01-27 09:58:36 -080060public final class MultiSelectManager {
Steve McKay4b3a13c2015-06-11 10:10:49 -070061
Steve McKaye852d932016-02-08 19:09:42 -080062 @IntDef(flag = true, value = {
63 MODE_MULTIPLE,
64 MODE_SINGLE
65 })
66 @Retention(RetentionPolicy.SOURCE)
67 public @interface SelectionMode {}
Steve McKaydbec47a2015-08-12 14:48:34 -070068 public static final int MODE_MULTIPLE = 0;
Steve McKaydbec47a2015-08-12 14:48:34 -070069 public static final int MODE_SINGLE = 1;
70
Steve McKay4b3a13c2015-06-11 10:10:49 -070071 private static final String TAG = "MultiSelectManager";
Steve McKay4b3a13c2015-06-11 10:10:49 -070072
73 private final Selection mSelection = new Selection();
Steve McKaye63dce72015-07-28 19:20:01 -070074
Steve McKay44408262016-01-05 15:27:17 -080075 private final SelectionEnvironment mEnvironment;
76 private final DocumentsAdapter mAdapter;
Steve McKay4b3a13c2015-06-11 10:10:49 -070077 private final List<MultiSelectManager.Callback> mCallbacks = new ArrayList<>(1);
78
Steve McKay44408262016-01-05 15:27:17 -080079 private Range mRanger;
Steve McKaydbec47a2015-08-12 14:48:34 -070080 private boolean mSingleSelect;
Steve McKaya698d632015-09-04 14:51:16 -070081
Steve McKayc3ef0d62015-09-08 17:15:25 -070082 @Nullable private BandController mBandManager;
Steve McKay4b3a13c2015-06-11 10:10:49 -070083
Steve McKay44408262016-01-05 15:27:17 -080084
Steve McKay4b3a13c2015-06-11 10:10:49 -070085 /**
Steve McKaye852d932016-02-08 19:09:42 -080086 * @param mode Selection single or multiple selection mode.
87 * @param initialSelection selection state probably preserved in external state.
Steve McKay4b3a13c2015-06-11 10:10:49 -070088 */
Steve McKay44408262016-01-05 15:27:17 -080089 public MultiSelectManager(
Steve McKaye852d932016-02-08 19:09:42 -080090 final RecyclerView recyclerView,
91 DocumentsAdapter adapter,
92 @SelectionMode int mode,
93 @Nullable Selection initialSelection) {
94
95 this(new RuntimeSelectionEnvironment(recyclerView), adapter, mode, initialSelection);
Ben Kwa8250db42015-10-07 14:15:12 -070096
Steve McKaya698d632015-09-04 14:51:16 -070097 if (mode == MODE_MULTIPLE) {
Steve McKaye852d932016-02-08 19:09:42 -080098 // TODO: Don't load this on low memory devices.
Steve McKay669ebe72015-10-19 12:04:21 -070099 mBandManager = new BandController();
Steve McKaya698d632015-09-04 14:51:16 -0700100 }
Kyle Horimoto2c802572015-08-18 13:25:29 -0700101
Steve McKay4b3a13c2015-06-11 10:10:49 -0700102 recyclerView.addOnItemTouchListener(
103 new RecyclerView.OnItemTouchListener() {
Steve McKay635b0942015-09-03 16:49:51 -0700104 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -0700105 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
Steve McKay669ebe72015-10-19 12:04:21 -0700106 if (mBandManager != null) {
107 return mBandManager.handleEvent(new MotionInputEvent(e, recyclerView));
Steve McKaya698d632015-09-04 14:51:16 -0700108 }
Steve McKay669ebe72015-10-19 12:04:21 -0700109 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700110 }
Steve McKay635b0942015-09-03 16:49:51 -0700111
112 @Override
Kyle Horimoto2c802572015-08-18 13:25:29 -0700113 public void onTouchEvent(RecyclerView rv, MotionEvent e) {
Steve McKay669ebe72015-10-19 12:04:21 -0700114 mBandManager.processInputEvent(
Steve McKay635b0942015-09-03 16:49:51 -0700115 new MotionInputEvent(e, recyclerView));
Kyle Horimoto2c802572015-08-18 13:25:29 -0700116 }
Steve McKay635b0942015-09-03 16:49:51 -0700117 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -0700118 public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
119 });
120 }
121
Steve McKayb04b1642015-07-24 13:14:20 -0700122 /**
123 * Constructs a new instance with {@code adapter} and {@code helper}.
Steve McKay57a93ba2015-11-11 16:26:59 +0900124 * @param runtimeSelectionEnvironment
Steve McKayb04b1642015-07-24 13:14:20 -0700125 * @hide
126 */
127 @VisibleForTesting
Steve McKaye852d932016-02-08 19:09:42 -0800128 MultiSelectManager(
129 SelectionEnvironment environment,
130 DocumentsAdapter adapter,
131 @SelectionMode int mode,
132 @Nullable Selection initialSelection) {
133
Steve McKaya1f76802016-02-25 13:34:03 -0800134 assert(environment != null);
135 assert(adapter != null);
136
137 mEnvironment = environment;
138 mAdapter = adapter;
139
Steve McKaydbec47a2015-08-12 14:48:34 -0700140 mSingleSelect = mode == MODE_SINGLE;
Steve McKaye852d932016-02-08 19:09:42 -0800141 if (initialSelection != null) {
142 mSelection.copyFrom(initialSelection);
143 }
Steve McKaydbec47a2015-08-12 14:48:34 -0700144
Steve McKay44408262016-01-05 15:27:17 -0800145 mAdapter.registerAdapterDataObserver(
Ben Kwa743c7c22015-12-01 19:56:57 -0800146 new RecyclerView.AdapterDataObserver() {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700147
Ben Kwafcb54d82015-12-10 15:21:18 -0800148 private List<String> mModelIds;
Ben Kwadb65cd52015-12-09 14:33:49 -0800149
Steve McKay4b3a13c2015-06-11 10:10:49 -0700150 @Override
151 public void onChanged() {
Steve McKay44408262016-01-05 15:27:17 -0800152 mModelIds = mAdapter.getModelIds();
Ben Kwad4d0e702016-02-01 09:11:55 -0800153
154 // Update the selection to remove any disappeared IDs.
155 mSelection.cancelProvisionalSelection();
156 mSelection.intersect(mModelIds);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700157 }
158
159 @Override
160 public void onItemRangeChanged(
Ben Kwa743c7c22015-12-01 19:56:57 -0800161 int startPosition, int itemCount, Object payload) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700162 // No change in position. Ignoring.
163 }
164
165 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800166 public void onItemRangeInserted(int startPosition, int itemCount) {
167 mSelection.cancelProvisionalSelection();
Steve McKay4b3a13c2015-06-11 10:10:49 -0700168 }
169
170 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800171 public void onItemRangeRemoved(int startPosition, int itemCount) {
Steve McKaya1f76802016-02-25 13:34:03 -0800172 assert(startPosition >= 0);
173 assert(itemCount > 0);
Ben Kwafcb54d82015-12-10 15:21:18 -0800174
Ben Kwa743c7c22015-12-01 19:56:57 -0800175 mSelection.cancelProvisionalSelection();
Ben Kwa743c7c22015-12-01 19:56:57 -0800176 // Remove any disappeared IDs from the selection.
Ben Kwafcb54d82015-12-10 15:21:18 -0800177 mSelection.intersect(mModelIds);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700178 }
179
180 @Override
181 public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
182 throw new UnsupportedOperationException();
183 }
184 });
185 }
186
Steve McKayb04b1642015-07-24 13:14:20 -0700187 /**
188 * Adds {@code callback} such that it will be notified when {@code MultiSelectManager}
189 * events occur.
190 *
191 * @param callback
192 */
Steve McKay4b3a13c2015-06-11 10:10:49 -0700193 public void addCallback(MultiSelectManager.Callback callback) {
194 mCallbacks.add(callback);
195 }
196
Ben Kwabd964562015-10-14 08:00:27 -0700197 public boolean hasSelection() {
198 return !mSelection.isEmpty();
199 }
200
Steve McKay4b3a13c2015-06-11 10:10:49 -0700201 /**
202 * Returns a Selection object that provides a live view
Ben Kwac64cb252015-08-27 16:04:46 -0700203 * on the current selection.
Steve McKay4b3a13c2015-06-11 10:10:49 -0700204 *
Ben Kwac64cb252015-08-27 16:04:46 -0700205 * @see #getSelection(Selection) on how to get a snapshot
Steve McKay4b3a13c2015-06-11 10:10:49 -0700206 * of the selection that will not reflect future changes
207 * to selection.
208 *
Ben Kwac64cb252015-08-27 16:04:46 -0700209 * @return The current selection.
Steve McKay4b3a13c2015-06-11 10:10:49 -0700210 */
211 public Selection getSelection() {
212 return mSelection;
213 }
214
215 /**
216 * Updates {@code dest} to reflect the current selection.
217 * @param dest
218 *
219 * @return The Selection instance passed in, for convenience.
220 */
221 public Selection getSelection(Selection dest) {
222 dest.copyFrom(mSelection);
223 return dest;
224 }
225
Steve McKayb04b1642015-07-24 13:14:20 -0700226 /**
Steve McKaye852d932016-02-08 19:09:42 -0800227 * Updates selection to include items in {@code selection}.
228 */
229 public void updateSelection(Selection selection) {
230 setItemsSelected(selection.toList(), true);
231 }
232
233 /**
Steve McKaydbec47a2015-08-12 14:48:34 -0700234 * Sets the selected state of the specified items. Note that the callback will NOT
235 * be consulted to see if an item can be selected.
236 *
Ben Kwa743c7c22015-12-01 19:56:57 -0800237 * @param ids
238 * @param selected
239 * @return
Steve McKayb04b1642015-07-24 13:14:20 -0700240 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800241 public boolean setItemsSelected(Iterable<String> ids, boolean selected) {
Steve McKayb04b1642015-07-24 13:14:20 -0700242 boolean changed = false;
Ben Kwa743c7c22015-12-01 19:56:57 -0800243 for (String id: ids) {
244 boolean itemChanged = selected ? mSelection.add(id) : mSelection.remove(id);
Steve McKaydbec47a2015-08-12 14:48:34 -0700245 if (itemChanged) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800246 notifyItemStateChanged(id, selected);
Steve McKaydbec47a2015-08-12 14:48:34 -0700247 }
248 changed |= itemChanged;
Steve McKayb04b1642015-07-24 13:14:20 -0700249 }
Steve McKaydbec47a2015-08-12 14:48:34 -0700250 notifySelectionChanged();
Steve McKayb04b1642015-07-24 13:14:20 -0700251 return changed;
252 }
253
254 /**
Steve McKaydbec47a2015-08-12 14:48:34 -0700255 * Clears the selection and notifies (even if nothing changes).
Steve McKayb04b1642015-07-24 13:14:20 -0700256 */
Steve McKay4b3a13c2015-06-11 10:10:49 -0700257 public void clearSelection() {
Steve McKaydbec47a2015-08-12 14:48:34 -0700258 clearSelectionQuietly();
259 notifySelectionChanged();
260 }
261
Kyle Horimotod017db02015-08-27 16:44:00 -0700262 public void handleLayoutChanged() {
Steve McKaya698d632015-09-04 14:51:16 -0700263 if (mBandManager != null) {
264 mBandManager.handleLayoutChanged();
265 }
Kyle Horimotod017db02015-08-27 16:44:00 -0700266 }
267
Steve McKaydbec47a2015-08-12 14:48:34 -0700268 /**
Ben Kwa67924892016-01-27 09:58:36 -0800269 * Clears the selection, without notifying selection listeners. UI elements still need to be
270 * notified about state changes so that they can update their appearance.
Steve McKaydbec47a2015-08-12 14:48:34 -0700271 */
272 private void clearSelectionQuietly() {
Steve McKaye63dce72015-07-28 19:20:01 -0700273 mRanger = null;
274
Ben Kwabd964562015-10-14 08:00:27 -0700275 if (!hasSelection()) {
Steve McKayb04b1642015-07-24 13:14:20 -0700276 return;
277 }
Ben Kwa743c7c22015-12-01 19:56:57 -0800278
Ben Kwa67924892016-01-27 09:58:36 -0800279 Selection oldSelection = getSelection(new Selection());
Steve McKay4b3a13c2015-06-11 10:10:49 -0700280 mSelection.clear();
281
Ben Kwa67924892016-01-27 09:58:36 -0800282 for (String id: oldSelection.getAll()) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800283 notifyItemStateChanged(id, false);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700284 }
285 }
286
Steve McKay635b0942015-09-03 16:49:51 -0700287 @VisibleForTesting
288 void onLongPress(InputEvent input) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700289 if (DEBUG) Log.d(TAG, "Handling long press event.");
290
Steve McKay635b0942015-09-03 16:49:51 -0700291 if (!input.isOverItem()) {
292 if (DEBUG) Log.i(TAG, "Cannot handle tap. No adapter position available.");
Steve McKay4b3a13c2015-06-11 10:10:49 -0700293 }
294
Steve McKay635b0942015-09-03 16:49:51 -0700295 handleAdapterEvent(input);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700296 }
297
Steve McKay4b3a13c2015-06-11 10:10:49 -0700298 @VisibleForTesting
Steve McKay635b0942015-09-03 16:49:51 -0700299 boolean onSingleTapUp(InputEvent input) {
300 if (DEBUG) Log.d(TAG, "Processing tap event.");
Ben Kwabd964562015-10-14 08:00:27 -0700301 if (!hasSelection()) {
Ben Kwa0436a752016-01-15 10:43:24 -0800302 // No selection active - do nothing.
Steve McKay5353a1e2015-07-30 12:27:44 -0700303 return false;
304 }
305
Steve McKay635b0942015-09-03 16:49:51 -0700306 if (!input.isOverItem()) {
307 if (DEBUG) Log.d(TAG, "Activity has no position. Canceling selection.");
Steve McKay5353a1e2015-07-30 12:27:44 -0700308 clearSelection();
309 return false;
310 }
311
Steve McKay635b0942015-09-03 16:49:51 -0700312 handleAdapterEvent(input);
Steve McKay6c86b192015-09-02 15:59:28 -0700313 return true;
Kyle Horimoto2c802572015-08-18 13:25:29 -0700314 }
315
316 /**
317 * Handles a change caused by a click on the item with the given position. If the Shift key is
318 * held down, this performs a range select; otherwise, it simply toggles the item's selection
319 * state.
320 */
Steve McKay635b0942015-09-03 16:49:51 -0700321 private void handleAdapterEvent(InputEvent input) {
322 if (mRanger != null && input.isShiftKeyDown()) {
323 mRanger.snapSelection(input.getItemPosition());
Steve McKay5353a1e2015-07-30 12:27:44 -0700324
Kyle Horimoto2c802572015-08-18 13:25:29 -0700325 // We're being lazy here notifying even when something might not have changed.
326 // To make this more correct, we'd need to update the Ranger class to return
327 // information about what has changed.
328 notifySelectionChanged();
Ben Kwabd7da4c2015-09-25 07:47:56 -0700329 } else {
Ben Kwa743c7c22015-12-01 19:56:57 -0800330 int position = input.getItemPosition();
331 toggleSelection(position);
332 setSelectionRangeBegin(position);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700333 }
Steve McKay5353a1e2015-07-30 12:27:44 -0700334 }
335
336 /**
Ben Kwa743c7c22015-12-01 19:56:57 -0800337 * A convenience method for toggling selection by adapter position.
Steve McKaye63dce72015-07-28 19:20:01 -0700338 *
Ben Kwa743c7c22015-12-01 19:56:57 -0800339 * @param position Adapter position to toggle.
Steve McKaye63dce72015-07-28 19:20:01 -0700340 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800341 private void toggleSelection(int position) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700342 // Position may be special "no position" during certain
343 // transitional phases. If so, skip handling of the event.
344 if (position == RecyclerView.NO_POSITION) {
345 if (DEBUG) Log.d(TAG, "Ignoring toggle for element with no position.");
Ben Kwabd7da4c2015-09-25 07:47:56 -0700346 return;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700347 }
Steve McKay44408262016-01-05 15:27:17 -0800348 String id = mAdapter.getModelId(position);
Steve McKayef16f5f2015-12-22 18:15:31 -0800349 if (id != null) {
350 toggleSelection(id);
351 }
Ben Kwa743c7c22015-12-01 19:56:57 -0800352 }
Steve McKay4b3a13c2015-06-11 10:10:49 -0700353
Ben Kwa743c7c22015-12-01 19:56:57 -0800354 /**
355 * Toggles selection on the item with the given model ID.
356 *
357 * @param modelId
358 */
359 public void toggleSelection(String modelId) {
Steve McKaya1f76802016-02-25 13:34:03 -0800360 assert(modelId != null);
361
Steve McKaydbec47a2015-08-12 14:48:34 -0700362 boolean changed = false;
Ben Kwa743c7c22015-12-01 19:56:57 -0800363 if (mSelection.contains(modelId)) {
364 changed = attemptDeselect(modelId);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700365 } else {
Ben Kwa67924892016-01-27 09:58:36 -0800366 changed = attemptSelect(modelId);
Steve McKaye63dce72015-07-28 19:20:01 -0700367 }
Steve McKaydbec47a2015-08-12 14:48:34 -0700368
Ben Kwabd7da4c2015-09-25 07:47:56 -0700369 if (changed) {
370 notifySelectionChanged();
371 }
Steve McKaye63dce72015-07-28 19:20:01 -0700372 }
373
374 /**
Ben Kwa83df50f2016-02-10 14:01:19 -0800375 * Starts a range selection. If a range selection is already active, this will start a new range
376 * selection (which will reset the range anchor).
Ben Kwa67924892016-01-27 09:58:36 -0800377 *
Ben Kwa83df50f2016-02-10 14:01:19 -0800378 * @param pos The anchor position for the selection range.
Ben Kwa67924892016-01-27 09:58:36 -0800379 */
Ben Kwa83df50f2016-02-10 14:01:19 -0800380 void startRangeSelection(int pos) {
381 attemptSelect(mAdapter.getModelId(pos));
382 setSelectionRangeBegin(pos);
383 }
Ben Kwa67924892016-01-27 09:58:36 -0800384
Ben Kwa83df50f2016-02-10 14:01:19 -0800385 /**
386 * Sets the end point for the current range selection, started by a call to
387 * {@link #startRangeSelection(int)}. This function should only be called when a range selection
388 * is active (see {@link #isRangeSelectionActive()}. Items in the range [anchor, end] will be
389 * selected.
390 *
391 * @param pos The new end position for the selection range.
392 */
393 void snapRangeSelection(int pos) {
Steve McKaya1f76802016-02-25 13:34:03 -0800394 assert(mRanger != null);
395
Ben Kwa83df50f2016-02-10 14:01:19 -0800396 mRanger.snapSelection(pos);
Ben Kwa67924892016-01-27 09:58:36 -0800397 notifySelectionChanged();
398 }
399
400 /**
Ben Kwa83df50f2016-02-10 14:01:19 -0800401 * Stops an in-progress range selection.
402 */
403 void endRangeSelection() {
404 mRanger = null;
405 }
406
407 /**
Ben Kwa67924892016-01-27 09:58:36 -0800408 * @return Whether or not there is a current range selection active.
409 */
Ben Kwa83df50f2016-02-10 14:01:19 -0800410 boolean isRangeSelectionActive() {
Ben Kwa67924892016-01-27 09:58:36 -0800411 return mRanger != null;
412 }
413
414 /**
415 * Sets the magic location at which a selection range begins (the selection anchor). This value
416 * is consulted when determining how to extend, and modify selection ranges. Calling this when a
417 * range selection is active will reset the range selection.
Steve McKay5353a1e2015-07-30 12:27:44 -0700418 *
419 * @throws IllegalStateException if {@code position} is not already be selected
420 * @param position
421 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800422 void setSelectionRangeBegin(int position) {
Ben Kwa20887032015-12-22 15:27:14 -0800423 if (position == RecyclerView.NO_POSITION) {
424 return;
425 }
426
Steve McKay44408262016-01-05 15:27:17 -0800427 if (mSelection.contains(mAdapter.getModelId(position))) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800428 mRanger = new Range(position);
429 }
Steve McKay5353a1e2015-07-30 12:27:44 -0700430 }
431
432 /**
Ben Kwa743c7c22015-12-01 19:56:57 -0800433 * Try to set selection state for all elements in range. Not that callbacks can cancel selection
434 * of specific items, so some or even all items may not reflect the desired state after the
435 * update is complete.
Steve McKaye63dce72015-07-28 19:20:01 -0700436 *
Ben Kwa743c7c22015-12-01 19:56:57 -0800437 * @param begin Adapter position for range start (inclusive).
438 * @param end Adapter position for range end (inclusive).
439 * @param selected New selection state.
Steve McKaye63dce72015-07-28 19:20:01 -0700440 */
441 private void updateRange(int begin, int end, boolean selected) {
Steve McKaya1f76802016-02-25 13:34:03 -0800442 assert(end >= begin);
Steve McKaye63dce72015-07-28 19:20:01 -0700443 for (int i = begin; i <= end; i++) {
Steve McKay44408262016-01-05 15:27:17 -0800444 String id = mAdapter.getModelId(i);
Steve McKayef16f5f2015-12-22 18:15:31 -0800445 if (id == null) {
446 continue;
447 }
448
Steve McKaye63dce72015-07-28 19:20:01 -0700449 if (selected) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800450 boolean canSelect = notifyBeforeItemStateChange(id, true);
Steve McKaydbec47a2015-08-12 14:48:34 -0700451 if (canSelect) {
Ben Kwabd964562015-10-14 08:00:27 -0700452 if (mSingleSelect && hasSelection()) {
Steve McKaydbec47a2015-08-12 14:48:34 -0700453 clearSelectionQuietly();
454 }
Ben Kwa743c7c22015-12-01 19:56:57 -0800455 selectAndNotify(id);
Steve McKaydbec47a2015-08-12 14:48:34 -0700456 }
Steve McKaye63dce72015-07-28 19:20:01 -0700457 } else {
Ben Kwa743c7c22015-12-01 19:56:57 -0800458 attemptDeselect(id);
Steve McKaye63dce72015-07-28 19:20:01 -0700459 }
460 }
461 }
462
463 /**
Ben Kwa743c7c22015-12-01 19:56:57 -0800464 * @param modelId
Steve McKaye63dce72015-07-28 19:20:01 -0700465 * @return True if the update was applied.
466 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800467 private boolean selectAndNotify(String modelId) {
468 boolean changed = mSelection.add(modelId);
Steve McKaydbec47a2015-08-12 14:48:34 -0700469 if (changed) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800470 notifyItemStateChanged(modelId, true);
Steve McKaye63dce72015-07-28 19:20:01 -0700471 }
Steve McKaydbec47a2015-08-12 14:48:34 -0700472 return changed;
Steve McKaye63dce72015-07-28 19:20:01 -0700473 }
474
475 /**
Ben Kwa743c7c22015-12-01 19:56:57 -0800476 * @param id
Steve McKaye63dce72015-07-28 19:20:01 -0700477 * @return True if the update was applied.
478 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800479 private boolean attemptDeselect(String id) {
Steve McKaya1f76802016-02-25 13:34:03 -0800480 assert(id != null);
Ben Kwa743c7c22015-12-01 19:56:57 -0800481 if (notifyBeforeItemStateChange(id, false)) {
482 mSelection.remove(id);
483 notifyItemStateChanged(id, false);
Steve McKaye63dce72015-07-28 19:20:01 -0700484 if (DEBUG) Log.d(TAG, "Selection after deselect: " + mSelection);
485 return true;
486 } else {
487 if (DEBUG) Log.d(TAG, "Select cancelled by listener.");
488 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700489 }
490 }
491
Ben Kwa67924892016-01-27 09:58:36 -0800492 /**
493 * @param id
494 * @return True if the update was applied.
495 */
496 private boolean attemptSelect(String id) {
Steve McKaya1f76802016-02-25 13:34:03 -0800497 assert(id != null);
Ben Kwa67924892016-01-27 09:58:36 -0800498 boolean canSelect = notifyBeforeItemStateChange(id, true);
499 if (!canSelect) {
500 return false;
501 }
502 if (mSingleSelect && hasSelection()) {
503 clearSelectionQuietly();
504 }
505
506 selectAndNotify(id);
507 return true;
508 }
509
Ben Kwa743c7c22015-12-01 19:56:57 -0800510 private boolean notifyBeforeItemStateChange(String id, boolean nextState) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700511 int lastListener = mCallbacks.size() - 1;
512 for (int i = lastListener; i > -1; i--) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800513 if (!mCallbacks.get(i).onBeforeItemStateChange(id, nextState)) {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700514 return false;
515 }
516 }
517 return true;
518 }
519
520 /**
Steve McKaydbec47a2015-08-12 14:48:34 -0700521 * Notifies registered listeners when the selection status of a single item
522 * (identified by {@code position}) changes.
Steve McKay4b3a13c2015-06-11 10:10:49 -0700523 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800524 private void notifyItemStateChanged(String id, boolean selected) {
Steve McKaya1f76802016-02-25 13:34:03 -0800525 assert(id != null);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700526 int lastListener = mCallbacks.size() - 1;
527 for (int i = lastListener; i > -1; i--) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800528 mCallbacks.get(i).onItemStateChanged(id, selected);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700529 }
Steve McKay44408262016-01-05 15:27:17 -0800530 mAdapter.onItemSelectionChanged(id);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700531 }
532
533 /**
Steve McKaydbec47a2015-08-12 14:48:34 -0700534 * Notifies registered listeners when the selection has changed. This
535 * notification should be sent only once a full series of changes
536 * is complete, e.g. clearingSelection, or updating the single
537 * selection from one item to another.
538 */
539 private void notifySelectionChanged() {
540 int lastListener = mCallbacks.size() - 1;
541 for (int i = lastListener; i > -1; i--) {
542 mCallbacks.get(i).onSelectionChanged();
543 }
544 }
545
546 /**
Steve McKaye63dce72015-07-28 19:20:01 -0700547 * Class providing support for managing range selections.
548 */
Steve McKay5353a1e2015-07-30 12:27:44 -0700549 private final class Range {
Steve McKaye63dce72015-07-28 19:20:01 -0700550 private static final int UNDEFINED = -1;
551
552 final int mBegin;
553 int mEnd = UNDEFINED;
554
Steve McKay5353a1e2015-07-30 12:27:44 -0700555 public Range(int begin) {
Steve McKaydbec47a2015-08-12 14:48:34 -0700556 if (DEBUG) Log.d(TAG, "New Ranger created beginning @ " + begin);
Steve McKaye63dce72015-07-28 19:20:01 -0700557 mBegin = begin;
558 }
559
Steve McKay5353a1e2015-07-30 12:27:44 -0700560 private void snapSelection(int position) {
Steve McKaya1f76802016-02-25 13:34:03 -0800561 assert(mRanger != null);
562 assert(position != RecyclerView.NO_POSITION);
Steve McKaye63dce72015-07-28 19:20:01 -0700563
564 if (mEnd == UNDEFINED || mEnd == mBegin) {
565 // Reset mEnd so it can be established in establishRange.
566 mEnd = UNDEFINED;
567 establishRange(position);
568 } else {
569 reviseRange(position);
570 }
571 }
572
573 private void establishRange(int position) {
Steve McKaya1f76802016-02-25 13:34:03 -0800574 assert(mRanger.mEnd == UNDEFINED);
Steve McKaye63dce72015-07-28 19:20:01 -0700575
576 if (position == mBegin) {
577 mEnd = position;
578 }
579
580 if (position > mBegin) {
581 updateRange(mBegin + 1, position, true);
582 } else if (position < mBegin) {
583 updateRange(position, mBegin - 1, true);
584 }
585
586 mEnd = position;
587 }
588
589 private void reviseRange(int position) {
Steve McKaya1f76802016-02-25 13:34:03 -0800590 assert(mEnd != UNDEFINED);
591 assert(mBegin != mEnd);
Steve McKaye63dce72015-07-28 19:20:01 -0700592
593 if (position == mEnd) {
594 if (DEBUG) Log.i(TAG, "Skipping no-op revision click on mEndRange.");
595 }
596
597 if (mEnd > mBegin) {
598 reviseAscendingRange(position);
599 } else if (mEnd < mBegin) {
600 reviseDescendingRange(position);
601 }
602 // the "else" case is covered by checkState at beginning of method.
603
604 mEnd = position;
605 }
606
607 /**
608 * Updates an existing ascending seleciton.
609 * @param position
610 */
611 private void reviseAscendingRange(int position) {
612 // Reducing or reversing the range....
613 if (position < mEnd) {
614 if (position < mBegin) {
615 updateRange(mBegin + 1, mEnd, false);
616 updateRange(position, mBegin -1, true);
617 } else {
618 updateRange(position + 1, mEnd, false);
619 }
620 }
621
622 // Extending the range...
623 else if (position > mEnd) {
624 updateRange(mEnd + 1, position, true);
625 }
626 }
627
628 private void reviseDescendingRange(int position) {
629 // Reducing or reversing the range....
630 if (position > mEnd) {
631 if (position > mBegin) {
632 updateRange(mEnd, mBegin - 1, false);
633 updateRange(mBegin + 1, position, true);
634 } else {
635 updateRange(mEnd, position - 1, false);
636 }
637 }
638
639 // Extending the range...
640 else if (position < mEnd) {
641 updateRange(position, mEnd - 1, true);
642 }
643 }
644 }
645
646 /**
Steve McKayb04b1642015-07-24 13:14:20 -0700647 * Object representing the current selection. Provides read only access
648 * public access, and private write access.
Steve McKay4b3a13c2015-06-11 10:10:49 -0700649 */
Steve McKaye852d932016-02-08 19:09:42 -0800650 public static final class Selection implements Parcelable {
Steve McKay4b3a13c2015-06-11 10:10:49 -0700651
Ben Kwa743c7c22015-12-01 19:56:57 -0800652 // This class tracks selected items by managing two sets: the saved selection, and the total
653 // selection. Saved selections are those which have been completed by tapping an item or by
654 // completing a band select operation. Provisional selections are selections which have been
655 // temporarily created by an in-progress band select operation (once the user releases the
656 // mouse button during a band select operation, the selected items become saved). The total
657 // selection is the combination of both the saved selection and the provisional
658 // selection. Tracking both separately is necessary to ensure that saved selections do not
659 // become deselected when they are removed from the provisional selection; for example, if
660 // item A is tapped (and selected), then an in-progress band select covers A then uncovers
661 // A, A should still be selected as it has been saved. To ensure this behavior, the saved
662 // selection must be tracked separately.
Steve McKay2fbb40e2016-02-19 10:57:08 -0800663 private final Set<String> mSelection;
664 private final Set<String> mProvisionalSelection;
Steve McKaye852d932016-02-08 19:09:42 -0800665 private String mDirectoryKey;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700666
Steve McKay2fbb40e2016-02-19 10:57:08 -0800667 public Selection() {
668 mSelection = new HashSet<String>();
669 mProvisionalSelection = new HashSet<String>();
670 }
671
672 /**
673 * Used by CREATOR.
674 */
675 private Selection(String directoryKey, List<String> selection) {
676 mDirectoryKey = directoryKey;
677 mSelection = new HashSet<String>(selection);
678 mProvisionalSelection = new HashSet<String>();
Steve McKayedd05752015-10-21 14:38:54 -0700679 }
680
Steve McKay4b3a13c2015-06-11 10:10:49 -0700681 /**
Ben Kwa743c7c22015-12-01 19:56:57 -0800682 * @param id
Steve McKay4b3a13c2015-06-11 10:10:49 -0700683 * @return true if the position is currently selected.
684 */
Steve McKayef16f5f2015-12-22 18:15:31 -0800685 public boolean contains(@Nullable String id) {
Steve McKaye852d932016-02-08 19:09:42 -0800686 return mSelection.contains(id) || mProvisionalSelection.contains(id);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700687 }
688
689 /**
Steve McKayedd05752015-10-21 14:38:54 -0700690 * Returns an unordered array of selected positions.
691 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800692 public String[] getAll() {
Steve McKaye852d932016-02-08 19:09:42 -0800693 return toList().toArray(new String[0]);
694 }
695
696 /**
697 * Returns an unordered array of selected positions (including any
698 * provisional selections current in effect).
699 */
Aga Wronska893390b2016-02-17 13:50:42 -0800700 public List<String> toList() {
Steve McKaye852d932016-02-08 19:09:42 -0800701 ArrayList<String> selection = new ArrayList<String>(mSelection);
702 selection.addAll(mProvisionalSelection);
703 return selection;
Steve McKayedd05752015-10-21 14:38:54 -0700704 }
705
706 /**
Steve McKay4b3a13c2015-06-11 10:10:49 -0700707 * @return size of the selection.
708 */
709 public int size() {
Steve McKaye852d932016-02-08 19:09:42 -0800710 return mSelection.size() + mProvisionalSelection.size();
Steve McKay4b3a13c2015-06-11 10:10:49 -0700711 }
712
Steve McKayb04b1642015-07-24 13:14:20 -0700713 /**
714 * @return true if the selection is empty.
715 */
716 public boolean isEmpty() {
Steve McKaye852d932016-02-08 19:09:42 -0800717 return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700718 }
719
720 /**
721 * Sets the provisional selection, which is a temporary selection that can be saved,
722 * canceled, or adjusted at a later time. When a new provision selection is applied, the old
723 * one (if it exists) is abandoned.
Steve McKaye852d932016-02-08 19:09:42 -0800724 * @return Map of ids added or removed. Added ids have a value of true, removed are false.
Kyle Horimoto2c802572015-08-18 13:25:29 -0700725 */
726 @VisibleForTesting
Steve McKaye852d932016-02-08 19:09:42 -0800727 protected Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800728 Map<String, Boolean> delta = new HashMap<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700729
Steve McKaye852d932016-02-08 19:09:42 -0800730 for (String id: mProvisionalSelection) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800731 // Mark each item that used to be in the selection but is unsaved and not in the new
732 // provisional selection.
Steve McKaye852d932016-02-08 19:09:42 -0800733 if (!newSelection.contains(id) && !mSelection.contains(id)) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800734 delta.put(id, false);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700735 }
736 }
737
Steve McKaye852d932016-02-08 19:09:42 -0800738 for (String id: mSelection) {
739 // Mark each item that used to be in the selection but is unsaved and not in the new
740 // provisional selection.
741 if (!newSelection.contains(id)) {
742 delta.put(id, false);
743 }
744 }
745
746 for (String id: newSelection) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800747 // Mark each item that was not previously in the selection but is in the new
748 // provisional selection.
Steve McKaye852d932016-02-08 19:09:42 -0800749 if (!mSelection.contains(id) && !mProvisionalSelection.contains(id)) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800750 delta.put(id, true);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700751 }
752 }
753
Ben Kwa743c7c22015-12-01 19:56:57 -0800754 // Now, iterate through the changes and actually add/remove them to/from the current
755 // selection. This could not be done in the previous loops because changing the size of
756 // the selection mid-iteration changes iteration order erroneously.
757 for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
758 String id = entry.getKey();
759 if (entry.getValue()) {
Steve McKaye852d932016-02-08 19:09:42 -0800760 mProvisionalSelection.add(id);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700761 } else {
Steve McKaye852d932016-02-08 19:09:42 -0800762 mProvisionalSelection.remove(id);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700763 }
764 }
765
766 return delta;
767 }
768
769 /**
770 * Saves the existing provisional selection. Once the provisional selection is saved,
771 * subsequent provisional selections which are different from this existing one cannot
772 * cause items in this existing provisional selection to become deselected.
773 */
774 @VisibleForTesting
775 protected void applyProvisionalSelection() {
Steve McKaye852d932016-02-08 19:09:42 -0800776 mSelection.addAll(mProvisionalSelection);
777 mProvisionalSelection.clear();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700778 }
779
780 /**
781 * Abandons the existing provisional selection so that all items provisionally selected are
782 * now deselected.
783 */
784 @VisibleForTesting
Ben Kwa743c7c22015-12-01 19:56:57 -0800785 void cancelProvisionalSelection() {
Steve McKaye852d932016-02-08 19:09:42 -0800786 mProvisionalSelection.clear();
Steve McKayb04b1642015-07-24 13:14:20 -0700787 }
788
Steve McKay4b3a13c2015-06-11 10:10:49 -0700789 /** @hide */
790 @VisibleForTesting
Ben Kwa743c7c22015-12-01 19:56:57 -0800791 boolean add(String id) {
Steve McKaye852d932016-02-08 19:09:42 -0800792 if (!mSelection.contains(id)) {
793 mSelection.add(id);
Steve McKayb04b1642015-07-24 13:14:20 -0700794 return true;
795 }
796 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700797 }
798
799 /** @hide */
800 @VisibleForTesting
Ben Kwa743c7c22015-12-01 19:56:57 -0800801 boolean remove(String id) {
Steve McKaye852d932016-02-08 19:09:42 -0800802 if (mSelection.contains(id)) {
803 mSelection.remove(id);
Steve McKayb04b1642015-07-24 13:14:20 -0700804 return true;
805 }
806 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700807 }
808
Steve McKayf8621552015-11-03 15:23:16 -0800809 public void clear() {
Steve McKaye852d932016-02-08 19:09:42 -0800810 mSelection.clear();
Steve McKay4b3a13c2015-06-11 10:10:49 -0700811 }
812
Ben Kwafcb54d82015-12-10 15:21:18 -0800813 /**
814 * Trims this selection to be the intersection of itself with the set of given IDs.
815 */
816 public void intersect(Collection<String> ids) {
Steve McKaye852d932016-02-08 19:09:42 -0800817 mSelection.retainAll(ids);
818 mProvisionalSelection.retainAll(ids);
Ben Kwafcb54d82015-12-10 15:21:18 -0800819 }
820
Steve McKay4b3a13c2015-06-11 10:10:49 -0700821 @VisibleForTesting
822 void copyFrom(Selection source) {
Steve McKay2fbb40e2016-02-19 10:57:08 -0800823 mSelection.clear();
824 mSelection.addAll(source.mSelection);
825
826 mProvisionalSelection.clear();
827 mProvisionalSelection.addAll(source.mProvisionalSelection);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700828 }
829
830 @Override
831 public String toString() {
832 if (size() <= 0) {
833 return "size=0, items=[]";
834 }
835
Steve McKaye852d932016-02-08 19:09:42 -0800836 StringBuilder buffer = new StringBuilder(size() * 28);
837 buffer.append("Selection{")
838 .append("applied{size=" + mSelection.size())
839 .append(", entries=" + mSelection)
840 .append("}, provisional{size=" + mProvisionalSelection.size())
841 .append(", entries=" + mProvisionalSelection)
842 .append("}}");
Steve McKay4b3a13c2015-06-11 10:10:49 -0700843 return buffer.toString();
844 }
845
846 @Override
Steve McKay3b409d02015-07-22 11:42:14 -0700847 public int hashCode() {
Steve McKaye852d932016-02-08 19:09:42 -0800848 return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
Steve McKay3b409d02015-07-22 11:42:14 -0700849 }
850
851 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -0700852 public boolean equals(Object that) {
853 if (this == that) {
854 return true;
855 }
856
Steve McKay3b409d02015-07-22 11:42:14 -0700857 if (!(that instanceof Selection)) {
858 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700859 }
Steve McKay3b409d02015-07-22 11:42:14 -0700860
Steve McKaye852d932016-02-08 19:09:42 -0800861 return mSelection.equals(((Selection) that).mSelection) &&
862 mProvisionalSelection.equals(((Selection) that).mProvisionalSelection);
863 }
864
865 /**
866 * Sets the state key for this selection, which allows us to match selections
867 * to particular states (of DirectoryFragment). Basically this lets us avoid
868 * loading a persisted selection in the wrong directory.
869 */
870 public void setDirectoryKey(String key) {
871 mDirectoryKey = key;
872 }
873
874 /**
875 * Sets the state key for this selection, which allows us to match selections
876 * to particular states (of DirectoryFragment). Basically this lets us avoid
877 * loading a persisted selection in the wrong directory.
878 */
879 public boolean hasDirectoryKey(String key) {
880 return key.equals(mDirectoryKey);
881 }
882
883 @Override
884 public int describeContents() {
885 return 0;
886 }
887
888 public void writeToParcel(Parcel dest, int flags) {
Steve McKaye852d932016-02-08 19:09:42 -0800889 dest.writeString(mDirectoryKey);
890 dest.writeList(new ArrayList<>(mSelection));
891 // We don't include provisional selection since it is
892 // typically coupled to some other runtime state (like a band).
Steve McKay4b3a13c2015-06-11 10:10:49 -0700893 }
Steve McKay2fbb40e2016-02-19 10:57:08 -0800894
895 public static final ClassLoaderCreator<Selection> CREATOR =
896 new ClassLoaderCreator<Selection>() {
897 @Override
898 public Selection createFromParcel(Parcel in) {
899 return createFromParcel(in, null);
900 }
901
902 @Override
903 public Selection createFromParcel(Parcel in, ClassLoader loader) {
904 return new Selection(
905 in.readString(),
906 (ArrayList<String>) in.readArrayList(loader));
907 }
908
909 @Override
910 public Selection[] newArray(int size) {
911 return new Selection[size];
912 }
913 };
Steve McKay4b3a13c2015-06-11 10:10:49 -0700914 }
915
Kyle Horimoto2c802572015-08-18 13:25:29 -0700916 /**
Steve McKayc3ef0d62015-09-08 17:15:25 -0700917 * Provides functionality for BandController. Exists primarily to tests that are
918 * fully isolated from RecyclerView.
Kyle Horimoto2c802572015-08-18 13:25:29 -0700919 */
Ben Kwa8250db42015-10-07 14:15:12 -0700920 interface SelectionEnvironment {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700921 void showBand(Rect rect);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700922 void hideBand();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700923 void addOnScrollListener(RecyclerView.OnScrollListener listener);
Steve McKayc3ef0d62015-09-08 17:15:25 -0700924 void removeOnScrollListener(RecyclerView.OnScrollListener listener);
925 void scrollBy(int dy);
926 int getHeight();
927 void invalidateView();
928 void runAtNextFrame(Runnable r);
929 void removeCallback(Runnable r);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700930 Point createAbsolutePoint(Point relativePoint);
931 Rect getAbsoluteRectForChildViewAt(int index);
Ben Kwafcb54d82015-12-10 15:21:18 -0800932 int getAdapterPositionAt(int index);
Steve McKayc3ef0d62015-09-08 17:15:25 -0700933 int getColumnCount();
934 int getRowCount();
935 int getChildCount();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700936 int getVisibleChildCount();
Ben Kwa22f479d2016-01-20 15:07:53 -0800937 /**
938 * Layout items are excluded from the GridModel.
939 */
940 boolean isLayoutItem(int adapterPosition);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700941 }
942
Steve McKay57a93ba2015-11-11 16:26:59 +0900943 /** Recycler view facade implementation backed by good ol' RecyclerView. */
Ben Kwa8250db42015-10-07 14:15:12 -0700944 private static final class RuntimeSelectionEnvironment implements SelectionEnvironment {
Kyle Horimoto2c802572015-08-18 13:25:29 -0700945
Steve McKayc3ef0d62015-09-08 17:15:25 -0700946 private final RecyclerView mView;
947 private final Drawable mBand;
Kyle Horimoto2c802572015-08-18 13:25:29 -0700948
949 private boolean mIsOverlayShown = false;
950
Steve McKay44408262016-01-05 15:27:17 -0800951 RuntimeSelectionEnvironment(RecyclerView view) {
952 mView = view;
Steve McKayc3ef0d62015-09-08 17:15:25 -0700953 mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700954 }
955
956 @Override
Ben Kwafcb54d82015-12-10 15:21:18 -0800957 public int getAdapterPositionAt(int index) {
Ben Kwa67924892016-01-27 09:58:36 -0800958 return mView.getChildAdapterPosition(mView.getChildAt(index));
Kyle Horimoto2c802572015-08-18 13:25:29 -0700959 }
960
961 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800962 public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700963 mView.addOnScrollListener(listener);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700964 }
965
966 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800967 public void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700968 mView.removeOnScrollListener(listener);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700969 }
970
971 @Override
972 public Point createAbsolutePoint(Point relativePoint) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700973 return new Point(relativePoint.x + mView.computeHorizontalScrollOffset(),
974 relativePoint.y + mView.computeVerticalScrollOffset());
Kyle Horimoto2c802572015-08-18 13:25:29 -0700975 }
976
977 @Override
978 public Rect getAbsoluteRectForChildViewAt(int index) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700979 final View child = mView.getChildAt(index);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700980 final Rect childRect = new Rect();
981 child.getHitRect(childRect);
Steve McKayc3ef0d62015-09-08 17:15:25 -0700982 childRect.left += mView.computeHorizontalScrollOffset();
983 childRect.right += mView.computeHorizontalScrollOffset();
984 childRect.top += mView.computeVerticalScrollOffset();
985 childRect.bottom += mView.computeVerticalScrollOffset();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700986 return childRect;
987 }
988
989 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -0700990 public int getChildCount() {
991 return mView.getAdapter().getItemCount();
992 }
993
994 @Override
Kyle Horimoto2c802572015-08-18 13:25:29 -0700995 public int getVisibleChildCount() {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700996 return mView.getChildCount();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700997 }
998
999 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -07001000 public int getColumnCount() {
Ben Kwa743c7c22015-12-01 19:56:57 -08001001 RecyclerView.LayoutManager layoutManager = mView.getLayoutManager();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001002 if (layoutManager instanceof GridLayoutManager) {
1003 return ((GridLayoutManager) layoutManager).getSpanCount();
1004 }
1005
1006 // Otherwise, it is a list with 1 column.
1007 return 1;
1008 }
1009
1010 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -07001011 public int getRowCount() {
1012 int numFullColumns = getChildCount() / getColumnCount();
1013 boolean hasPartiallyFullColumn = getChildCount() % getColumnCount() != 0;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001014 return numFullColumns + (hasPartiallyFullColumn ? 1 : 0);
1015 }
1016
1017 @Override
Kyle Horimoto2c802572015-08-18 13:25:29 -07001018 public int getHeight() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001019 return mView.getHeight();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001020 }
1021
1022 @Override
1023 public void invalidateView() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001024 mView.invalidate();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001025 }
1026
1027 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -07001028 public void runAtNextFrame(Runnable r) {
1029 mView.postOnAnimation(r);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001030 }
1031
1032 @Override
1033 public void removeCallback(Runnable r) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001034 mView.removeCallbacks(r);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001035 }
1036
1037 @Override
1038 public void scrollBy(int dy) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001039 mView.scrollBy(0, dy);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001040 }
1041
1042 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -07001043 public void showBand(Rect rect) {
1044 mBand.setBounds(rect);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001045
1046 if (!mIsOverlayShown) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001047 mView.getOverlay().add(mBand);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001048 }
1049 }
1050
1051 @Override
1052 public void hideBand() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001053 mView.getOverlay().remove(mBand);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001054 }
Ben Kwa8250db42015-10-07 14:15:12 -07001055
1056 @Override
Ben Kwa22f479d2016-01-20 15:07:53 -08001057 public boolean isLayoutItem(int pos) {
1058 // The band selection model only operates on documents and directories. Exclude other
1059 // types of adapter items (e.g. whitespace items like dividers).
1060 RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
1061 switch (vh.getItemViewType()) {
1062 case ITEM_TYPE_DOCUMENT:
1063 case ITEM_TYPE_DIRECTORY:
1064 return false;
1065 default:
1066 return true;
1067 }
1068 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001069 }
1070
Steve McKay4b3a13c2015-06-11 10:10:49 -07001071 public interface Callback {
1072 /**
1073 * Called when an item is selected or unselected while in selection mode.
1074 *
1075 * @param position Adapter position of the item that was checked or unchecked
1076 * @param selected <code>true</code> if the item is now selected, <code>false</code>
1077 * if the item is now unselected.
1078 */
Ben Kwa743c7c22015-12-01 19:56:57 -08001079 public void onItemStateChanged(String id, boolean selected);
Steve McKay4b3a13c2015-06-11 10:10:49 -07001080
1081 /**
Steve McKaydbec47a2015-08-12 14:48:34 -07001082 * Called prior to an item changing state. Callbacks can cancel
1083 * the change at {@code position} by returning {@code false}.
1084 *
Ben Kwa743c7c22015-12-01 19:56:57 -08001085 * @param id Adapter position of the item that was checked or unchecked
Steve McKaydbec47a2015-08-12 14:48:34 -07001086 * @param selected <code>true</code> if the item is to be selected, <code>false</code>
1087 * if the item is to be unselected.
Steve McKay4b3a13c2015-06-11 10:10:49 -07001088 */
Ben Kwa743c7c22015-12-01 19:56:57 -08001089 public boolean onBeforeItemStateChange(String id, boolean selected);
Steve McKaydbec47a2015-08-12 14:48:34 -07001090
1091 /**
1092 * Called immediately after completion of any set of changes.
1093 */
1094 public void onSelectionChanged();
Steve McKay4b3a13c2015-06-11 10:10:49 -07001095 }
1096
1097 /**
Kyle Horimoto2c802572015-08-18 13:25:29 -07001098 * Provides mouse driven band-select support when used in conjunction with {@link RecyclerView}
1099 * and {@link MultiSelectManager}. This class is responsible for rendering the band select
1100 * overlay and selecting overlaid items via MultiSelectManager.
1101 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001102 public class BandController extends RecyclerView.OnScrollListener
1103 implements GridModel.OnSelectionChangedListener {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001104
1105 private static final int NOT_SET = -1;
1106
Steve McKay635b0942015-09-03 16:49:51 -07001107 private final Runnable mModelBuilder;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001108
Steve McKay635b0942015-09-03 16:49:51 -07001109 @Nullable private Rect mBounds;
1110 @Nullable private Point mCurrentPosition;
1111 @Nullable private Point mOrigin;
Steve McKayc3ef0d62015-09-08 17:15:25 -07001112 @Nullable private GridModel mModel;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001113
1114 // The time at which the current band selection-induced scroll began. If no scroll is in
1115 // progress, the value is NOT_SET.
1116 private long mScrollStartTime = NOT_SET;
1117 private final Runnable mViewScroller = new ViewScroller();
1118
Steve McKay669ebe72015-10-19 12:04:21 -07001119 public BandController() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001120 mEnvironment.addOnScrollListener(this);
Steve McKay635b0942015-09-03 16:49:51 -07001121
1122 mModelBuilder = new Runnable() {
1123 @Override
1124 public void run() {
Steve McKay44408262016-01-05 15:27:17 -08001125 mModel = new GridModel(mEnvironment, mAdapter);
Steve McKayc3ef0d62015-09-08 17:15:25 -07001126 mModel.addOnSelectionChangedListener(BandController.this);
Steve McKay635b0942015-09-03 16:49:51 -07001127 }
1128 };
1129 }
1130
Steve McKay669ebe72015-10-19 12:04:21 -07001131 public boolean handleEvent(MotionInputEvent e) {
1132 // b/23793622 notes the fact that we *never* receive ACTION_DOWN
1133 // events in onTouchEvent. Where it not for this issue, we'd
1134 // push start handling down into handleInputEvent.
1135 if (mBandManager.shouldStart(e)) {
1136 // endBandSelect is handled in handleInputEvent.
1137 mBandManager.startBandSelect(e.getOrigin());
1138 } else if (mBandManager.isActive()
1139 && e.isMouseEvent()
1140 && e.isActionUp()) {
1141 // Same issue here w b/23793622. The ACTION_UP event
1142 // is only evert dispatched to onTouchEvent when
1143 // there is some associated motion. If a user taps
1144 // mouse, but doesn't move, then band select gets
1145 // started BUT not ended. Causing phantom
1146 // bands to appear when the user later clicks to start
1147 // band select.
1148 mBandManager.processInputEvent(e);
1149 }
1150
1151 return isActive();
1152 }
1153
Steve McKay635b0942015-09-03 16:49:51 -07001154 private boolean isActive() {
1155 return mModel != null;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001156 }
1157
1158 /**
Kyle Horimotod017db02015-08-27 16:44:00 -07001159 * Handle a change in layout by cleaning up and getting rid of the old model and creating
1160 * a new model which will track the new layout.
1161 */
1162 public void handleLayoutChanged() {
Steve McKay635b0942015-09-03 16:49:51 -07001163 if (mModel != null) {
1164 mModel.removeOnSelectionChangedListener(this);
1165 mModel.stopListening();
Kyle Horimotod017db02015-08-27 16:44:00 -07001166
Steve McKay635b0942015-09-03 16:49:51 -07001167 // build a new model, all fresh and happy.
1168 mModelBuilder.run();
1169 }
1170 }
1171
Steve McKay669ebe72015-10-19 12:04:21 -07001172 boolean shouldStart(MotionInputEvent e) {
Steve McKay635b0942015-09-03 16:49:51 -07001173 return !isActive()
Steve McKay669ebe72015-10-19 12:04:21 -07001174 && e.isMouseEvent() // a mouse
1175 && e.isActionDown() // the initial button press
Steve McKay44408262016-01-05 15:27:17 -08001176 && mAdapter.getItemCount() > 0
Steve McKay669ebe72015-10-19 12:04:21 -07001177 && e.getItemPosition() == RecyclerView.NO_ID; // in empty space
Steve McKay635b0942015-09-03 16:49:51 -07001178 }
1179
1180 boolean shouldStop(InputEvent input) {
1181 return isActive()
1182 && input.isMouseEvent()
1183 && input.isActionUp();
Kyle Horimotod017db02015-08-27 16:44:00 -07001184 }
1185
1186 /**
Kyle Horimoto2c802572015-08-18 13:25:29 -07001187 * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
Steve McKay635b0942015-09-03 16:49:51 -07001188 * @param input
Kyle Horimoto2c802572015-08-18 13:25:29 -07001189 */
Steve McKay669ebe72015-10-19 12:04:21 -07001190 private void processInputEvent(InputEvent input) {
Steve McKaya1f76802016-02-25 13:34:03 -08001191 assert(input.isMouseEvent());
Steve McKay635b0942015-09-03 16:49:51 -07001192
1193 if (shouldStop(input)) {
Steve McKaya698d632015-09-04 14:51:16 -07001194 endBandSelect();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001195 return;
1196 }
1197
Steve McKay635b0942015-09-03 16:49:51 -07001198 // We shouldn't get any events in this method when band select is not active,
1199 // but it turns some guests show up late to the party.
1200 if (!isActive()) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001201 return;
1202 }
1203
Steve McKay635b0942015-09-03 16:49:51 -07001204 mCurrentPosition = input.getOrigin();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001205 scrollViewIfNecessary();
1206 resizeBandSelectRectangle();
1207 }
1208
1209 /**
1210 * Starts band select by adding the drawable to the RecyclerView's overlay.
1211 */
Steve McKay635b0942015-09-03 16:49:51 -07001212 private void startBandSelect(Point origin) {
1213 if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
1214
1215 mOrigin = origin;
1216 mModelBuilder.run(); // Creates a new selection model.
Kyle Horimoto2c802572015-08-18 13:25:29 -07001217 mModel.startSelection(mOrigin);
1218 }
1219
1220 /**
1221 * Scrolls the view if necessary.
1222 */
1223 private void scrollViewIfNecessary() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001224 mEnvironment.removeCallback(mViewScroller);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001225 mViewScroller.run();
Steve McKayc3ef0d62015-09-08 17:15:25 -07001226 mEnvironment.invalidateView();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001227 }
1228
1229 /**
1230 * Resizes the band select rectangle by using the origin and the current pointer position as
1231 * two opposite corners of the selection.
1232 */
1233 private void resizeBandSelectRectangle() {
Steve McKay635b0942015-09-03 16:49:51 -07001234 mBounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
1235 Math.min(mOrigin.y, mCurrentPosition.y),
1236 Math.max(mOrigin.x, mCurrentPosition.x),
1237 Math.max(mOrigin.y, mCurrentPosition.y));
Steve McKayc3ef0d62015-09-08 17:15:25 -07001238 mEnvironment.showBand(mBounds);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001239 }
1240
1241 /**
1242 * Ends band select by removing the overlay.
1243 */
1244 private void endBandSelect() {
1245 if (DEBUG) Log.d(TAG, "Ending band select.");
Steve McKay635b0942015-09-03 16:49:51 -07001246
Steve McKayc3ef0d62015-09-08 17:15:25 -07001247 mEnvironment.hideBand();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001248 mSelection.applyProvisionalSelection();
1249 mModel.endSelection();
Ben Kwafcb54d82015-12-10 15:21:18 -08001250 int firstSelected = mModel.getPositionNearestOrigin();
Ben Kwa20887032015-12-22 15:27:14 -08001251 if (firstSelected != NOT_SET) {
Steve McKay44408262016-01-05 15:27:17 -08001252 if (mSelection.contains(mAdapter.getModelId(firstSelected))) {
Ben Kwa20887032015-12-22 15:27:14 -08001253 // TODO: firstSelected should really be lastSelected, we want to anchor the item
1254 // where the mouse-up occurred.
1255 setSelectionRangeBegin(firstSelected);
1256 } else {
1257 // TODO: Check if this is really happening.
1258 Log.w(TAG, "First selected by band is NOT in selection!");
1259 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001260 }
Steve McKay635b0942015-09-03 16:49:51 -07001261
1262 mModel = null;
1263 mOrigin = null;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001264 }
1265
1266 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -08001267 public void onSelectionChanged(Set<String> updatedSelection) {
1268 Map<String, Boolean> delta = mSelection.setProvisionalSelection(updatedSelection);
1269 for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
1270 notifyItemStateChanged(entry.getKey(), entry.getValue());
Kyle Horimoto2c802572015-08-18 13:25:29 -07001271 }
1272 notifySelectionChanged();
1273 }
1274
1275 private class ViewScroller implements Runnable {
1276 /**
1277 * The number of milliseconds of scrolling at which scroll speed continues to increase.
1278 * At first, the scroll starts slowly; then, the rate of scrolling increases until it
1279 * reaches its maximum value at after this many milliseconds.
1280 */
1281 private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
1282
1283 @Override
1284 public void run() {
1285 // Compute the number of pixels the pointer's y-coordinate is past the view.
1286 // Negative values mean the pointer is at or before the top of the view, and
1287 // positive values mean that the pointer is at or after the bottom of the view. Note
1288 // that one additional pixel is added here so that the view still scrolls when the
1289 // pointer is exactly at the top or bottom.
1290 int pixelsPastView = 0;
Steve McKay635b0942015-09-03 16:49:51 -07001291 if (mCurrentPosition.y <= 0) {
1292 pixelsPastView = mCurrentPosition.y - 1;
Steve McKayc3ef0d62015-09-08 17:15:25 -07001293 } else if (mCurrentPosition.y >= mEnvironment.getHeight() - 1) {
1294 pixelsPastView = mCurrentPosition.y - mEnvironment.getHeight() + 1;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001295 }
1296
Steve McKay635b0942015-09-03 16:49:51 -07001297 if (!isActive() || pixelsPastView == 0) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001298 // If band selection is inactive, or if it is active but not at the edge of the
1299 // view, no scrolling is necessary.
1300 mScrollStartTime = NOT_SET;
1301 return;
1302 }
1303
1304 if (mScrollStartTime == NOT_SET) {
1305 // If the pointer was previously not at the edge of the view but now is, set the
1306 // start time for the scroll.
1307 mScrollStartTime = System.currentTimeMillis();
1308 }
1309
1310 // Compute the number of pixels to scroll, and scroll that many pixels.
1311 final int numPixels = computeScrollDistance(
1312 pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
Steve McKayc3ef0d62015-09-08 17:15:25 -07001313 mEnvironment.scrollBy(numPixels);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001314
Steve McKayc3ef0d62015-09-08 17:15:25 -07001315 mEnvironment.removeCallback(mViewScroller);
1316 mEnvironment.runAtNextFrame(this);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001317 }
1318
1319 /**
1320 * Computes the number of pixels to scroll based on how far the pointer is past the end
1321 * of the view and how long it has been there. Roughly based on ItemTouchHelper's
1322 * algorithm for computing the number of pixels to scroll when an item is dragged to the
1323 * end of a {@link RecyclerView}.
1324 * @param pixelsPastView
1325 * @param scrollDuration
1326 * @return
1327 */
1328 private int computeScrollDistance(int pixelsPastView, long scrollDuration) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001329 final int maxScrollStep = mEnvironment.getHeight();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001330 final int direction = (int) Math.signum(pixelsPastView);
1331 final int absPastView = Math.abs(pixelsPastView);
1332
1333 // Calculate the ratio of how far out of the view the pointer currently resides to
1334 // the entire height of the view.
1335 final float outOfBoundsRatio = Math.min(
Steve McKayc3ef0d62015-09-08 17:15:25 -07001336 1.0f, (float) absPastView / mEnvironment.getHeight());
Kyle Horimoto2c802572015-08-18 13:25:29 -07001337 // Interpolate this ratio and use it to compute the maximum scroll that should be
1338 // possible for this step.
1339 final float cappedScrollStep =
1340 direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);
1341
1342 // Likewise, calculate the ratio of the time spent in the scroll to the limit.
1343 final float timeRatio = Math.min(
1344 1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
1345 // Interpolate this ratio and use it to compute the final number of pixels to
1346 // scroll.
1347 final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));
1348
1349 // If the final number of pixels to scroll ends up being 0, the view should still
1350 // scroll at least one pixel.
1351 return numPixels != 0 ? numPixels : direction;
1352 }
1353
1354 /**
1355 * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
1356 * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
1357 * drags that are at the edge or barely past the edge of the view still cause sufficient
1358 * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if
1359 * needed.
1360 * @param ratio A ratio which is in the range [0, 1].
1361 * @return A "smoothed" value, also in the range [0, 1].
1362 */
1363 private float smoothOutOfBoundsRatio(float ratio) {
1364 return (float) Math.pow(ratio - 1.0f, 5) + 1.0f;
1365 }
1366
1367 /**
1368 * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1)
1369 * and stays close to 0 for most input values except those very close to 1. This ensures
1370 * that scrolls start out very slowly but speed up drastically after the scroll has been
1371 * in progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used,
1372 * but this could also be tweaked if needed.
1373 * @param ratio A ratio which is in the range [0, 1].
1374 * @return A "smoothed" value, also in the range [0, 1].
1375 */
1376 private float smoothTimeRatio(float ratio) {
1377 return (float) Math.pow(ratio, 5);
1378 }
1379 };
Kyle Horimotoea910a12015-08-25 17:12:42 -07001380
1381 @Override
1382 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
Steve McKay635b0942015-09-03 16:49:51 -07001383 if (!isActive()) {
Kyle Horimotoea910a12015-08-25 17:12:42 -07001384 return;
1385 }
1386
1387 // Adjust the y-coordinate of the origin the opposite number of pixels so that the
1388 // origin remains in the same place relative to the view's items.
1389 mOrigin.y -= dy;
1390 resizeBandSelectRectangle();
1391 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001392 }
1393
1394 /**
1395 * Provides a band selection item model for views within a RecyclerView. This class queries the
1396 * RecyclerView to determine where its items are placed; then, once band selection is underway,
1397 * it alerts listeners of which items are covered by the selections.
1398 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001399 public static final class GridModel extends RecyclerView.OnScrollListener {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001400
1401 public static final int NOT_SET = -1;
1402
1403 // Enum values used to determine the corner at which the origin is located within the
1404 private static final int UPPER = 0x00;
1405 private static final int LOWER = 0x01;
1406 private static final int LEFT = 0x00;
1407 private static final int RIGHT = 0x02;
1408 private static final int UPPER_LEFT = UPPER | LEFT;
1409 private static final int UPPER_RIGHT = UPPER | RIGHT;
1410 private static final int LOWER_LEFT = LOWER | LEFT;
1411 private static final int LOWER_RIGHT = LOWER | RIGHT;
1412
Ben Kwa8250db42015-10-07 14:15:12 -07001413 private final SelectionEnvironment mHelper;
Steve McKay44408262016-01-05 15:27:17 -08001414 private final DocumentsAdapter mAdapter;
1415
Steve McKayc3ef0d62015-09-08 17:15:25 -07001416 private final List<OnSelectionChangedListener> mOnSelectionChangedListeners =
1417 new ArrayList<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001418
Kyle Horimoto786babe2015-08-25 21:03:38 -07001419 // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed
1420 // by their y-offset. For example, if the first column of the view starts at an x-value of 5,
1421 // mColumns.get(5) would return an array of positions in that column. Within that array, the
1422 // value for key y is the adapter position for the item whose y-offset is y.
Ben Kwafcb54d82015-12-10 15:21:18 -08001423 private final SparseArray<SparseIntArray> mColumns = new SparseArray<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001424
Steve McKayc3ef0d62015-09-08 17:15:25 -07001425 // List of limits along the x-axis (columns).
1426 // This list is sorted from furthest left to furthest right.
1427 private final List<Limits> mColumnBounds = new ArrayList<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001428
Steve McKayc3ef0d62015-09-08 17:15:25 -07001429 // List of limits along the y-axis (rows). Note that this list only contains items which
1430 // have been in the viewport.
1431 private final List<Limits> mRowBounds = new ArrayList<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001432
1433 // The adapter positions which have been recorded so far.
Ben Kwafcb54d82015-12-10 15:21:18 -08001434 private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001435
1436 // Array passed to registered OnSelectionChangedListeners. One array is created and reused
1437 // throughout the lifetime of the object.
Ben Kwa743c7c22015-12-01 19:56:57 -08001438 private final Set<String> mSelection = new HashSet<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001439
1440 // The current pointer (in absolute positioning from the top of the view).
1441 private Point mPointer = null;
1442
1443 // The bounds of the band selection.
1444 private RelativePoint mRelativeOrigin;
1445 private RelativePoint mRelativePointer;
1446
1447 private boolean mIsActive;
1448
1449 // Tracks where the band select originated from. This is used to determine where selections
1450 // should expand from when Shift+click is used.
Ben Kwafcb54d82015-12-10 15:21:18 -08001451 private int mPositionNearestOrigin = NOT_SET;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001452
Steve McKay44408262016-01-05 15:27:17 -08001453 GridModel(SelectionEnvironment helper, DocumentsAdapter adapter) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001454 mHelper = helper;
Steve McKay44408262016-01-05 15:27:17 -08001455 mAdapter = adapter;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001456 mHelper.addOnScrollListener(this);
1457 }
1458
1459 /**
1460 * Stops listening to the view's scrolls. Call this function before discarding a
1461 * BandSelecModel object to prevent memory leaks.
1462 */
1463 void stopListening() {
1464 mHelper.removeOnScrollListener(this);
1465 }
1466
1467 /**
1468 * Start a band select operation at the given point.
1469 * @param relativeOrigin The origin of the band select operation, relative to the viewport.
1470 * For example, if the view is scrolled to the bottom, the top-left of the viewport
1471 * would have a relative origin of (0, 0), even though its absolute point has a higher
1472 * y-value.
1473 */
1474 void startSelection(Point relativeOrigin) {
1475 mIsActive = true;
1476 mPointer = mHelper.createAbsolutePoint(relativeOrigin);
1477
1478 recordVisibleChildren();
1479 mRelativeOrigin = new RelativePoint(mPointer);
1480 mRelativePointer = new RelativePoint(mPointer);
1481 computeCurrentSelection();
1482 notifyListeners();
1483 }
1484
1485 /**
1486 * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
1487 * opposite the origin.
1488 * @param relativePointer The pointer (opposite of the origin) of the band select operation,
1489 * relative to the viewport. For example, if the view is scrolled to the bottom, the
1490 * top-left of the viewport would have a relative origin of (0, 0), even though its
1491 * absolute point has a higher y-value.
1492 */
Steve McKaya1f76802016-02-25 13:34:03 -08001493 @VisibleForTesting
Kyle Horimoto2c802572015-08-18 13:25:29 -07001494 void resizeSelection(Point relativePointer) {
1495 mPointer = mHelper.createAbsolutePoint(relativePointer);
1496 updateModel();
1497 }
1498
1499 /**
1500 * Ends the band selection.
1501 */
1502 void endSelection() {
1503 mIsActive = false;
1504 }
1505
1506 /**
1507 * @return The adapter position for the item nearest the origin corresponding to the latest
1508 * band select operation, or NOT_SET if the selection did not cover any items.
1509 */
Ben Kwafcb54d82015-12-10 15:21:18 -08001510 int getPositionNearestOrigin() {
1511 return mPositionNearestOrigin;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001512 }
1513
1514 @Override
1515 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
1516 if (!mIsActive) {
1517 return;
1518 }
1519
1520 mPointer.x += dx;
1521 mPointer.y += dy;
1522 recordVisibleChildren();
1523 updateModel();
1524 }
1525
1526 /**
1527 * Queries the view for all children and records their location metadata.
1528 */
1529 private void recordVisibleChildren() {
1530 for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001531 int adapterPosition = mHelper.getAdapterPositionAt(i);
Ben Kwa22f479d2016-01-20 15:07:53 -08001532 if (!mHelper.isLayoutItem(adapterPosition) &&
1533 !mKnownPositions.get(adapterPosition)) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001534 mKnownPositions.put(adapterPosition, true);
1535 recordItemData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001536 }
1537 }
1538 }
1539
1540 /**
1541 * Updates the limits lists and column map with the given item metadata.
1542 * @param absoluteChildRect The absolute rectangle for the child view being processed.
1543 * @param adapterPosition The position of the child view being processed.
1544 */
Ben Kwafcb54d82015-12-10 15:21:18 -08001545 private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001546 if (mColumnBounds.size() != mHelper.getColumnCount()) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001547 // If not all x-limits have been recorded, record this one.
1548 recordLimits(
Steve McKayc3ef0d62015-09-08 17:15:25 -07001549 mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right));
Kyle Horimoto2c802572015-08-18 13:25:29 -07001550 }
1551
Steve McKayc3ef0d62015-09-08 17:15:25 -07001552 if (mRowBounds.size() != mHelper.getRowCount()) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001553 // If not all y-limits have been recorded, record this one.
1554 recordLimits(
Steve McKayc3ef0d62015-09-08 17:15:25 -07001555 mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
Kyle Horimoto2c802572015-08-18 13:25:29 -07001556 }
1557
Ben Kwafcb54d82015-12-10 15:21:18 -08001558 SparseIntArray columnList = mColumns.get(absoluteChildRect.left);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001559 if (columnList == null) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001560 columnList = new SparseIntArray();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001561 mColumns.put(absoluteChildRect.left, columnList);
1562 }
Ben Kwafcb54d82015-12-10 15:21:18 -08001563 columnList.put(absoluteChildRect.top, adapterPosition);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001564 }
1565
1566 /**
1567 * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
1568 * does not exist.
1569 */
1570 private void recordLimits(List<Limits> limitsList, Limits limits) {
1571 int index = Collections.binarySearch(limitsList, limits);
1572 if (index < 0) {
1573 limitsList.add(~index, limits);
1574 }
1575 }
1576
1577 /**
1578 * Handles a moved pointer; this function determines whether the pointer movement resulted
1579 * in a selection change and, if it has, notifies listeners of this change.
1580 */
1581 private void updateModel() {
1582 RelativePoint old = mRelativePointer;
1583 mRelativePointer = new RelativePoint(mPointer);
1584 if (old != null && mRelativePointer.equals(old)) {
1585 return;
1586 }
1587
1588 computeCurrentSelection();
1589 notifyListeners();
1590 }
1591
1592 /**
1593 * Computes the currently-selected items.
1594 */
1595 private void computeCurrentSelection() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001596 if (areItemsCoveredByBand(mRelativePointer, mRelativeOrigin)) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001597 updateSelection(computeBounds());
1598 } else {
1599 mSelection.clear();
Ben Kwafcb54d82015-12-10 15:21:18 -08001600 mPositionNearestOrigin = NOT_SET;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001601 }
1602 }
1603
1604 /**
1605 * Notifies all listeners of a selection change. Note that this function simply passes
1606 * mSelection, so computeCurrentSelection() should be called before this
1607 * function.
1608 */
1609 private void notifyListeners() {
1610 for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
1611 listener.onSelectionChanged(mSelection);
1612 }
1613 }
1614
1615 /**
1616 * @param rect Rectangle including all covered items.
1617 */
1618 private void updateSelection(Rect rect) {
Ben Kwa22f479d2016-01-20 15:07:53 -08001619 int columnStart =
Steve McKayc3ef0d62015-09-08 17:15:25 -07001620 Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
Steve McKaya1f76802016-02-25 13:34:03 -08001621 assert(columnStart >= 0);
Ben Kwa22f479d2016-01-20 15:07:53 -08001622 int columnEnd = columnStart;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001623
Ben Kwa22f479d2016-01-20 15:07:53 -08001624 for (int i = columnStart; i < mColumnBounds.size()
Steve McKayc3ef0d62015-09-08 17:15:25 -07001625 && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
Ben Kwa22f479d2016-01-20 15:07:53 -08001626 columnEnd = i;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001627 }
1628
Ben Kwa22f479d2016-01-20 15:07:53 -08001629 int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
1630 if (rowStart < 0) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001631 mPositionNearestOrigin = NOT_SET;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001632 return;
1633 }
1634
Ben Kwa22f479d2016-01-20 15:07:53 -08001635 int rowEnd = rowStart;
1636 for (int i = rowStart; i < mRowBounds.size()
1637 && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
1638 rowEnd = i;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001639 }
1640
Ben Kwa22f479d2016-01-20 15:07:53 -08001641 updateSelection(columnStart, columnEnd, rowStart, rowEnd);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001642 }
1643
1644 /**
1645 * Computes the selection given the previously-computed start- and end-indices for each
1646 * row and column.
1647 */
1648 private void updateSelection(
1649 int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
Ben Kwa22f479d2016-01-20 15:07:53 -08001650 if (DEBUG) Log.d(TAG, String.format("updateSelection: %d, %d, %d, %d",
1651 columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
1652
Kyle Horimoto2c802572015-08-18 13:25:29 -07001653 mSelection.clear();
1654 for (int column = columnStartIndex; column <= columnEndIndex; column++) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001655 SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001656 for (int row = rowStartIndex; row <= rowEndIndex; row++) {
Ben Kwa20887032015-12-22 15:27:14 -08001657 // The default return value for SparseIntArray.get is 0, which is a valid
1658 // position. Use a sentry value to prevent erroneously selecting item 0.
Ben Kwa22f479d2016-01-20 15:07:53 -08001659 final int rowKey = mRowBounds.get(row).lowerLimit;
1660 int position = items.get(rowKey, NOT_SET);
Ben Kwa20887032015-12-22 15:27:14 -08001661 if (position != NOT_SET) {
Steve McKay44408262016-01-05 15:27:17 -08001662 String id = mAdapter.getModelId(position);
Ben Kwa20887032015-12-22 15:27:14 -08001663 if (id != null) {
1664 // The adapter inserts items for UI layout purposes that aren't associated
1665 // with files. Those will have a null model ID. Don't select them.
1666 mSelection.add(id);
1667 }
1668 if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
1669 row, rowStartIndex, rowEndIndex)) {
1670 // If this is the position nearest the origin, record it now so that it
1671 // can be returned by endSelection() later.
1672 mPositionNearestOrigin = position;
1673 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001674 }
1675 }
1676 }
1677 }
1678
1679 /**
1680 * @return Returns true if the position is the nearest to the origin, or, in the case of the
1681 * lower-right corner, whether it is possible that the position is the nearest to the
1682 * origin. See comment below for reasoning for this special case.
1683 */
1684 private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
1685 int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
1686 int corner = computeCornerNearestOrigin();
1687 switch (corner) {
1688 case UPPER_LEFT:
1689 return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
1690 case UPPER_RIGHT:
1691 return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
1692 case LOWER_LEFT:
1693 return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
1694 case LOWER_RIGHT:
1695 // Note that in some cases, the last row will not have as many items as there
1696 // are columns (e.g., if there are 4 items and 3 columns, the second row will
1697 // only have one item in the first column). This function is invoked for each
1698 // position from left to right, so return true for any position in the bottom
1699 // row and only the right-most position in the bottom row will be recorded.
1700 return rowIndex == rowEndIndex;
1701 default:
1702 throw new RuntimeException("Invalid corner type.");
1703 }
1704 }
1705
1706 /**
1707 * Listener for changes in which items have been band selected.
1708 */
1709 static interface OnSelectionChangedListener {
Ben Kwa743c7c22015-12-01 19:56:57 -08001710 public void onSelectionChanged(Set<String> updatedSelection);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001711 }
1712
1713 void addOnSelectionChangedListener(OnSelectionChangedListener listener) {
1714 mOnSelectionChangedListeners.add(listener);
1715 }
1716
1717 void removeOnSelectionChangedListener(OnSelectionChangedListener listener) {
1718 mOnSelectionChangedListeners.remove(listener);
1719 }
1720
1721 /**
Kyle Horimoto2c802572015-08-18 13:25:29 -07001722 * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
1723 * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
1724 * of item columns and the top- and bottom sides of item rows so that it can be determined
1725 * whether the pointer is located within the bounds of an item.
1726 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001727 private static class Limits implements Comparable<Limits> {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001728 int lowerLimit;
1729 int upperLimit;
1730
1731 Limits(int lowerLimit, int upperLimit) {
1732 this.lowerLimit = lowerLimit;
1733 this.upperLimit = upperLimit;
1734 }
1735
1736 @Override
1737 public int compareTo(Limits other) {
1738 return lowerLimit - other.lowerLimit;
1739 }
1740
1741 @Override
1742 public boolean equals(Object other) {
1743 if (!(other instanceof Limits)) {
1744 return false;
1745 }
1746
1747 return ((Limits) other).lowerLimit == lowerLimit &&
1748 ((Limits) other).upperLimit == upperLimit;
1749 }
1750 }
1751
1752 /**
1753 * The location of a coordinate relative to items. This class represents a general area of the
1754 * view as it relates to band selection rather than an explicit point. For example, two
1755 * different points within an item are considered to have the same "location" because band
1756 * selection originating within the item would select the same items no matter which point
1757 * was used. Same goes for points between items as well as those at the very beginning or end
1758 * of the view.
1759 *
1760 * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
1761 * advantage of tying the value to the Limits of items along that axis. This allows easy
1762 * selection of items within those Limits as opposed to a search through every item to see if a
1763 * given coordinate value falls within those Limits.
1764 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001765 private static class RelativeCoordinate
Kyle Horimoto2c802572015-08-18 13:25:29 -07001766 implements Comparable<RelativeCoordinate> {
1767 /**
1768 * Location describing points after the last known item.
1769 */
1770 static final int AFTER_LAST_ITEM = 0;
1771
1772 /**
1773 * Location describing points before the first known item.
1774 */
1775 static final int BEFORE_FIRST_ITEM = 1;
1776
1777 /**
1778 * Location describing points between two items.
1779 */
1780 static final int BETWEEN_TWO_ITEMS = 2;
1781
1782 /**
1783 * Location describing points within the limits of one item.
1784 */
1785 static final int WITHIN_LIMITS = 3;
1786
1787 /**
1788 * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
1789 * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
1790 */
1791 final int type;
1792
1793 /**
1794 * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
1795 * BETWEEN_TWO_ITEMS.
1796 */
1797 Limits limitsBeforeCoordinate;
1798
1799 /**
1800 * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
1801 */
1802 Limits limitsAfterCoordinate;
1803
1804 // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
1805 Limits mFirstKnownItem;
1806 // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
1807 Limits mLastKnownItem;
1808
1809 /**
1810 * @param limitsList The sorted limits list for the coordinate type. If this
1811 * CoordinateLocation is an x-value, mXLimitsList should be passed; otherwise,
1812 * mYLimitsList should be pased.
1813 * @param value The coordinate value.
1814 */
1815 RelativeCoordinate(List<Limits> limitsList, int value) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001816 int index = Collections.binarySearch(limitsList, new Limits(value, value));
Kyle Horimoto2c802572015-08-18 13:25:29 -07001817
1818 if (index >= 0) {
1819 this.type = WITHIN_LIMITS;
1820 this.limitsBeforeCoordinate = limitsList.get(index);
1821 } else if (~index == 0) {
1822 this.type = BEFORE_FIRST_ITEM;
1823 this.mFirstKnownItem = limitsList.get(0);
1824 } else if (~index == limitsList.size()) {
1825 Limits lastLimits = limitsList.get(limitsList.size() - 1);
1826 if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
1827 this.type = WITHIN_LIMITS;
1828 this.limitsBeforeCoordinate = lastLimits;
1829 } else {
1830 this.type = AFTER_LAST_ITEM;
1831 this.mLastKnownItem = lastLimits;
1832 }
1833 } else {
1834 Limits limitsBeforeIndex = limitsList.get(~index - 1);
1835 if (limitsBeforeIndex.lowerLimit <= value && value <= limitsBeforeIndex.upperLimit) {
1836 this.type = WITHIN_LIMITS;
1837 this.limitsBeforeCoordinate = limitsList.get(~index - 1);
1838 } else {
1839 this.type = BETWEEN_TWO_ITEMS;
1840 this.limitsBeforeCoordinate = limitsList.get(~index - 1);
1841 this.limitsAfterCoordinate = limitsList.get(~index);
1842 }
1843 }
1844 }
1845
1846 int toComparisonValue() {
1847 if (type == BEFORE_FIRST_ITEM) {
1848 return mFirstKnownItem.lowerLimit - 1;
1849 } else if (type == AFTER_LAST_ITEM) {
1850 return mLastKnownItem.upperLimit + 1;
1851 } else if (type == BETWEEN_TWO_ITEMS) {
1852 return limitsBeforeCoordinate.upperLimit + 1;
1853 } else {
1854 return limitsBeforeCoordinate.lowerLimit;
1855 }
1856 }
1857
1858 @Override
1859 public boolean equals(Object other) {
1860 if (!(other instanceof RelativeCoordinate)) {
1861 return false;
1862 }
1863
1864 RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
1865 return toComparisonValue() == otherCoordinate.toComparisonValue();
1866 }
1867
1868 @Override
1869 public int compareTo(RelativeCoordinate other) {
1870 return toComparisonValue() - other.toComparisonValue();
1871 }
1872 }
1873
1874 /**
1875 * The location of a point relative to the Limits of nearby items; consists of both an x- and
1876 * y-RelativeCoordinateLocation.
1877 */
1878 private class RelativePoint {
1879 final RelativeCoordinate xLocation;
1880 final RelativeCoordinate yLocation;
1881
1882 RelativePoint(Point point) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001883 this.xLocation = new RelativeCoordinate(mColumnBounds, point.x);
1884 this.yLocation = new RelativeCoordinate(mRowBounds, point.y);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001885 }
1886
1887 @Override
1888 public boolean equals(Object other) {
1889 if (!(other instanceof RelativePoint)) {
1890 return false;
1891 }
1892
1893 RelativePoint otherPoint = (RelativePoint) other;
1894 return xLocation.equals(otherPoint.xLocation) && yLocation.equals(otherPoint.yLocation);
1895 }
1896 }
1897
1898 /**
1899 * Generates a rectangle which contains the items selected by the pointer and origin.
1900 * @return The rectangle, or null if no items were selected.
1901 */
1902 private Rect computeBounds() {
1903 Rect rect = new Rect();
1904 rect.left = getCoordinateValue(
1905 min(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001906 mColumnBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001907 true);
1908 rect.right = getCoordinateValue(
1909 max(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001910 mColumnBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001911 false);
1912 rect.top = getCoordinateValue(
1913 min(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001914 mRowBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001915 true);
1916 rect.bottom = getCoordinateValue(
1917 max(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001918 mRowBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001919 false);
1920 return rect;
1921 }
1922
1923 /**
1924 * Computes the corner of the selection nearest the origin.
1925 * @return
1926 */
1927 private int computeCornerNearestOrigin() {
1928 int cornerValue = 0;
1929
1930 if (mRelativeOrigin.yLocation ==
1931 min(mRelativeOrigin.yLocation, mRelativePointer.yLocation)) {
1932 cornerValue |= UPPER;
1933 } else {
1934 cornerValue |= LOWER;
1935 }
1936
1937 if (mRelativeOrigin.xLocation ==
1938 min(mRelativeOrigin.xLocation, mRelativePointer.xLocation)) {
1939 cornerValue |= LEFT;
1940 } else {
1941 cornerValue |= RIGHT;
1942 }
1943
1944 return cornerValue;
1945 }
1946
1947 private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) {
1948 return first.compareTo(second) < 0 ? first : second;
1949 }
1950
1951 private RelativeCoordinate max(RelativeCoordinate first, RelativeCoordinate second) {
1952 return first.compareTo(second) > 0 ? first : second;
1953 }
1954
1955 /**
1956 * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
1957 * coordinate.
1958 */
1959 private int getCoordinateValue(RelativeCoordinate coordinate,
1960 List<Limits> limitsList, boolean isStartOfRange) {
1961 switch (coordinate.type) {
1962 case RelativeCoordinate.BEFORE_FIRST_ITEM:
1963 return limitsList.get(0).lowerLimit;
1964 case RelativeCoordinate.AFTER_LAST_ITEM:
1965 return limitsList.get(limitsList.size() - 1).upperLimit;
1966 case RelativeCoordinate.BETWEEN_TWO_ITEMS:
1967 if (isStartOfRange) {
1968 return coordinate.limitsAfterCoordinate.lowerLimit;
1969 } else {
1970 return coordinate.limitsBeforeCoordinate.upperLimit;
1971 }
1972 case RelativeCoordinate.WITHIN_LIMITS:
1973 return coordinate.limitsBeforeCoordinate.lowerLimit;
1974 }
1975
1976 throw new RuntimeException("Invalid coordinate value.");
1977 }
1978
Steve McKayc3ef0d62015-09-08 17:15:25 -07001979 private boolean areItemsCoveredByBand(
Kyle Horimoto2c802572015-08-18 13:25:29 -07001980 RelativePoint first, RelativePoint second) {
1981 return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) &&
1982 doesCoordinateLocationCoverItems(first.yLocation, second.yLocation);
1983 }
1984
1985 private boolean doesCoordinateLocationCoverItems(
1986 RelativeCoordinate pointerCoordinate,
1987 RelativeCoordinate originCoordinate) {
1988 if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM &&
1989 originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
1990 return false;
1991 }
1992
1993 if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM &&
1994 originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
1995 return false;
1996 }
1997
1998 if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
1999 originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
2000 pointerCoordinate.limitsBeforeCoordinate.equals(
2001 originCoordinate.limitsBeforeCoordinate) &&
2002 pointerCoordinate.limitsAfterCoordinate.equals(
2003 originCoordinate.limitsAfterCoordinate)) {
2004 return false;
2005 }
2006
2007 return true;
2008 }
2009 }
Steve McKay4b3a13c2015-06-11 10:10:49 -07002010}