blob: 4cf1048b6ab4491f17e51fe49fab51185dffb5ea [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 McKay2fbb40e2016-02-19 10:57:08 -0800660 private final Set<String> mSelection;
661 private final Set<String> mProvisionalSelection;
Steve McKaye852d932016-02-08 19:09:42 -0800662 private String mDirectoryKey;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700663
Steve McKay2fbb40e2016-02-19 10:57:08 -0800664 public Selection() {
665 mSelection = new HashSet<String>();
666 mProvisionalSelection = new HashSet<String>();
667 }
668
669 /**
670 * Used by CREATOR.
671 */
672 private Selection(String directoryKey, List<String> selection) {
673 mDirectoryKey = directoryKey;
674 mSelection = new HashSet<String>(selection);
675 mProvisionalSelection = new HashSet<String>();
Steve McKayedd05752015-10-21 14:38:54 -0700676 }
677
Steve McKay4b3a13c2015-06-11 10:10:49 -0700678 /**
Ben Kwa743c7c22015-12-01 19:56:57 -0800679 * @param id
Steve McKay4b3a13c2015-06-11 10:10:49 -0700680 * @return true if the position is currently selected.
681 */
Steve McKayef16f5f2015-12-22 18:15:31 -0800682 public boolean contains(@Nullable String id) {
Steve McKaye852d932016-02-08 19:09:42 -0800683 return mSelection.contains(id) || mProvisionalSelection.contains(id);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700684 }
685
686 /**
Steve McKayedd05752015-10-21 14:38:54 -0700687 * Returns an unordered array of selected positions.
688 */
Ben Kwa743c7c22015-12-01 19:56:57 -0800689 public String[] getAll() {
Steve McKaye852d932016-02-08 19:09:42 -0800690 return toList().toArray(new String[0]);
691 }
692
693 /**
694 * Returns an unordered array of selected positions (including any
695 * provisional selections current in effect).
696 */
Aga Wronska893390b2016-02-17 13:50:42 -0800697 public List<String> toList() {
Steve McKaye852d932016-02-08 19:09:42 -0800698 ArrayList<String> selection = new ArrayList<String>(mSelection);
699 selection.addAll(mProvisionalSelection);
700 return selection;
Steve McKayedd05752015-10-21 14:38:54 -0700701 }
702
703 /**
Steve McKay4b3a13c2015-06-11 10:10:49 -0700704 * @return size of the selection.
705 */
706 public int size() {
Steve McKaye852d932016-02-08 19:09:42 -0800707 return mSelection.size() + mProvisionalSelection.size();
Steve McKay4b3a13c2015-06-11 10:10:49 -0700708 }
709
Steve McKayb04b1642015-07-24 13:14:20 -0700710 /**
711 * @return true if the selection is empty.
712 */
713 public boolean isEmpty() {
Steve McKaye852d932016-02-08 19:09:42 -0800714 return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700715 }
716
717 /**
718 * Sets the provisional selection, which is a temporary selection that can be saved,
719 * canceled, or adjusted at a later time. When a new provision selection is applied, the old
720 * one (if it exists) is abandoned.
Steve McKaye852d932016-02-08 19:09:42 -0800721 * @return Map of ids added or removed. Added ids have a value of true, removed are false.
Kyle Horimoto2c802572015-08-18 13:25:29 -0700722 */
723 @VisibleForTesting
Steve McKaye852d932016-02-08 19:09:42 -0800724 protected Map<String, Boolean> setProvisionalSelection(Set<String> newSelection) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800725 Map<String, Boolean> delta = new HashMap<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700726
Steve McKaye852d932016-02-08 19:09:42 -0800727 for (String id: mProvisionalSelection) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800728 // Mark each item that used to be in the selection but is unsaved and not in the new
729 // provisional selection.
Steve McKaye852d932016-02-08 19:09:42 -0800730 if (!newSelection.contains(id) && !mSelection.contains(id)) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800731 delta.put(id, false);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700732 }
733 }
734
Steve McKaye852d932016-02-08 19:09:42 -0800735 for (String id: mSelection) {
736 // Mark each item that used to be in the selection but is unsaved and not in the new
737 // provisional selection.
738 if (!newSelection.contains(id)) {
739 delta.put(id, false);
740 }
741 }
742
743 for (String id: newSelection) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800744 // Mark each item that was not previously in the selection but is in the new
745 // provisional selection.
Steve McKaye852d932016-02-08 19:09:42 -0800746 if (!mSelection.contains(id) && !mProvisionalSelection.contains(id)) {
Ben Kwa743c7c22015-12-01 19:56:57 -0800747 delta.put(id, true);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700748 }
749 }
750
Ben Kwa743c7c22015-12-01 19:56:57 -0800751 // Now, iterate through the changes and actually add/remove them to/from the current
752 // selection. This could not be done in the previous loops because changing the size of
753 // the selection mid-iteration changes iteration order erroneously.
754 for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
755 String id = entry.getKey();
756 if (entry.getValue()) {
Steve McKaye852d932016-02-08 19:09:42 -0800757 mProvisionalSelection.add(id);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700758 } else {
Steve McKaye852d932016-02-08 19:09:42 -0800759 mProvisionalSelection.remove(id);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700760 }
761 }
762
763 return delta;
764 }
765
766 /**
767 * Saves the existing provisional selection. Once the provisional selection is saved,
768 * subsequent provisional selections which are different from this existing one cannot
769 * cause items in this existing provisional selection to become deselected.
770 */
771 @VisibleForTesting
772 protected void applyProvisionalSelection() {
Steve McKaye852d932016-02-08 19:09:42 -0800773 mSelection.addAll(mProvisionalSelection);
774 mProvisionalSelection.clear();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700775 }
776
777 /**
778 * Abandons the existing provisional selection so that all items provisionally selected are
779 * now deselected.
780 */
781 @VisibleForTesting
Ben Kwa743c7c22015-12-01 19:56:57 -0800782 void cancelProvisionalSelection() {
Steve McKaye852d932016-02-08 19:09:42 -0800783 mProvisionalSelection.clear();
Steve McKayb04b1642015-07-24 13:14:20 -0700784 }
785
Steve McKay4b3a13c2015-06-11 10:10:49 -0700786 /** @hide */
787 @VisibleForTesting
Ben Kwa743c7c22015-12-01 19:56:57 -0800788 boolean add(String id) {
Steve McKaye852d932016-02-08 19:09:42 -0800789 if (!mSelection.contains(id)) {
790 mSelection.add(id);
Steve McKayb04b1642015-07-24 13:14:20 -0700791 return true;
792 }
793 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700794 }
795
796 /** @hide */
797 @VisibleForTesting
Ben Kwa743c7c22015-12-01 19:56:57 -0800798 boolean remove(String id) {
Steve McKaye852d932016-02-08 19:09:42 -0800799 if (mSelection.contains(id)) {
800 mSelection.remove(id);
Steve McKayb04b1642015-07-24 13:14:20 -0700801 return true;
802 }
803 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700804 }
805
Steve McKayf8621552015-11-03 15:23:16 -0800806 public void clear() {
Steve McKaye852d932016-02-08 19:09:42 -0800807 mSelection.clear();
Steve McKay4b3a13c2015-06-11 10:10:49 -0700808 }
809
Ben Kwafcb54d82015-12-10 15:21:18 -0800810 /**
811 * Trims this selection to be the intersection of itself with the set of given IDs.
812 */
813 public void intersect(Collection<String> ids) {
Steve McKaye852d932016-02-08 19:09:42 -0800814 mSelection.retainAll(ids);
815 mProvisionalSelection.retainAll(ids);
Ben Kwafcb54d82015-12-10 15:21:18 -0800816 }
817
Steve McKay4b3a13c2015-06-11 10:10:49 -0700818 @VisibleForTesting
819 void copyFrom(Selection source) {
Steve McKay2fbb40e2016-02-19 10:57:08 -0800820 mSelection.clear();
821 mSelection.addAll(source.mSelection);
822
823 mProvisionalSelection.clear();
824 mProvisionalSelection.addAll(source.mProvisionalSelection);
Steve McKay4b3a13c2015-06-11 10:10:49 -0700825 }
826
827 @Override
828 public String toString() {
829 if (size() <= 0) {
830 return "size=0, items=[]";
831 }
832
Steve McKaye852d932016-02-08 19:09:42 -0800833 StringBuilder buffer = new StringBuilder(size() * 28);
834 buffer.append("Selection{")
835 .append("applied{size=" + mSelection.size())
836 .append(", entries=" + mSelection)
837 .append("}, provisional{size=" + mProvisionalSelection.size())
838 .append(", entries=" + mProvisionalSelection)
839 .append("}}");
Steve McKay4b3a13c2015-06-11 10:10:49 -0700840 return buffer.toString();
841 }
842
843 @Override
Steve McKay3b409d02015-07-22 11:42:14 -0700844 public int hashCode() {
Steve McKaye852d932016-02-08 19:09:42 -0800845 return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
Steve McKay3b409d02015-07-22 11:42:14 -0700846 }
847
848 @Override
Steve McKay4b3a13c2015-06-11 10:10:49 -0700849 public boolean equals(Object that) {
850 if (this == that) {
851 return true;
852 }
853
Steve McKay3b409d02015-07-22 11:42:14 -0700854 if (!(that instanceof Selection)) {
855 return false;
Steve McKay4b3a13c2015-06-11 10:10:49 -0700856 }
Steve McKay3b409d02015-07-22 11:42:14 -0700857
Steve McKaye852d932016-02-08 19:09:42 -0800858 return mSelection.equals(((Selection) that).mSelection) &&
859 mProvisionalSelection.equals(((Selection) that).mProvisionalSelection);
860 }
861
862 /**
863 * Sets the state key for this selection, which allows us to match selections
864 * to particular states (of DirectoryFragment). Basically this lets us avoid
865 * loading a persisted selection in the wrong directory.
866 */
867 public void setDirectoryKey(String key) {
868 mDirectoryKey = key;
869 }
870
871 /**
872 * Sets the state key for this selection, which allows us to match selections
873 * to particular states (of DirectoryFragment). Basically this lets us avoid
874 * loading a persisted selection in the wrong directory.
875 */
876 public boolean hasDirectoryKey(String key) {
877 return key.equals(mDirectoryKey);
878 }
879
880 @Override
881 public int describeContents() {
882 return 0;
883 }
884
885 public void writeToParcel(Parcel dest, int flags) {
Steve McKaye852d932016-02-08 19:09:42 -0800886 dest.writeString(mDirectoryKey);
887 dest.writeList(new ArrayList<>(mSelection));
888 // We don't include provisional selection since it is
889 // typically coupled to some other runtime state (like a band).
Steve McKay4b3a13c2015-06-11 10:10:49 -0700890 }
Steve McKay2fbb40e2016-02-19 10:57:08 -0800891
892 public static final ClassLoaderCreator<Selection> CREATOR =
893 new ClassLoaderCreator<Selection>() {
894 @Override
895 public Selection createFromParcel(Parcel in) {
896 return createFromParcel(in, null);
897 }
898
899 @Override
900 public Selection createFromParcel(Parcel in, ClassLoader loader) {
901 return new Selection(
902 in.readString(),
903 (ArrayList<String>) in.readArrayList(loader));
904 }
905
906 @Override
907 public Selection[] newArray(int size) {
908 return new Selection[size];
909 }
910 };
Steve McKay4b3a13c2015-06-11 10:10:49 -0700911 }
912
Kyle Horimoto2c802572015-08-18 13:25:29 -0700913 /**
Steve McKayc3ef0d62015-09-08 17:15:25 -0700914 * Provides functionality for BandController. Exists primarily to tests that are
915 * fully isolated from RecyclerView.
Kyle Horimoto2c802572015-08-18 13:25:29 -0700916 */
Ben Kwa8250db42015-10-07 14:15:12 -0700917 interface SelectionEnvironment {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700918 void showBand(Rect rect);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700919 void hideBand();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700920 void addOnScrollListener(RecyclerView.OnScrollListener listener);
Steve McKayc3ef0d62015-09-08 17:15:25 -0700921 void removeOnScrollListener(RecyclerView.OnScrollListener listener);
922 void scrollBy(int dy);
923 int getHeight();
924 void invalidateView();
925 void runAtNextFrame(Runnable r);
926 void removeCallback(Runnable r);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700927 Point createAbsolutePoint(Point relativePoint);
928 Rect getAbsoluteRectForChildViewAt(int index);
Ben Kwafcb54d82015-12-10 15:21:18 -0800929 int getAdapterPositionAt(int index);
Steve McKayc3ef0d62015-09-08 17:15:25 -0700930 int getColumnCount();
931 int getRowCount();
932 int getChildCount();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700933 int getVisibleChildCount();
Ben Kwa22f479d2016-01-20 15:07:53 -0800934 /**
935 * Layout items are excluded from the GridModel.
936 */
937 boolean isLayoutItem(int adapterPosition);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700938 }
939
Steve McKay57a93ba2015-11-11 16:26:59 +0900940 /** Recycler view facade implementation backed by good ol' RecyclerView. */
Ben Kwa8250db42015-10-07 14:15:12 -0700941 private static final class RuntimeSelectionEnvironment implements SelectionEnvironment {
Kyle Horimoto2c802572015-08-18 13:25:29 -0700942
Steve McKayc3ef0d62015-09-08 17:15:25 -0700943 private final RecyclerView mView;
944 private final Drawable mBand;
Kyle Horimoto2c802572015-08-18 13:25:29 -0700945
946 private boolean mIsOverlayShown = false;
947
Steve McKay44408262016-01-05 15:27:17 -0800948 RuntimeSelectionEnvironment(RecyclerView view) {
949 mView = view;
Steve McKayc3ef0d62015-09-08 17:15:25 -0700950 mBand = mView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700951 }
952
953 @Override
Ben Kwafcb54d82015-12-10 15:21:18 -0800954 public int getAdapterPositionAt(int index) {
Ben Kwa67924892016-01-27 09:58:36 -0800955 return mView.getChildAdapterPosition(mView.getChildAt(index));
Kyle Horimoto2c802572015-08-18 13:25:29 -0700956 }
957
958 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800959 public void addOnScrollListener(RecyclerView.OnScrollListener listener) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700960 mView.addOnScrollListener(listener);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700961 }
962
963 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -0800964 public void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700965 mView.removeOnScrollListener(listener);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700966 }
967
968 @Override
969 public Point createAbsolutePoint(Point relativePoint) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700970 return new Point(relativePoint.x + mView.computeHorizontalScrollOffset(),
971 relativePoint.y + mView.computeVerticalScrollOffset());
Kyle Horimoto2c802572015-08-18 13:25:29 -0700972 }
973
974 @Override
975 public Rect getAbsoluteRectForChildViewAt(int index) {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700976 final View child = mView.getChildAt(index);
Kyle Horimoto2c802572015-08-18 13:25:29 -0700977 final Rect childRect = new Rect();
978 child.getHitRect(childRect);
Steve McKayc3ef0d62015-09-08 17:15:25 -0700979 childRect.left += mView.computeHorizontalScrollOffset();
980 childRect.right += mView.computeHorizontalScrollOffset();
981 childRect.top += mView.computeVerticalScrollOffset();
982 childRect.bottom += mView.computeVerticalScrollOffset();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700983 return childRect;
984 }
985
986 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -0700987 public int getChildCount() {
988 return mView.getAdapter().getItemCount();
989 }
990
991 @Override
Kyle Horimoto2c802572015-08-18 13:25:29 -0700992 public int getVisibleChildCount() {
Steve McKayc3ef0d62015-09-08 17:15:25 -0700993 return mView.getChildCount();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700994 }
995
996 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -0700997 public int getColumnCount() {
Ben Kwa743c7c22015-12-01 19:56:57 -0800998 RecyclerView.LayoutManager layoutManager = mView.getLayoutManager();
Kyle Horimoto2c802572015-08-18 13:25:29 -0700999 if (layoutManager instanceof GridLayoutManager) {
1000 return ((GridLayoutManager) layoutManager).getSpanCount();
1001 }
1002
1003 // Otherwise, it is a list with 1 column.
1004 return 1;
1005 }
1006
1007 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -07001008 public int getRowCount() {
1009 int numFullColumns = getChildCount() / getColumnCount();
1010 boolean hasPartiallyFullColumn = getChildCount() % getColumnCount() != 0;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001011 return numFullColumns + (hasPartiallyFullColumn ? 1 : 0);
1012 }
1013
1014 @Override
Kyle Horimoto2c802572015-08-18 13:25:29 -07001015 public int getHeight() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001016 return mView.getHeight();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001017 }
1018
1019 @Override
1020 public void invalidateView() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001021 mView.invalidate();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001022 }
1023
1024 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -07001025 public void runAtNextFrame(Runnable r) {
1026 mView.postOnAnimation(r);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001027 }
1028
1029 @Override
1030 public void removeCallback(Runnable r) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001031 mView.removeCallbacks(r);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001032 }
1033
1034 @Override
1035 public void scrollBy(int dy) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001036 mView.scrollBy(0, dy);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001037 }
1038
1039 @Override
Steve McKayc3ef0d62015-09-08 17:15:25 -07001040 public void showBand(Rect rect) {
1041 mBand.setBounds(rect);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001042
1043 if (!mIsOverlayShown) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001044 mView.getOverlay().add(mBand);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001045 }
1046 }
1047
1048 @Override
1049 public void hideBand() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001050 mView.getOverlay().remove(mBand);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001051 }
Ben Kwa8250db42015-10-07 14:15:12 -07001052
1053 @Override
Ben Kwa22f479d2016-01-20 15:07:53 -08001054 public boolean isLayoutItem(int pos) {
1055 // The band selection model only operates on documents and directories. Exclude other
1056 // types of adapter items (e.g. whitespace items like dividers).
1057 RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
1058 switch (vh.getItemViewType()) {
1059 case ITEM_TYPE_DOCUMENT:
1060 case ITEM_TYPE_DIRECTORY:
1061 return false;
1062 default:
1063 return true;
1064 }
1065 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001066 }
1067
Steve McKay4b3a13c2015-06-11 10:10:49 -07001068 public interface Callback {
1069 /**
1070 * Called when an item is selected or unselected while in selection mode.
1071 *
1072 * @param position Adapter position of the item that was checked or unchecked
1073 * @param selected <code>true</code> if the item is now selected, <code>false</code>
1074 * if the item is now unselected.
1075 */
Ben Kwa743c7c22015-12-01 19:56:57 -08001076 public void onItemStateChanged(String id, boolean selected);
Steve McKay4b3a13c2015-06-11 10:10:49 -07001077
1078 /**
Steve McKaydbec47a2015-08-12 14:48:34 -07001079 * Called prior to an item changing state. Callbacks can cancel
1080 * the change at {@code position} by returning {@code false}.
1081 *
Ben Kwa743c7c22015-12-01 19:56:57 -08001082 * @param id Adapter position of the item that was checked or unchecked
Steve McKaydbec47a2015-08-12 14:48:34 -07001083 * @param selected <code>true</code> if the item is to be selected, <code>false</code>
1084 * if the item is to be unselected.
Steve McKay4b3a13c2015-06-11 10:10:49 -07001085 */
Ben Kwa743c7c22015-12-01 19:56:57 -08001086 public boolean onBeforeItemStateChange(String id, boolean selected);
Steve McKaydbec47a2015-08-12 14:48:34 -07001087
1088 /**
1089 * Called immediately after completion of any set of changes.
1090 */
1091 public void onSelectionChanged();
Steve McKay4b3a13c2015-06-11 10:10:49 -07001092 }
1093
1094 /**
Kyle Horimoto2c802572015-08-18 13:25:29 -07001095 * Provides mouse driven band-select support when used in conjunction with {@link RecyclerView}
1096 * and {@link MultiSelectManager}. This class is responsible for rendering the band select
1097 * overlay and selecting overlaid items via MultiSelectManager.
1098 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001099 public class BandController extends RecyclerView.OnScrollListener
1100 implements GridModel.OnSelectionChangedListener {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001101
1102 private static final int NOT_SET = -1;
1103
Steve McKay635b0942015-09-03 16:49:51 -07001104 private final Runnable mModelBuilder;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001105
Steve McKay635b0942015-09-03 16:49:51 -07001106 @Nullable private Rect mBounds;
1107 @Nullable private Point mCurrentPosition;
1108 @Nullable private Point mOrigin;
Steve McKayc3ef0d62015-09-08 17:15:25 -07001109 @Nullable private GridModel mModel;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001110
1111 // The time at which the current band selection-induced scroll began. If no scroll is in
1112 // progress, the value is NOT_SET.
1113 private long mScrollStartTime = NOT_SET;
1114 private final Runnable mViewScroller = new ViewScroller();
1115
Steve McKay669ebe72015-10-19 12:04:21 -07001116 public BandController() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001117 mEnvironment.addOnScrollListener(this);
Steve McKay635b0942015-09-03 16:49:51 -07001118
1119 mModelBuilder = new Runnable() {
1120 @Override
1121 public void run() {
Steve McKay44408262016-01-05 15:27:17 -08001122 mModel = new GridModel(mEnvironment, mAdapter);
Steve McKayc3ef0d62015-09-08 17:15:25 -07001123 mModel.addOnSelectionChangedListener(BandController.this);
Steve McKay635b0942015-09-03 16:49:51 -07001124 }
1125 };
1126 }
1127
Steve McKay669ebe72015-10-19 12:04:21 -07001128 public boolean handleEvent(MotionInputEvent e) {
1129 // b/23793622 notes the fact that we *never* receive ACTION_DOWN
1130 // events in onTouchEvent. Where it not for this issue, we'd
1131 // push start handling down into handleInputEvent.
1132 if (mBandManager.shouldStart(e)) {
1133 // endBandSelect is handled in handleInputEvent.
1134 mBandManager.startBandSelect(e.getOrigin());
1135 } else if (mBandManager.isActive()
1136 && e.isMouseEvent()
1137 && e.isActionUp()) {
1138 // Same issue here w b/23793622. The ACTION_UP event
1139 // is only evert dispatched to onTouchEvent when
1140 // there is some associated motion. If a user taps
1141 // mouse, but doesn't move, then band select gets
1142 // started BUT not ended. Causing phantom
1143 // bands to appear when the user later clicks to start
1144 // band select.
1145 mBandManager.processInputEvent(e);
1146 }
1147
1148 return isActive();
1149 }
1150
Steve McKay635b0942015-09-03 16:49:51 -07001151 private boolean isActive() {
1152 return mModel != null;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001153 }
1154
1155 /**
Kyle Horimotod017db02015-08-27 16:44:00 -07001156 * Handle a change in layout by cleaning up and getting rid of the old model and creating
1157 * a new model which will track the new layout.
1158 */
1159 public void handleLayoutChanged() {
Steve McKay635b0942015-09-03 16:49:51 -07001160 if (mModel != null) {
1161 mModel.removeOnSelectionChangedListener(this);
1162 mModel.stopListening();
Kyle Horimotod017db02015-08-27 16:44:00 -07001163
Steve McKay635b0942015-09-03 16:49:51 -07001164 // build a new model, all fresh and happy.
1165 mModelBuilder.run();
1166 }
1167 }
1168
Steve McKay669ebe72015-10-19 12:04:21 -07001169 boolean shouldStart(MotionInputEvent e) {
Steve McKay635b0942015-09-03 16:49:51 -07001170 return !isActive()
Steve McKay669ebe72015-10-19 12:04:21 -07001171 && e.isMouseEvent() // a mouse
1172 && e.isActionDown() // the initial button press
Steve McKay44408262016-01-05 15:27:17 -08001173 && mAdapter.getItemCount() > 0
Steve McKay669ebe72015-10-19 12:04:21 -07001174 && e.getItemPosition() == RecyclerView.NO_ID; // in empty space
Steve McKay635b0942015-09-03 16:49:51 -07001175 }
1176
1177 boolean shouldStop(InputEvent input) {
1178 return isActive()
1179 && input.isMouseEvent()
1180 && input.isActionUp();
Kyle Horimotod017db02015-08-27 16:44:00 -07001181 }
1182
1183 /**
Kyle Horimoto2c802572015-08-18 13:25:29 -07001184 * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
Steve McKay635b0942015-09-03 16:49:51 -07001185 * @param input
Kyle Horimoto2c802572015-08-18 13:25:29 -07001186 */
Steve McKay669ebe72015-10-19 12:04:21 -07001187 private void processInputEvent(InputEvent input) {
Steve McKay635b0942015-09-03 16:49:51 -07001188 checkArgument(input.isMouseEvent());
1189
1190 if (shouldStop(input)) {
Steve McKaya698d632015-09-04 14:51:16 -07001191 endBandSelect();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001192 return;
1193 }
1194
Steve McKay635b0942015-09-03 16:49:51 -07001195 // We shouldn't get any events in this method when band select is not active,
1196 // but it turns some guests show up late to the party.
1197 if (!isActive()) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001198 return;
1199 }
1200
Steve McKay635b0942015-09-03 16:49:51 -07001201 mCurrentPosition = input.getOrigin();
1202 mModel.resizeSelection(input.getOrigin());
Kyle Horimoto2c802572015-08-18 13:25:29 -07001203 scrollViewIfNecessary();
1204 resizeBandSelectRectangle();
1205 }
1206
1207 /**
1208 * Starts band select by adding the drawable to the RecyclerView's overlay.
1209 */
Steve McKay635b0942015-09-03 16:49:51 -07001210 private void startBandSelect(Point origin) {
1211 if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
1212
1213 mOrigin = origin;
1214 mModelBuilder.run(); // Creates a new selection model.
Kyle Horimoto2c802572015-08-18 13:25:29 -07001215 mModel.startSelection(mOrigin);
1216 }
1217
1218 /**
1219 * Scrolls the view if necessary.
1220 */
1221 private void scrollViewIfNecessary() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001222 mEnvironment.removeCallback(mViewScroller);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001223 mViewScroller.run();
Steve McKayc3ef0d62015-09-08 17:15:25 -07001224 mEnvironment.invalidateView();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001225 }
1226
1227 /**
1228 * Resizes the band select rectangle by using the origin and the current pointer position as
1229 * two opposite corners of the selection.
1230 */
1231 private void resizeBandSelectRectangle() {
Steve McKay635b0942015-09-03 16:49:51 -07001232 mBounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
1233 Math.min(mOrigin.y, mCurrentPosition.y),
1234 Math.max(mOrigin.x, mCurrentPosition.x),
1235 Math.max(mOrigin.y, mCurrentPosition.y));
Steve McKayc3ef0d62015-09-08 17:15:25 -07001236 mEnvironment.showBand(mBounds);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001237 }
1238
1239 /**
1240 * Ends band select by removing the overlay.
1241 */
1242 private void endBandSelect() {
1243 if (DEBUG) Log.d(TAG, "Ending band select.");
Steve McKay635b0942015-09-03 16:49:51 -07001244
Steve McKayc3ef0d62015-09-08 17:15:25 -07001245 mEnvironment.hideBand();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001246 mSelection.applyProvisionalSelection();
1247 mModel.endSelection();
Ben Kwafcb54d82015-12-10 15:21:18 -08001248 int firstSelected = mModel.getPositionNearestOrigin();
Ben Kwa20887032015-12-22 15:27:14 -08001249 if (firstSelected != NOT_SET) {
Steve McKay44408262016-01-05 15:27:17 -08001250 if (mSelection.contains(mAdapter.getModelId(firstSelected))) {
Ben Kwa20887032015-12-22 15:27:14 -08001251 // TODO: firstSelected should really be lastSelected, we want to anchor the item
1252 // where the mouse-up occurred.
1253 setSelectionRangeBegin(firstSelected);
1254 } else {
1255 // TODO: Check if this is really happening.
1256 Log.w(TAG, "First selected by band is NOT in selection!");
1257 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001258 }
Steve McKay635b0942015-09-03 16:49:51 -07001259
1260 mModel = null;
1261 mOrigin = null;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001262 }
1263
1264 @Override
Ben Kwa743c7c22015-12-01 19:56:57 -08001265 public void onSelectionChanged(Set<String> updatedSelection) {
1266 Map<String, Boolean> delta = mSelection.setProvisionalSelection(updatedSelection);
1267 for (Map.Entry<String, Boolean> entry: delta.entrySet()) {
1268 notifyItemStateChanged(entry.getKey(), entry.getValue());
Kyle Horimoto2c802572015-08-18 13:25:29 -07001269 }
1270 notifySelectionChanged();
1271 }
1272
1273 private class ViewScroller implements Runnable {
1274 /**
1275 * The number of milliseconds of scrolling at which scroll speed continues to increase.
1276 * At first, the scroll starts slowly; then, the rate of scrolling increases until it
1277 * reaches its maximum value at after this many milliseconds.
1278 */
1279 private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
1280
1281 @Override
1282 public void run() {
1283 // Compute the number of pixels the pointer's y-coordinate is past the view.
1284 // Negative values mean the pointer is at or before the top of the view, and
1285 // positive values mean that the pointer is at or after the bottom of the view. Note
1286 // that one additional pixel is added here so that the view still scrolls when the
1287 // pointer is exactly at the top or bottom.
1288 int pixelsPastView = 0;
Steve McKay635b0942015-09-03 16:49:51 -07001289 if (mCurrentPosition.y <= 0) {
1290 pixelsPastView = mCurrentPosition.y - 1;
Steve McKayc3ef0d62015-09-08 17:15:25 -07001291 } else if (mCurrentPosition.y >= mEnvironment.getHeight() - 1) {
1292 pixelsPastView = mCurrentPosition.y - mEnvironment.getHeight() + 1;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001293 }
1294
Steve McKay635b0942015-09-03 16:49:51 -07001295 if (!isActive() || pixelsPastView == 0) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001296 // If band selection is inactive, or if it is active but not at the edge of the
1297 // view, no scrolling is necessary.
1298 mScrollStartTime = NOT_SET;
1299 return;
1300 }
1301
1302 if (mScrollStartTime == NOT_SET) {
1303 // If the pointer was previously not at the edge of the view but now is, set the
1304 // start time for the scroll.
1305 mScrollStartTime = System.currentTimeMillis();
1306 }
1307
1308 // Compute the number of pixels to scroll, and scroll that many pixels.
1309 final int numPixels = computeScrollDistance(
1310 pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
Steve McKayc3ef0d62015-09-08 17:15:25 -07001311 mEnvironment.scrollBy(numPixels);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001312
Steve McKayc3ef0d62015-09-08 17:15:25 -07001313 mEnvironment.removeCallback(mViewScroller);
1314 mEnvironment.runAtNextFrame(this);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001315 }
1316
1317 /**
1318 * Computes the number of pixels to scroll based on how far the pointer is past the end
1319 * of the view and how long it has been there. Roughly based on ItemTouchHelper's
1320 * algorithm for computing the number of pixels to scroll when an item is dragged to the
1321 * end of a {@link RecyclerView}.
1322 * @param pixelsPastView
1323 * @param scrollDuration
1324 * @return
1325 */
1326 private int computeScrollDistance(int pixelsPastView, long scrollDuration) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001327 final int maxScrollStep = mEnvironment.getHeight();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001328 final int direction = (int) Math.signum(pixelsPastView);
1329 final int absPastView = Math.abs(pixelsPastView);
1330
1331 // Calculate the ratio of how far out of the view the pointer currently resides to
1332 // the entire height of the view.
1333 final float outOfBoundsRatio = Math.min(
Steve McKayc3ef0d62015-09-08 17:15:25 -07001334 1.0f, (float) absPastView / mEnvironment.getHeight());
Kyle Horimoto2c802572015-08-18 13:25:29 -07001335 // Interpolate this ratio and use it to compute the maximum scroll that should be
1336 // possible for this step.
1337 final float cappedScrollStep =
1338 direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);
1339
1340 // Likewise, calculate the ratio of the time spent in the scroll to the limit.
1341 final float timeRatio = Math.min(
1342 1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
1343 // Interpolate this ratio and use it to compute the final number of pixels to
1344 // scroll.
1345 final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));
1346
1347 // If the final number of pixels to scroll ends up being 0, the view should still
1348 // scroll at least one pixel.
1349 return numPixels != 0 ? numPixels : direction;
1350 }
1351
1352 /**
1353 * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
1354 * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
1355 * drags that are at the edge or barely past the edge of the view still cause sufficient
1356 * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if
1357 * needed.
1358 * @param ratio A ratio which is in the range [0, 1].
1359 * @return A "smoothed" value, also in the range [0, 1].
1360 */
1361 private float smoothOutOfBoundsRatio(float ratio) {
1362 return (float) Math.pow(ratio - 1.0f, 5) + 1.0f;
1363 }
1364
1365 /**
1366 * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1)
1367 * and stays close to 0 for most input values except those very close to 1. This ensures
1368 * that scrolls start out very slowly but speed up drastically after the scroll has been
1369 * in progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used,
1370 * but this could also be tweaked if needed.
1371 * @param ratio A ratio which is in the range [0, 1].
1372 * @return A "smoothed" value, also in the range [0, 1].
1373 */
1374 private float smoothTimeRatio(float ratio) {
1375 return (float) Math.pow(ratio, 5);
1376 }
1377 };
Kyle Horimotoea910a12015-08-25 17:12:42 -07001378
1379 @Override
1380 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
Steve McKay635b0942015-09-03 16:49:51 -07001381 if (!isActive()) {
Kyle Horimotoea910a12015-08-25 17:12:42 -07001382 return;
1383 }
1384
1385 // Adjust the y-coordinate of the origin the opposite number of pixels so that the
1386 // origin remains in the same place relative to the view's items.
1387 mOrigin.y -= dy;
1388 resizeBandSelectRectangle();
1389 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001390 }
1391
1392 /**
1393 * Provides a band selection item model for views within a RecyclerView. This class queries the
1394 * RecyclerView to determine where its items are placed; then, once band selection is underway,
1395 * it alerts listeners of which items are covered by the selections.
1396 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001397 public static final class GridModel extends RecyclerView.OnScrollListener {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001398
1399 public static final int NOT_SET = -1;
1400
1401 // Enum values used to determine the corner at which the origin is located within the
1402 private static final int UPPER = 0x00;
1403 private static final int LOWER = 0x01;
1404 private static final int LEFT = 0x00;
1405 private static final int RIGHT = 0x02;
1406 private static final int UPPER_LEFT = UPPER | LEFT;
1407 private static final int UPPER_RIGHT = UPPER | RIGHT;
1408 private static final int LOWER_LEFT = LOWER | LEFT;
1409 private static final int LOWER_RIGHT = LOWER | RIGHT;
1410
Ben Kwa8250db42015-10-07 14:15:12 -07001411 private final SelectionEnvironment mHelper;
Steve McKay44408262016-01-05 15:27:17 -08001412 private final DocumentsAdapter mAdapter;
1413
Steve McKayc3ef0d62015-09-08 17:15:25 -07001414 private final List<OnSelectionChangedListener> mOnSelectionChangedListeners =
1415 new ArrayList<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001416
Kyle Horimoto786babe2015-08-25 21:03:38 -07001417 // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed
1418 // by their y-offset. For example, if the first column of the view starts at an x-value of 5,
1419 // mColumns.get(5) would return an array of positions in that column. Within that array, the
1420 // value for key y is the adapter position for the item whose y-offset is y.
Ben Kwafcb54d82015-12-10 15:21:18 -08001421 private final SparseArray<SparseIntArray> mColumns = new SparseArray<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001422
Steve McKayc3ef0d62015-09-08 17:15:25 -07001423 // List of limits along the x-axis (columns).
1424 // This list is sorted from furthest left to furthest right.
1425 private final List<Limits> mColumnBounds = new ArrayList<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001426
Steve McKayc3ef0d62015-09-08 17:15:25 -07001427 // List of limits along the y-axis (rows). Note that this list only contains items which
1428 // have been in the viewport.
1429 private final List<Limits> mRowBounds = new ArrayList<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001430
1431 // The adapter positions which have been recorded so far.
Ben Kwafcb54d82015-12-10 15:21:18 -08001432 private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001433
1434 // Array passed to registered OnSelectionChangedListeners. One array is created and reused
1435 // throughout the lifetime of the object.
Ben Kwa743c7c22015-12-01 19:56:57 -08001436 private final Set<String> mSelection = new HashSet<>();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001437
1438 // The current pointer (in absolute positioning from the top of the view).
1439 private Point mPointer = null;
1440
1441 // The bounds of the band selection.
1442 private RelativePoint mRelativeOrigin;
1443 private RelativePoint mRelativePointer;
1444
1445 private boolean mIsActive;
1446
1447 // Tracks where the band select originated from. This is used to determine where selections
1448 // should expand from when Shift+click is used.
Ben Kwafcb54d82015-12-10 15:21:18 -08001449 private int mPositionNearestOrigin = NOT_SET;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001450
Steve McKay44408262016-01-05 15:27:17 -08001451 GridModel(SelectionEnvironment helper, DocumentsAdapter adapter) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001452 mHelper = helper;
Steve McKay44408262016-01-05 15:27:17 -08001453 mAdapter = adapter;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001454 mHelper.addOnScrollListener(this);
1455 }
1456
1457 /**
1458 * Stops listening to the view's scrolls. Call this function before discarding a
1459 * BandSelecModel object to prevent memory leaks.
1460 */
1461 void stopListening() {
1462 mHelper.removeOnScrollListener(this);
1463 }
1464
1465 /**
1466 * Start a band select operation at the given point.
1467 * @param relativeOrigin The origin of the band select operation, relative to the viewport.
1468 * For example, if the view is scrolled to the bottom, the top-left of the viewport
1469 * would have a relative origin of (0, 0), even though its absolute point has a higher
1470 * y-value.
1471 */
1472 void startSelection(Point relativeOrigin) {
1473 mIsActive = true;
1474 mPointer = mHelper.createAbsolutePoint(relativeOrigin);
1475
1476 recordVisibleChildren();
1477 mRelativeOrigin = new RelativePoint(mPointer);
1478 mRelativePointer = new RelativePoint(mPointer);
1479 computeCurrentSelection();
1480 notifyListeners();
1481 }
1482
1483 /**
1484 * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
1485 * opposite the origin.
1486 * @param relativePointer The pointer (opposite of the origin) of the band select operation,
1487 * relative to the viewport. For example, if the view is scrolled to the bottom, the
1488 * top-left of the viewport would have a relative origin of (0, 0), even though its
1489 * absolute point has a higher y-value.
1490 */
1491 void resizeSelection(Point relativePointer) {
1492 mPointer = mHelper.createAbsolutePoint(relativePointer);
1493 updateModel();
1494 }
1495
1496 /**
1497 * Ends the band selection.
1498 */
1499 void endSelection() {
1500 mIsActive = false;
1501 }
1502
1503 /**
1504 * @return The adapter position for the item nearest the origin corresponding to the latest
1505 * band select operation, or NOT_SET if the selection did not cover any items.
1506 */
Ben Kwafcb54d82015-12-10 15:21:18 -08001507 int getPositionNearestOrigin() {
1508 return mPositionNearestOrigin;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001509 }
1510
1511 @Override
1512 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
1513 if (!mIsActive) {
1514 return;
1515 }
1516
1517 mPointer.x += dx;
1518 mPointer.y += dy;
1519 recordVisibleChildren();
1520 updateModel();
1521 }
1522
1523 /**
1524 * Queries the view for all children and records their location metadata.
1525 */
1526 private void recordVisibleChildren() {
1527 for (int i = 0; i < mHelper.getVisibleChildCount(); i++) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001528 int adapterPosition = mHelper.getAdapterPositionAt(i);
Ben Kwa22f479d2016-01-20 15:07:53 -08001529 if (!mHelper.isLayoutItem(adapterPosition) &&
1530 !mKnownPositions.get(adapterPosition)) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001531 mKnownPositions.put(adapterPosition, true);
1532 recordItemData(mHelper.getAbsoluteRectForChildViewAt(i), adapterPosition);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001533 }
1534 }
1535 }
1536
1537 /**
1538 * Updates the limits lists and column map with the given item metadata.
1539 * @param absoluteChildRect The absolute rectangle for the child view being processed.
1540 * @param adapterPosition The position of the child view being processed.
1541 */
Ben Kwafcb54d82015-12-10 15:21:18 -08001542 private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001543 if (mColumnBounds.size() != mHelper.getColumnCount()) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001544 // If not all x-limits have been recorded, record this one.
1545 recordLimits(
Steve McKayc3ef0d62015-09-08 17:15:25 -07001546 mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right));
Kyle Horimoto2c802572015-08-18 13:25:29 -07001547 }
1548
Steve McKayc3ef0d62015-09-08 17:15:25 -07001549 if (mRowBounds.size() != mHelper.getRowCount()) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001550 // If not all y-limits have been recorded, record this one.
1551 recordLimits(
Steve McKayc3ef0d62015-09-08 17:15:25 -07001552 mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
Kyle Horimoto2c802572015-08-18 13:25:29 -07001553 }
1554
Ben Kwafcb54d82015-12-10 15:21:18 -08001555 SparseIntArray columnList = mColumns.get(absoluteChildRect.left);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001556 if (columnList == null) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001557 columnList = new SparseIntArray();
Kyle Horimoto2c802572015-08-18 13:25:29 -07001558 mColumns.put(absoluteChildRect.left, columnList);
1559 }
Ben Kwafcb54d82015-12-10 15:21:18 -08001560 columnList.put(absoluteChildRect.top, adapterPosition);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001561 }
1562
1563 /**
1564 * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
1565 * does not exist.
1566 */
1567 private void recordLimits(List<Limits> limitsList, Limits limits) {
1568 int index = Collections.binarySearch(limitsList, limits);
1569 if (index < 0) {
1570 limitsList.add(~index, limits);
1571 }
1572 }
1573
1574 /**
1575 * Handles a moved pointer; this function determines whether the pointer movement resulted
1576 * in a selection change and, if it has, notifies listeners of this change.
1577 */
1578 private void updateModel() {
1579 RelativePoint old = mRelativePointer;
1580 mRelativePointer = new RelativePoint(mPointer);
1581 if (old != null && mRelativePointer.equals(old)) {
1582 return;
1583 }
1584
1585 computeCurrentSelection();
1586 notifyListeners();
1587 }
1588
1589 /**
1590 * Computes the currently-selected items.
1591 */
1592 private void computeCurrentSelection() {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001593 if (areItemsCoveredByBand(mRelativePointer, mRelativeOrigin)) {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001594 updateSelection(computeBounds());
1595 } else {
1596 mSelection.clear();
Ben Kwafcb54d82015-12-10 15:21:18 -08001597 mPositionNearestOrigin = NOT_SET;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001598 }
1599 }
1600
1601 /**
1602 * Notifies all listeners of a selection change. Note that this function simply passes
1603 * mSelection, so computeCurrentSelection() should be called before this
1604 * function.
1605 */
1606 private void notifyListeners() {
1607 for (OnSelectionChangedListener listener : mOnSelectionChangedListeners) {
1608 listener.onSelectionChanged(mSelection);
1609 }
1610 }
1611
1612 /**
1613 * @param rect Rectangle including all covered items.
1614 */
1615 private void updateSelection(Rect rect) {
Ben Kwa22f479d2016-01-20 15:07:53 -08001616 int columnStart =
Steve McKayc3ef0d62015-09-08 17:15:25 -07001617 Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
Ben Kwa22f479d2016-01-20 15:07:53 -08001618 checkState(columnStart >= 0);
1619 int columnEnd = columnStart;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001620
Ben Kwa22f479d2016-01-20 15:07:53 -08001621 for (int i = columnStart; i < mColumnBounds.size()
Steve McKayc3ef0d62015-09-08 17:15:25 -07001622 && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
Ben Kwa22f479d2016-01-20 15:07:53 -08001623 columnEnd = i;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001624 }
1625
Ben Kwa22f479d2016-01-20 15:07:53 -08001626 int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
1627 if (rowStart < 0) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001628 mPositionNearestOrigin = NOT_SET;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001629 return;
1630 }
1631
Ben Kwa22f479d2016-01-20 15:07:53 -08001632 int rowEnd = rowStart;
1633 for (int i = rowStart; i < mRowBounds.size()
1634 && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
1635 rowEnd = i;
Kyle Horimoto2c802572015-08-18 13:25:29 -07001636 }
1637
Ben Kwa22f479d2016-01-20 15:07:53 -08001638 updateSelection(columnStart, columnEnd, rowStart, rowEnd);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001639 }
1640
1641 /**
1642 * Computes the selection given the previously-computed start- and end-indices for each
1643 * row and column.
1644 */
1645 private void updateSelection(
1646 int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
Ben Kwa22f479d2016-01-20 15:07:53 -08001647 if (DEBUG) Log.d(TAG, String.format("updateSelection: %d, %d, %d, %d",
1648 columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
1649
Kyle Horimoto2c802572015-08-18 13:25:29 -07001650 mSelection.clear();
1651 for (int column = columnStartIndex; column <= columnEndIndex; column++) {
Ben Kwafcb54d82015-12-10 15:21:18 -08001652 SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001653 for (int row = rowStartIndex; row <= rowEndIndex; row++) {
Ben Kwa20887032015-12-22 15:27:14 -08001654 // The default return value for SparseIntArray.get is 0, which is a valid
1655 // position. Use a sentry value to prevent erroneously selecting item 0.
Ben Kwa22f479d2016-01-20 15:07:53 -08001656 final int rowKey = mRowBounds.get(row).lowerLimit;
1657 int position = items.get(rowKey, NOT_SET);
Ben Kwa20887032015-12-22 15:27:14 -08001658 if (position != NOT_SET) {
Steve McKay44408262016-01-05 15:27:17 -08001659 String id = mAdapter.getModelId(position);
Ben Kwa20887032015-12-22 15:27:14 -08001660 if (id != null) {
1661 // The adapter inserts items for UI layout purposes that aren't associated
1662 // with files. Those will have a null model ID. Don't select them.
1663 mSelection.add(id);
1664 }
1665 if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
1666 row, rowStartIndex, rowEndIndex)) {
1667 // If this is the position nearest the origin, record it now so that it
1668 // can be returned by endSelection() later.
1669 mPositionNearestOrigin = position;
1670 }
Kyle Horimoto2c802572015-08-18 13:25:29 -07001671 }
1672 }
1673 }
1674 }
1675
1676 /**
1677 * @return Returns true if the position is the nearest to the origin, or, in the case of the
1678 * lower-right corner, whether it is possible that the position is the nearest to the
1679 * origin. See comment below for reasoning for this special case.
1680 */
1681 private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
1682 int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
1683 int corner = computeCornerNearestOrigin();
1684 switch (corner) {
1685 case UPPER_LEFT:
1686 return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
1687 case UPPER_RIGHT:
1688 return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
1689 case LOWER_LEFT:
1690 return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
1691 case LOWER_RIGHT:
1692 // Note that in some cases, the last row will not have as many items as there
1693 // are columns (e.g., if there are 4 items and 3 columns, the second row will
1694 // only have one item in the first column). This function is invoked for each
1695 // position from left to right, so return true for any position in the bottom
1696 // row and only the right-most position in the bottom row will be recorded.
1697 return rowIndex == rowEndIndex;
1698 default:
1699 throw new RuntimeException("Invalid corner type.");
1700 }
1701 }
1702
1703 /**
1704 * Listener for changes in which items have been band selected.
1705 */
1706 static interface OnSelectionChangedListener {
Ben Kwa743c7c22015-12-01 19:56:57 -08001707 public void onSelectionChanged(Set<String> updatedSelection);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001708 }
1709
1710 void addOnSelectionChangedListener(OnSelectionChangedListener listener) {
1711 mOnSelectionChangedListeners.add(listener);
1712 }
1713
1714 void removeOnSelectionChangedListener(OnSelectionChangedListener listener) {
1715 mOnSelectionChangedListeners.remove(listener);
1716 }
1717
1718 /**
Kyle Horimoto2c802572015-08-18 13:25:29 -07001719 * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
1720 * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
1721 * of item columns and the top- and bottom sides of item rows so that it can be determined
1722 * whether the pointer is located within the bounds of an item.
1723 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001724 private static class Limits implements Comparable<Limits> {
Kyle Horimoto2c802572015-08-18 13:25:29 -07001725 int lowerLimit;
1726 int upperLimit;
1727
1728 Limits(int lowerLimit, int upperLimit) {
1729 this.lowerLimit = lowerLimit;
1730 this.upperLimit = upperLimit;
1731 }
1732
1733 @Override
1734 public int compareTo(Limits other) {
1735 return lowerLimit - other.lowerLimit;
1736 }
1737
1738 @Override
1739 public boolean equals(Object other) {
1740 if (!(other instanceof Limits)) {
1741 return false;
1742 }
1743
1744 return ((Limits) other).lowerLimit == lowerLimit &&
1745 ((Limits) other).upperLimit == upperLimit;
1746 }
1747 }
1748
1749 /**
1750 * The location of a coordinate relative to items. This class represents a general area of the
1751 * view as it relates to band selection rather than an explicit point. For example, two
1752 * different points within an item are considered to have the same "location" because band
1753 * selection originating within the item would select the same items no matter which point
1754 * was used. Same goes for points between items as well as those at the very beginning or end
1755 * of the view.
1756 *
1757 * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
1758 * advantage of tying the value to the Limits of items along that axis. This allows easy
1759 * selection of items within those Limits as opposed to a search through every item to see if a
1760 * given coordinate value falls within those Limits.
1761 */
Steve McKayc3ef0d62015-09-08 17:15:25 -07001762 private static class RelativeCoordinate
Kyle Horimoto2c802572015-08-18 13:25:29 -07001763 implements Comparable<RelativeCoordinate> {
1764 /**
1765 * Location describing points after the last known item.
1766 */
1767 static final int AFTER_LAST_ITEM = 0;
1768
1769 /**
1770 * Location describing points before the first known item.
1771 */
1772 static final int BEFORE_FIRST_ITEM = 1;
1773
1774 /**
1775 * Location describing points between two items.
1776 */
1777 static final int BETWEEN_TWO_ITEMS = 2;
1778
1779 /**
1780 * Location describing points within the limits of one item.
1781 */
1782 static final int WITHIN_LIMITS = 3;
1783
1784 /**
1785 * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
1786 * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
1787 */
1788 final int type;
1789
1790 /**
1791 * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
1792 * BETWEEN_TWO_ITEMS.
1793 */
1794 Limits limitsBeforeCoordinate;
1795
1796 /**
1797 * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
1798 */
1799 Limits limitsAfterCoordinate;
1800
1801 // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
1802 Limits mFirstKnownItem;
1803 // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
1804 Limits mLastKnownItem;
1805
1806 /**
1807 * @param limitsList The sorted limits list for the coordinate type. If this
1808 * CoordinateLocation is an x-value, mXLimitsList should be passed; otherwise,
1809 * mYLimitsList should be pased.
1810 * @param value The coordinate value.
1811 */
1812 RelativeCoordinate(List<Limits> limitsList, int value) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001813 int index = Collections.binarySearch(limitsList, new Limits(value, value));
Kyle Horimoto2c802572015-08-18 13:25:29 -07001814
1815 if (index >= 0) {
1816 this.type = WITHIN_LIMITS;
1817 this.limitsBeforeCoordinate = limitsList.get(index);
1818 } else if (~index == 0) {
1819 this.type = BEFORE_FIRST_ITEM;
1820 this.mFirstKnownItem = limitsList.get(0);
1821 } else if (~index == limitsList.size()) {
1822 Limits lastLimits = limitsList.get(limitsList.size() - 1);
1823 if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
1824 this.type = WITHIN_LIMITS;
1825 this.limitsBeforeCoordinate = lastLimits;
1826 } else {
1827 this.type = AFTER_LAST_ITEM;
1828 this.mLastKnownItem = lastLimits;
1829 }
1830 } else {
1831 Limits limitsBeforeIndex = limitsList.get(~index - 1);
1832 if (limitsBeforeIndex.lowerLimit <= value && value <= limitsBeforeIndex.upperLimit) {
1833 this.type = WITHIN_LIMITS;
1834 this.limitsBeforeCoordinate = limitsList.get(~index - 1);
1835 } else {
1836 this.type = BETWEEN_TWO_ITEMS;
1837 this.limitsBeforeCoordinate = limitsList.get(~index - 1);
1838 this.limitsAfterCoordinate = limitsList.get(~index);
1839 }
1840 }
1841 }
1842
1843 int toComparisonValue() {
1844 if (type == BEFORE_FIRST_ITEM) {
1845 return mFirstKnownItem.lowerLimit - 1;
1846 } else if (type == AFTER_LAST_ITEM) {
1847 return mLastKnownItem.upperLimit + 1;
1848 } else if (type == BETWEEN_TWO_ITEMS) {
1849 return limitsBeforeCoordinate.upperLimit + 1;
1850 } else {
1851 return limitsBeforeCoordinate.lowerLimit;
1852 }
1853 }
1854
1855 @Override
1856 public boolean equals(Object other) {
1857 if (!(other instanceof RelativeCoordinate)) {
1858 return false;
1859 }
1860
1861 RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
1862 return toComparisonValue() == otherCoordinate.toComparisonValue();
1863 }
1864
1865 @Override
1866 public int compareTo(RelativeCoordinate other) {
1867 return toComparisonValue() - other.toComparisonValue();
1868 }
1869 }
1870
1871 /**
1872 * The location of a point relative to the Limits of nearby items; consists of both an x- and
1873 * y-RelativeCoordinateLocation.
1874 */
1875 private class RelativePoint {
1876 final RelativeCoordinate xLocation;
1877 final RelativeCoordinate yLocation;
1878
1879 RelativePoint(Point point) {
Steve McKayc3ef0d62015-09-08 17:15:25 -07001880 this.xLocation = new RelativeCoordinate(mColumnBounds, point.x);
1881 this.yLocation = new RelativeCoordinate(mRowBounds, point.y);
Kyle Horimoto2c802572015-08-18 13:25:29 -07001882 }
1883
1884 @Override
1885 public boolean equals(Object other) {
1886 if (!(other instanceof RelativePoint)) {
1887 return false;
1888 }
1889
1890 RelativePoint otherPoint = (RelativePoint) other;
1891 return xLocation.equals(otherPoint.xLocation) && yLocation.equals(otherPoint.yLocation);
1892 }
1893 }
1894
1895 /**
1896 * Generates a rectangle which contains the items selected by the pointer and origin.
1897 * @return The rectangle, or null if no items were selected.
1898 */
1899 private Rect computeBounds() {
1900 Rect rect = new Rect();
1901 rect.left = getCoordinateValue(
1902 min(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001903 mColumnBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001904 true);
1905 rect.right = getCoordinateValue(
1906 max(mRelativeOrigin.xLocation, mRelativePointer.xLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001907 mColumnBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001908 false);
1909 rect.top = getCoordinateValue(
1910 min(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001911 mRowBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001912 true);
1913 rect.bottom = getCoordinateValue(
1914 max(mRelativeOrigin.yLocation, mRelativePointer.yLocation),
Steve McKayc3ef0d62015-09-08 17:15:25 -07001915 mRowBounds,
Kyle Horimoto2c802572015-08-18 13:25:29 -07001916 false);
1917 return rect;
1918 }
1919
1920 /**
1921 * Computes the corner of the selection nearest the origin.
1922 * @return
1923 */
1924 private int computeCornerNearestOrigin() {
1925 int cornerValue = 0;
1926
1927 if (mRelativeOrigin.yLocation ==
1928 min(mRelativeOrigin.yLocation, mRelativePointer.yLocation)) {
1929 cornerValue |= UPPER;
1930 } else {
1931 cornerValue |= LOWER;
1932 }
1933
1934 if (mRelativeOrigin.xLocation ==
1935 min(mRelativeOrigin.xLocation, mRelativePointer.xLocation)) {
1936 cornerValue |= LEFT;
1937 } else {
1938 cornerValue |= RIGHT;
1939 }
1940
1941 return cornerValue;
1942 }
1943
1944 private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) {
1945 return first.compareTo(second) < 0 ? first : second;
1946 }
1947
1948 private RelativeCoordinate max(RelativeCoordinate first, RelativeCoordinate second) {
1949 return first.compareTo(second) > 0 ? first : second;
1950 }
1951
1952 /**
1953 * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
1954 * coordinate.
1955 */
1956 private int getCoordinateValue(RelativeCoordinate coordinate,
1957 List<Limits> limitsList, boolean isStartOfRange) {
1958 switch (coordinate.type) {
1959 case RelativeCoordinate.BEFORE_FIRST_ITEM:
1960 return limitsList.get(0).lowerLimit;
1961 case RelativeCoordinate.AFTER_LAST_ITEM:
1962 return limitsList.get(limitsList.size() - 1).upperLimit;
1963 case RelativeCoordinate.BETWEEN_TWO_ITEMS:
1964 if (isStartOfRange) {
1965 return coordinate.limitsAfterCoordinate.lowerLimit;
1966 } else {
1967 return coordinate.limitsBeforeCoordinate.upperLimit;
1968 }
1969 case RelativeCoordinate.WITHIN_LIMITS:
1970 return coordinate.limitsBeforeCoordinate.lowerLimit;
1971 }
1972
1973 throw new RuntimeException("Invalid coordinate value.");
1974 }
1975
Steve McKayc3ef0d62015-09-08 17:15:25 -07001976 private boolean areItemsCoveredByBand(
Kyle Horimoto2c802572015-08-18 13:25:29 -07001977 RelativePoint first, RelativePoint second) {
1978 return doesCoordinateLocationCoverItems(first.xLocation, second.xLocation) &&
1979 doesCoordinateLocationCoverItems(first.yLocation, second.yLocation);
1980 }
1981
1982 private boolean doesCoordinateLocationCoverItems(
1983 RelativeCoordinate pointerCoordinate,
1984 RelativeCoordinate originCoordinate) {
1985 if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM &&
1986 originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
1987 return false;
1988 }
1989
1990 if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM &&
1991 originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
1992 return false;
1993 }
1994
1995 if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
1996 originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS &&
1997 pointerCoordinate.limitsBeforeCoordinate.equals(
1998 originCoordinate.limitsBeforeCoordinate) &&
1999 pointerCoordinate.limitsAfterCoordinate.equals(
2000 originCoordinate.limitsAfterCoordinate)) {
2001 return false;
2002 }
2003
2004 return true;
2005 }
2006 }
Steve McKay4b3a13c2015-06-11 10:10:49 -07002007}