blob: 59b2c2a395f7e737ba142f0d20ce02e08ba35396 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2008 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 android.widget;
18
19import android.database.Cursor;
20import android.database.DataSetObserver;
21import android.util.SparseIntArray;
22
23/**
24 * A helper class for adapters that implement the SectionIndexer interface.
25 * If the items in the adapter are sorted by simple alphabet-based sorting, then
26 * this class provides a way to do fast indexing of large lists using binary search.
27 * It caches the indices that have been determined through the binary search and also
28 * invalidates the cache if changes occur in the cursor.
29 * <p/>
30 * Your adapter is responsible for updating the cursor by calling {@link #setCursor} if the
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -070031 * cursor changes. {@link #getPositionForSection} method does the binary search for the starting
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080032 * index of a given section (alphabet).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033 */
34public class AlphabetIndexer extends DataSetObserver implements SectionIndexer {
35
36 /**
37 * Cursor that is used by the adapter of the list view.
38 */
39 protected Cursor mDataCursor;
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -070040
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080041 /**
42 * The index of the cursor column that this list is sorted on.
43 */
44 protected int mColumnIndex;
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -070045
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080046 /**
47 * The string of characters that make up the indexing sections.
48 */
49 protected CharSequence mAlphabet;
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -070050
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051 /**
52 * Cached length of the alphabet array.
53 */
54 private int mAlphabetLength;
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -070055
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080056 /**
57 * This contains a cache of the computed indices so far. It will get reset whenever
58 * the dataset changes or the cursor changes.
59 */
60 private SparseIntArray mAlphaMap;
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -070061
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080062 /**
63 * Use a collator to compare strings in a localized manner.
64 */
65 private java.text.Collator mCollator;
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -070066
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080067 /**
68 * The section array converted from the alphabet string.
69 */
70 private String[] mAlphabetArray;
71
72 /**
73 * Constructs the indexer.
74 * @param cursor the cursor containing the data set
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -070075 * @param sortedColumnIndex the column number in the cursor that is sorted
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080076 * alphabetically
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -070077 * @param alphabet string containing the alphabet, with space as the first character.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080078 * For example, use the string " ABCDEFGHIJKLMNOPQRSTUVWXYZ" for English indexing.
79 * The characters must be uppercase and be sorted in ascii/unicode order. Basically
80 * characters in the alphabet will show up as preview letters.
81 */
82 public AlphabetIndexer(Cursor cursor, int sortedColumnIndex, CharSequence alphabet) {
83 mDataCursor = cursor;
84 mColumnIndex = sortedColumnIndex;
85 mAlphabet = alphabet;
86 mAlphabetLength = alphabet.length();
87 mAlphabetArray = new String[mAlphabetLength];
88 for (int i = 0; i < mAlphabetLength; i++) {
89 mAlphabetArray[i] = Character.toString(mAlphabet.charAt(i));
90 }
91 mAlphaMap = new SparseIntArray(mAlphabetLength);
92 if (cursor != null) {
93 cursor.registerDataSetObserver(this);
94 }
95 // Get a Collator for the current locale for string comparisons.
96 mCollator = java.text.Collator.getInstance();
97 mCollator.setStrength(java.text.Collator.PRIMARY);
98 }
99
100 /**
101 * Returns the section array constructed from the alphabet provided in the constructor.
102 * @return the section array
103 */
104 public Object[] getSections() {
105 return mAlphabetArray;
106 }
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -0700107
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800108 /**
109 * Sets a new cursor as the data set and resets the cache of indices.
110 * @param cursor the new cursor to use as the data set
111 */
112 public void setCursor(Cursor cursor) {
113 if (mDataCursor != null) {
114 mDataCursor.unregisterDataSetObserver(this);
115 }
116 mDataCursor = cursor;
117 if (cursor != null) {
118 mDataCursor.registerDataSetObserver(this);
119 }
120 mAlphaMap.clear();
121 }
122
123 /**
124 * Default implementation compares the first character of word with letter.
125 */
126 protected int compare(String word, String letter) {
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -0700127 final String firstLetter;
128 if (word.length() == 0) {
129 firstLetter = " ";
130 } else {
131 firstLetter = word.substring(0, 1);
132 }
133
134 return mCollator.compare(firstLetter, letter);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800135 }
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -0700136
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800137 /**
138 * Performs a binary search or cache lookup to find the first row that
139 * matches a given section's starting letter.
140 * @param sectionIndex the section to search for
141 * @return the row index of the first occurrence, or the nearest next letter.
142 * For instance, if searching for "T" and no "T" is found, then the first
143 * row starting with "U" or any higher letter is returned. If there is no
144 * data following "T" at all, then the list size is returned.
145 */
146 public int getPositionForSection(int sectionIndex) {
147 final SparseIntArray alphaMap = mAlphaMap;
148 final Cursor cursor = mDataCursor;
149
150 if (cursor == null || mAlphabet == null) {
151 return 0;
152 }
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -0700153
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800154 // Check bounds
155 if (sectionIndex <= 0) {
156 return 0;
157 }
158 if (sectionIndex >= mAlphabetLength) {
159 sectionIndex = mAlphabetLength - 1;
160 }
161
162 int savedCursorPos = cursor.getPosition();
163
164 int count = cursor.getCount();
165 int start = 0;
166 int end = count;
167 int pos;
168
169 char letter = mAlphabet.charAt(sectionIndex);
170 String targetLetter = Character.toString(letter);
171 int key = letter;
172 // Check map
173 if (Integer.MIN_VALUE != (pos = alphaMap.get(key, Integer.MIN_VALUE))) {
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -0700174 // Is it approximate? Using negative value to indicate that it's
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800175 // an approximation and positive value when it is the accurate
176 // position.
177 if (pos < 0) {
178 pos = -pos;
179 end = pos;
180 } else {
181 // Not approximate, this is the confirmed start of section, return it
182 return pos;
183 }
184 }
185
186 // Do we have the position of the previous section?
187 if (sectionIndex > 0) {
188 int prevLetter =
189 mAlphabet.charAt(sectionIndex - 1);
190 int prevLetterPos = alphaMap.get(prevLetter, Integer.MIN_VALUE);
191 if (prevLetterPos != Integer.MIN_VALUE) {
192 start = Math.abs(prevLetterPos);
193 }
194 }
195
196 // Now that we have a possibly optimized start and end, let's binary search
197
198 pos = (end + start) / 2;
199
200 while (pos < end) {
201 // Get letter at pos
202 cursor.moveToPosition(pos);
203 String curName = cursor.getString(mColumnIndex);
204 if (curName == null) {
205 if (pos == 0) {
206 break;
207 } else {
208 pos--;
209 continue;
210 }
211 }
212 int diff = compare(curName, targetLetter);
213 if (diff != 0) {
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -0700214 // TODO: Commenting out approximation code because it doesn't work for certain
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800215 // lists with custom comparators
216 // Enter approximation in hash if a better solution doesn't exist
217 // String startingLetter = Character.toString(getFirstLetter(curName));
218 // int startingLetterKey = startingLetter.charAt(0);
219 // int curPos = alphaMap.get(startingLetterKey, Integer.MIN_VALUE);
220 // if (curPos == Integer.MIN_VALUE || Math.abs(curPos) > pos) {
221 // Negative pos indicates that it is an approximation
222 // alphaMap.put(startingLetterKey, -pos);
223 // }
224 // if (mCollator.compare(startingLetter, targetLetter) < 0) {
225 if (diff < 0) {
226 start = pos + 1;
227 if (start >= count) {
228 pos = count;
229 break;
230 }
231 } else {
232 end = pos;
233 }
234 } else {
235 // They're the same, but that doesn't mean it's the start
236 if (start == pos) {
237 // This is it
238 break;
239 } else {
240 // Need to go further lower to find the starting row
241 end = pos;
242 }
243 }
244 pos = (start + end) / 2;
245 }
246 alphaMap.put(key, pos);
247 cursor.moveToPosition(savedCursorPos);
248 return pos;
249 }
250
251 /**
252 * Returns the section index for a given position in the list by querying the item
253 * and comparing it with all items in the section array.
254 */
255 public int getSectionForPosition(int position) {
256 int savedCursorPos = mDataCursor.getPosition();
257 mDataCursor.moveToPosition(position);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800258 String curName = mDataCursor.getString(mColumnIndex);
Phil Dubach1b111bb2009-06-05 12:27:59 -0700259 mDataCursor.moveToPosition(savedCursorPos);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800260 // Linear search, as there are only a few items in the section index
261 // Could speed this up later if it actually gets used.
262 for (int i = 0; i < mAlphabetLength; i++) {
263 char letter = mAlphabet.charAt(i);
264 String targetLetter = Character.toString(letter);
265 if (compare(curName, targetLetter) == 0) {
266 return i;
267 }
268 }
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -0700269 return 0; // Don't recognize the letter - falls under zero'th section
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800270 }
Dmitri Plotnikov0918bf02009-06-10 16:13:08 -0700271
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800272 /*
273 * @hide
274 */
275 @Override
276 public void onChanged() {
277 super.onChanged();
278 mAlphaMap.clear();
279 }
280
281 /*
282 * @hide
283 */
284 @Override
285 public void onInvalidated() {
286 super.onInvalidated();
287 mAlphaMap.clear();
288 }
289}