1 /* 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 17 package android.widget; 18 19 import android.database.Cursor; 20 import android.database.DataSetObserver; 21 import 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 31 * cursor changes. {@link #getPositionForSection} method does the binary search for the starting 32 * index of a given section (alphabet). 33 */ 34 public 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; 40 41 /** 42 * The index of the cursor column that this list is sorted on. 43 */ 44 protected int mColumnIndex; 45 46 /** 47 * The string of characters that make up the indexing sections. 48 */ 49 protected CharSequence mAlphabet; 50 51 /** 52 * Cached length of the alphabet array. 53 */ 54 private int mAlphabetLength; 55 56 /** 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; 61 62 /** 63 * Use a collator to compare strings in a localized manner. 64 */ 65 private java.text.Collator mCollator; 66 67 /** 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 75 * @param sortedColumnIndex the column number in the cursor that is sorted 76 * alphabetically 77 * @param alphabet string containing the alphabet, with space as the first character. 78 * 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 */ AlphabetIndexer(Cursor cursor, int sortedColumnIndex, CharSequence alphabet)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 */ getSections()104 public Object[] getSections() { 105 return mAlphabetArray; 106 } 107 108 /** 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 */ setCursor(Cursor cursor)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 */ compare(String word, String letter)126 protected int compare(String word, String letter) { 127 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); 135 } 136 137 /** 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 */ getPositionForSection(int sectionIndex)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 } 153 154 // 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))) { 174 // Is it approximate? Using negative value to indicate that it's 175 // 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) { 214 // TODO: Commenting out approximation code because it doesn't work for certain 215 // 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 */ getSectionForPosition(int position)255 public int getSectionForPosition(int position) { 256 int savedCursorPos = mDataCursor.getPosition(); 257 mDataCursor.moveToPosition(position); 258 String curName = mDataCursor.getString(mColumnIndex); 259 mDataCursor.moveToPosition(savedCursorPos); 260 // 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 } 269 return 0; // Don't recognize the letter - falls under zero'th section 270 } 271 272 /* 273 * @hide 274 */ 275 @Override onChanged()276 public void onChanged() { 277 super.onChanged(); 278 mAlphaMap.clear(); 279 } 280 281 /* 282 * @hide 283 */ 284 @Override onInvalidated()285 public void onInvalidated() { 286 super.onInvalidated(); 287 mAlphaMap.clear(); 288 } 289 } 290