Range.java revision 60dadaeed4f5cee272b575dfde6c02e3506a2fa0
1/*
2 * Copyright 2017 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
17package androidx.recyclerview.selection;
18
19import static androidx.core.util.Preconditions.checkArgument;
20import static androidx.recyclerview.selection.Shared.DEBUG;
21import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
22
23import android.util.Log;
24
25import androidx.annotation.IntDef;
26import androidx.annotation.NonNull;
27
28import java.lang.annotation.Retention;
29import java.lang.annotation.RetentionPolicy;
30
31/**
32 * Class providing support for managing range selections.
33 */
34final class Range {
35
36    static final int TYPE_PRIMARY = 0;
37
38    /**
39     * "Provisional" selection represents a overlay on the primary selection. A provisional
40     * selection maybe be eventually added to the primary selection, or it may be abandoned.
41     *
42     * <p>
43     * E.g. BandSelectionHelper creates a provisional selection while a user is actively
44     * selecting items with a band. GestureSelectionHelper creates a provisional selection
45     * while a user is active selecting via gesture.
46     *
47     * <p>
48     * Provisionally selected items are considered to be selected in
49     * {@link Selection#contains(String)} and related methods. A provisional may be abandoned or
50     * merged into the promary selection.
51     *
52     * <p>
53     * A provisional selection may intersect with the primary selection, however clearing the
54     * provisional selection will not affect the primary selection where the two may intersect.
55     */
56    static final int TYPE_PROVISIONAL = 1;
57    @IntDef({
58            TYPE_PRIMARY,
59            TYPE_PROVISIONAL
60    })
61    @Retention(RetentionPolicy.SOURCE)
62    @interface RangeType {}
63
64    private static final String TAG = "Range";
65
66    private final Callbacks mCallbacks;
67    private final int mBegin;
68    private int mEnd = NO_POSITION;
69
70    /**
71     * Creates a new range anchored at {@code position}.
72     *
73     * @param position
74     * @param callbacks
75     */
76    Range(int position, @NonNull Callbacks callbacks) {
77        mBegin = position;
78        mCallbacks = callbacks;
79        if (DEBUG) Log.d(TAG, "Creating new Range anchored @ " + position);
80    }
81
82    void extendRange(int position, @RangeType int type) {
83        checkArgument(position != NO_POSITION, "Position cannot be NO_POSITION.");
84
85        if (mEnd == NO_POSITION || mEnd == mBegin) {
86            // Reset mEnd so it can be established in establishRange.
87            mEnd = NO_POSITION;
88            establishRange(position, type);
89        } else {
90            reviseRange(position, type);
91        }
92    }
93
94    private void establishRange(int position, @RangeType int type) {
95        checkArgument(mEnd == NO_POSITION, "End has already been set.");
96
97        mEnd = position;
98
99        if (position > mBegin) {
100            if (DEBUG) log(type, "Establishing initial range at @ " + position);
101            updateRange(mBegin + 1, position, true, type);
102        } else if (position < mBegin) {
103            if (DEBUG) log(type, "Establishing initial range at @ " + position);
104            updateRange(position, mBegin - 1, true, type);
105        }
106    }
107
108    private void reviseRange(int position, @RangeType int type) {
109        checkArgument(mEnd != NO_POSITION, "End must already be set.");
110        checkArgument(mBegin != mEnd, "Beging and end point to same position.");
111
112        if (position == mEnd) {
113            if (DEBUG) log(type, "Ignoring no-op revision for range @ " + position);
114        }
115
116        if (mEnd > mBegin) {
117            reviseAscending(position, type);
118        } else if (mEnd < mBegin) {
119            reviseDescending(position, type);
120        }
121        // the "else" case is covered by checkState at beginning of method.
122
123        mEnd = position;
124    }
125
126    /**
127     * Updates an existing ascending selection.
128     */
129    private void reviseAscending(int position, @RangeType int type) {
130        if (DEBUG) log(type, "*ascending* Revising range @ " + position);
131
132        if (position < mEnd) {
133            if (position < mBegin) {
134                updateRange(mBegin + 1, mEnd, false, type);
135                updateRange(position, mBegin - 1, true, type);
136            } else {
137                updateRange(position + 1, mEnd, false, type);
138            }
139        } else if (position > mEnd) {   // Extending the range...
140            updateRange(mEnd + 1, position, true, type);
141        }
142    }
143
144    private void reviseDescending(int position, @RangeType int type) {
145        if (DEBUG) log(type, "*descending* Revising range @ " + position);
146
147        if (position > mEnd) {
148            if (position > mBegin) {
149                updateRange(mEnd, mBegin - 1, false, type);
150                updateRange(mBegin + 1, position, true, type);
151            } else {
152                updateRange(mEnd, position - 1, false, type);
153            }
154        } else if (position < mEnd) {   // Extending the range...
155            updateRange(position, mEnd - 1, true, type);
156        }
157    }
158
159    /**
160     * Try to set selection state for all elements in range. Not that callbacks can cancel
161     * selection of specific items, so some or even all items may not reflect the desired state
162     * after the update is complete.
163     *
164     * @param begin    Adapter position for range start (inclusive).
165     * @param end      Adapter position for range end (inclusive).
166     * @param selected New selection state.
167     */
168    private void updateRange(
169            int begin, int end, boolean selected, @RangeType int type) {
170        mCallbacks.updateForRange(begin, end, selected, type);
171    }
172
173    @Override
174    public String toString() {
175        return "Range{begin=" + mBegin + ", end=" + mEnd + "}";
176    }
177
178    private void log(@RangeType int type, String message) {
179        String opType = type == TYPE_PRIMARY ? "PRIMARY" : "PROVISIONAL";
180        Log.d(TAG, String.valueOf(this) + ": " + message + " (" + opType + ")");
181    }
182
183    /*
184     * @see {@link DefaultSelectionTracker#updateForRange(int, int , boolean, int)}.
185     */
186    abstract static class Callbacks {
187        abstract void updateForRange(
188                int begin, int end, boolean selected, @RangeType int type);
189    }
190}
191