OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.enhancedbookmarks; |
| 6 |
| 7 import android.content.Context; |
| 8 import android.graphics.Bitmap; |
| 9 import android.graphics.Typeface; |
| 10 import android.graphics.drawable.Drawable; |
| 11 import android.text.Editable; |
| 12 import android.text.Layout; |
| 13 import android.text.SpannableString; |
| 14 import android.text.SpannableStringBuilder; |
| 15 import android.text.TextUtils; |
| 16 import android.text.TextUtils.TruncateAt; |
| 17 import android.text.TextWatcher; |
| 18 import android.text.style.StyleSpan; |
| 19 import android.util.AttributeSet; |
| 20 import android.util.Pair; |
| 21 import android.view.KeyEvent; |
| 22 import android.view.LayoutInflater; |
| 23 import android.view.View; |
| 24 import android.view.ViewGroup; |
| 25 import android.view.inputmethod.InputMethodManager; |
| 26 import android.widget.AdapterView; |
| 27 import android.widget.AdapterView.OnItemClickListener; |
| 28 import android.widget.ArrayAdapter; |
| 29 import android.widget.BaseAdapter; |
| 30 import android.widget.EditText; |
| 31 import android.widget.ImageButton; |
| 32 import android.widget.LinearLayout; |
| 33 import android.widget.ListView; |
| 34 import android.widget.TextView; |
| 35 import android.widget.ViewSwitcher; |
| 36 |
| 37 import com.google.android.apps.chrome.R; |
| 38 |
| 39 import org.chromium.base.ApiCompatibilityUtils; |
| 40 import org.chromium.chrome.browser.BookmarksBridge.BookmarkItem; |
| 41 import org.chromium.chrome.browser.BookmarksBridge.BookmarkModelObserver; |
| 42 import org.chromium.chrome.browser.enhanced_bookmarks.EnhancedBookmarksBridge.Fi
ltersObserver; |
| 43 import org.chromium.chrome.browser.enhanced_bookmarks.EnhancedBookmarksModel; |
| 44 import org.chromium.chrome.browser.enhanced_bookmarks.LaunchLocation; |
| 45 import org.chromium.chrome.browser.enhancedbookmarks.EnhancedBookmarkSalientImag
eView.SalientImageDrawableFactory; |
| 46 import org.chromium.chrome.browser.widget.CustomShapeDrawable.CircularDrawable; |
| 47 import org.chromium.components.bookmarks.BookmarkId; |
| 48 import org.chromium.components.bookmarks.BookmarkMatch; |
| 49 |
| 50 import java.util.ArrayList; |
| 51 import java.util.List; |
| 52 |
| 53 /** |
| 54 * UI implementation for search in enhanced bookmark. Search results will be upd
ated when user is |
| 55 * typing. Before typing, a list of search history is shown. |
| 56 */ |
| 57 public class EnhancedBookmarkSearchView extends LinearLayout implements View.OnC
lickListener, |
| 58 OnItemClickListener, FiltersObserver, EnhancedBookmarkUIObserver { |
| 59 |
| 60 private static enum UIState {HISTORY, RESULT, EMPTY} |
| 61 private static final int HISTORY_ITEM_PADDING_START_DP = 72; |
| 62 private static final int MAXIMUM_NUMBER_OF_RESULTS = 500; |
| 63 private static final String HORIZONTAL_ELLIPSIS = "\u2026"; |
| 64 private EnhancedBookmarksModel mEnhancedBookmarksModel; |
| 65 private EnhancedBookmarkDelegate mDelegate; |
| 66 private ImageButton mBackButton; |
| 67 private EditText mSearchText; |
| 68 private ListView mResultList; |
| 69 private ListView mHistoryList; |
| 70 private HistoryResultSwitcher mHistoryResultSwitcher; |
| 71 private UIState mCurrentUIState; |
| 72 |
| 73 private BookmarkModelObserver mModelObserver = new BookmarkModelObserver() { |
| 74 @Override |
| 75 public void bookmarkModelChanged() { |
| 76 if (mCurrentUIState == UIState.RESULT || mCurrentUIState == UIState.
EMPTY) { |
| 77 sendLocalSearchQuery(); |
| 78 } |
| 79 } |
| 80 |
| 81 @Override |
| 82 public void bookmarkNodeRemoved(BookmarkItem parent, int oldIndex, Bookm
arkItem node, |
| 83 boolean isDoingExtensiveChanges) { |
| 84 // If isDoingExtensiveChanges is false, it will fall back to bookmar
kModelChange() |
| 85 if (isDoingExtensiveChanges && mCurrentUIState == UIState.RESULT) { |
| 86 sendLocalSearchQuery(); |
| 87 } |
| 88 } |
| 89 }; |
| 90 |
| 91 /** |
| 92 * Constructor for inflating from XML. |
| 93 */ |
| 94 public EnhancedBookmarkSearchView(Context context, AttributeSet attrs) { |
| 95 super(context, attrs); |
| 96 } |
| 97 |
| 98 @Override |
| 99 protected void onFinishInflate() { |
| 100 super.onFinishInflate(); |
| 101 mBackButton = (ImageButton) findViewById(R.id.eb_search_back); |
| 102 mSearchText = (EditText) findViewById(R.id.eb_search_text); |
| 103 mResultList = (ListView) findViewById(R.id.eb_result_list); |
| 104 mHistoryList = (ListView) findViewById(R.id.eb_history_list); |
| 105 mHistoryResultSwitcher = (HistoryResultSwitcher) findViewById(R.id.histo
ry_result_switcher); |
| 106 |
| 107 mHistoryList.setOnItemClickListener(this); |
| 108 mResultList.setOnItemClickListener(this); |
| 109 mBackButton.setOnClickListener(this); |
| 110 mSearchText.addTextChangedListener(new TextWatcher() { |
| 111 @Override |
| 112 public void beforeTextChanged(CharSequence s, int start, int count,
int after) { |
| 113 } |
| 114 |
| 115 @Override |
| 116 public void onTextChanged(CharSequence s, int start, int before, int
count) { |
| 117 } |
| 118 |
| 119 @Override |
| 120 public void afterTextChanged(Editable s) { |
| 121 if (TextUtils.isEmpty(s.toString().trim())) { |
| 122 resetUI(); |
| 123 } else { |
| 124 sendLocalSearchQuery(); |
| 125 } |
| 126 } |
| 127 }); |
| 128 mCurrentUIState = UIState.HISTORY; |
| 129 } |
| 130 |
| 131 @Override |
| 132 public void onItemClick(AdapterView<?> parent, View view, int position, long
id) { |
| 133 if (parent == mHistoryList && view instanceof TextView) { |
| 134 List<BookmarkId> bookmarkIds = mEnhancedBookmarksModel |
| 135 .getBookmarksForFilter((String) parent.getAdapter().getItem(
position)); |
| 136 List<BookmarkMatch> bookmarkMatches = new ArrayList<BookmarkMatch>()
; |
| 137 for (BookmarkId bookmarkId : bookmarkIds) { |
| 138 bookmarkMatches.add(new BookmarkMatch(bookmarkId, null, null)); |
| 139 } |
| 140 populateResultListView(bookmarkMatches); |
| 141 } else if (parent == mResultList) { |
| 142 mDelegate.openBookmark( |
| 143 ((BookmarkMatch) parent.getAdapter().getItem(position)).getB
ookmarkId(), |
| 144 LaunchLocation.SEARCH); |
| 145 mDelegate.closeSearchUI(); |
| 146 } |
| 147 } |
| 148 |
| 149 private void resetUI() { |
| 150 setUIState(UIState.HISTORY); |
| 151 mResultList.setAdapter(null); |
| 152 if (!mSearchText.getText().toString().isEmpty()) mSearchText.setText("")
; |
| 153 } |
| 154 |
| 155 private void sendLocalSearchQuery() { |
| 156 String currentText = mSearchText.getText().toString().trim(); |
| 157 if (TextUtils.isEmpty(currentText)) return; |
| 158 |
| 159 List<BookmarkMatch> results = mEnhancedBookmarksModel.getLocalSearchForQ
uery(currentText, |
| 160 MAXIMUM_NUMBER_OF_RESULTS); |
| 161 if (results != null) populateResultListView(results); |
| 162 } |
| 163 |
| 164 /** |
| 165 * Make result list visible and popuplate the list with given list of bookma
rks. |
| 166 */ |
| 167 private void populateResultListView(List<BookmarkMatch> ids) { |
| 168 if (ids.isEmpty()) { |
| 169 setUIState(UIState.EMPTY); |
| 170 } else { |
| 171 setUIState(UIState.RESULT); |
| 172 mResultList.setAdapter(new ResultListAdapter(ids, mEnhancedBookmarks
Model)); |
| 173 } |
| 174 } |
| 175 |
| 176 @Override |
| 177 protected void onVisibilityChanged(View changedView, int visibility) { |
| 178 super.onVisibilityChanged(changedView, visibility); |
| 179 // This method might be called very early. Null check on bookmark model
here. |
| 180 if (mEnhancedBookmarksModel == null) return; |
| 181 InputMethodManager imm = (InputMethodManager) getContext().getSystemServ
ice( |
| 182 Context.INPUT_METHOD_SERVICE); |
| 183 if (visibility != View.VISIBLE) { |
| 184 imm.hideSoftInputFromWindow(mSearchText.getWindowToken(), 0); |
| 185 mEnhancedBookmarksModel.removeFiltersObserver(this); |
| 186 mEnhancedBookmarksModel.removeModelObserver(mModelObserver); |
| 187 resetUI(); |
| 188 clearFocus(); |
| 189 } else { |
| 190 mEnhancedBookmarksModel.addModelObserver(mModelObserver); |
| 191 mEnhancedBookmarksModel.addFiltersObserver(this); |
| 192 onFiltersChanged(); |
| 193 mSearchText.requestFocus(); |
| 194 imm.showSoftInput(mSearchText, 0); |
| 195 } |
| 196 } |
| 197 |
| 198 private void setUIState(UIState state) { |
| 199 if (mCurrentUIState == state) return; |
| 200 mCurrentUIState = state; |
| 201 if (state == UIState.HISTORY) { |
| 202 mHistoryResultSwitcher.showHistory(); |
| 203 } else if (state == UIState.RESULT) { |
| 204 mHistoryResultSwitcher.showResult(); |
| 205 } else if (state == UIState.EMPTY) { |
| 206 mHistoryResultSwitcher.showEmpty(); |
| 207 } |
| 208 } |
| 209 |
| 210 @Override |
| 211 public boolean dispatchKeyEvent(KeyEvent event) { |
| 212 // To intercept hardware key, a view must have focus. |
| 213 if (mDelegate == null) return super.dispatchKeyEvent(event); |
| 214 |
| 215 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { |
| 216 KeyEvent.DispatcherState state = getKeyDispatcherState(); |
| 217 if (state != null) { |
| 218 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeat
Count() == 0) { |
| 219 state.startTracking(event, this); |
| 220 return true; |
| 221 } else if (event.getAction() == KeyEvent.ACTION_UP && !event.isC
anceled() |
| 222 && state.isTracking(event)) { |
| 223 onClick(mBackButton); |
| 224 return true; |
| 225 } |
| 226 } |
| 227 } |
| 228 |
| 229 return super.dispatchKeyEvent(event); |
| 230 } |
| 231 |
| 232 /** |
| 233 * Click listener for back button. |
| 234 */ |
| 235 @Override |
| 236 public void onClick(View v) { |
| 237 assert v == mBackButton; |
| 238 if (mCurrentUIState == UIState.HISTORY) { |
| 239 mDelegate.closeSearchUI(); |
| 240 } else { |
| 241 resetUI(); |
| 242 } |
| 243 } |
| 244 |
| 245 @Override |
| 246 public void onFiltersChanged() { |
| 247 // We use android default list and textviews for history. Only start pad
ding is modified. |
| 248 mHistoryList.setAdapter(new ArrayAdapter<String>(getContext(), |
| 249 android.R.layout.simple_list_item_1, android.R.id.text1, |
| 250 mEnhancedBookmarksModel.getFilters()) { |
| 251 @Override |
| 252 public View getView(int position, View convertView, ViewGroup parent
) { |
| 253 View textView = super.getView(position, convertView, parent); |
| 254 // Set padding start to specific size. |
| 255 int paddingStart = (int) (HISTORY_ITEM_PADDING_START_DP |
| 256 * getResources().getDisplayMetrics().density); |
| 257 ApiCompatibilityUtils.setPaddingRelative(textView, paddingStart, |
| 258 textView.getPaddingTop(), textView.getPaddingRight(), |
| 259 textView.getPaddingBottom()); |
| 260 return textView; |
| 261 } |
| 262 }); |
| 263 } |
| 264 |
| 265 // EnhancedBookmarkUIObserver implementation |
| 266 |
| 267 @Override |
| 268 public void onEnhancedBookmarkDelegateInitialized(EnhancedBookmarkDelegate d
elegate) { |
| 269 mDelegate = delegate; |
| 270 mDelegate.addUIObserver(this); |
| 271 mEnhancedBookmarksModel = mDelegate.getModel(); |
| 272 } |
| 273 |
| 274 @Override |
| 275 public void onDestroy() { |
| 276 mEnhancedBookmarksModel.removeFiltersObserver(this); |
| 277 mEnhancedBookmarksModel.removeModelObserver(mModelObserver); |
| 278 mDelegate.removeUIObserver(this); |
| 279 } |
| 280 |
| 281 @Override |
| 282 public void onAllBookmarksStateSet() { |
| 283 } |
| 284 |
| 285 @Override |
| 286 public void onFolderStateSet(BookmarkId folder) { |
| 287 } |
| 288 |
| 289 @Override |
| 290 public void onFilterStateSet(String filter) { |
| 291 } |
| 292 |
| 293 @Override |
| 294 public void onSelectionStateChange(List<BookmarkId> selectedBookmarks) { |
| 295 } |
| 296 |
| 297 @Override |
| 298 public void onListModeChange(boolean isListModeEnabled) {} |
| 299 |
| 300 /** |
| 301 * List Adapter that organizes search results. |
| 302 */ |
| 303 private static class ResultListAdapter extends BaseAdapter implements |
| 304 SalientImageDrawableFactory { |
| 305 |
| 306 private EnhancedBookmarksModel mBookmarksModel; |
| 307 private List<BookmarkMatch> mResultList; |
| 308 |
| 309 public ResultListAdapter(List<BookmarkMatch> bookmarkMatches, |
| 310 EnhancedBookmarksModel enhancedBookmarksModel) { |
| 311 mBookmarksModel = enhancedBookmarksModel; |
| 312 mResultList = bookmarkMatches; |
| 313 } |
| 314 |
| 315 @Override |
| 316 public View getView(int position, View convertView, ViewGroup parent) { |
| 317 final BookmarkMatch bookmarkMatch = getItem(position); |
| 318 BookmarkItem bookmarkItem = mBookmarksModel.getBookmarkById( |
| 319 bookmarkMatch.getBookmarkId()); |
| 320 if (convertView == null) { |
| 321 convertView = LayoutInflater.from(parent.getContext()).inflate( |
| 322 R.layout.eb_search_result_item, parent, false); |
| 323 } |
| 324 final TextView titleTextView = (TextView) convertView.findViewById(R
.id.title); |
| 325 final TextView urlTextView = (TextView) convertView.findViewById(R.i
d.url); |
| 326 EnhancedBookmarkSalientImageView imageView = (EnhancedBookmarkSalien
tImageView) |
| 327 convertView.findViewById(R.id.round_image); |
| 328 imageView.load(mBookmarksModel, bookmarkItem.getUrl(), |
| 329 EnhancedBookmarkUtils.generateBackgroundColor(bookmarkItem),
this); |
| 330 |
| 331 final SpannableString title = boldMatchPositions(bookmarkItem.getTit
le(), |
| 332 bookmarkMatch.getTitleMatchPositions()); |
| 333 final SpannableString url = boldMatchPositions(bookmarkItem.getUrl()
, |
| 334 bookmarkMatch.getUrlMatchPositions()); |
| 335 |
| 336 titleTextView.setText(title); |
| 337 urlTextView.setText(url); |
| 338 |
| 339 // Ensure the first matched search term is visible on the screen by
calling |
| 340 // moveCharactersOnScreen after the titleTextView.onLayout() has bee
n executed |
| 341 if (bookmarkMatch.getTitleMatchPositions() != null |
| 342 && bookmarkMatch.getTitleMatchPositions().size() > 0) { |
| 343 titleTextView.addOnLayoutChangeListener(new OnLayoutChangeListen
er(){ |
| 344 @Override |
| 345 public void onLayoutChange(View v, int left, int top, int ri
ght, int bottom, |
| 346 int oldLeft, int oldTop, int oldRight, int oldBottom
) { |
| 347 moveCharactersOnScreen(titleTextView, title, |
| 348 bookmarkMatch.getTitleMatchPositions().get(0).fi
rst, |
| 349 bookmarkMatch.getTitleMatchPositions().get(0).se
cond); |
| 350 } |
| 351 }); |
| 352 } |
| 353 |
| 354 // Ensure the first matched search term is visible on the screen by
calling |
| 355 // moveCharactersOnScreen after the urlTextView.onLayout() has been
executed |
| 356 if (bookmarkMatch.getUrlMatchPositions() != null |
| 357 && bookmarkMatch.getUrlMatchPositions().size() > 0) { |
| 358 urlTextView.addOnLayoutChangeListener(new OnLayoutChangeListener
(){ |
| 359 @Override |
| 360 public void onLayoutChange(View v, int left, int top, int ri
ght, int bottom, |
| 361 int oldLeft, int oldTop, int oldRight, int oldBottom
) { |
| 362 moveCharactersOnScreen(urlTextView, url, |
| 363 bookmarkMatch.getUrlMatchPositions().get(0).firs
t, |
| 364 bookmarkMatch.getUrlMatchPositions().get(0).seco
nd); |
| 365 } |
| 366 }); |
| 367 } |
| 368 |
| 369 return convertView; |
| 370 } |
| 371 |
| 372 private SpannableString boldMatchPositions(String input, |
| 373 List<Pair<Integer, Integer>> matches) { |
| 374 SpannableString output = new SpannableString(input); |
| 375 if (matches != null) { |
| 376 for (Pair<Integer, Integer> match : matches) { |
| 377 StyleSpan boldSpan = new StyleSpan(Typeface.BOLD); |
| 378 output.setSpan(boldSpan, match.first, match.second, 0); |
| 379 } |
| 380 } |
| 381 |
| 382 return output; |
| 383 } |
| 384 |
| 385 /** |
| 386 * Ensures the characters from [charStartPos, charEndPos) are on screen. |
| 387 * |
| 388 * @param textView The textView to manipulate |
| 389 * @param viewText A SpannableString containing the textView's text |
| 390 * @param charStartPos The starting position for characters that should
be on screen |
| 391 * @param charEndPos The ending position for characters that should be o
n screen |
| 392 */ |
| 393 private void moveCharactersOnScreen(TextView textView, SpannableString v
iewText, |
| 394 int charStartPos, int charEndPos) { |
| 395 if (textView.getLayout() == null) return; |
| 396 |
| 397 if (!isCharacterOnScreen(textView.getLayout(), charEndPos - 1)) { |
| 398 int subStringLength = textView.getLayout().getEllipsisStart(0); |
| 399 int termLength = charEndPos - charStartPos; |
| 400 int subStringStartPos = charStartPos - ((subStringLength - termL
ength) / 2); |
| 401 |
| 402 if (subStringStartPos + subStringLength > viewText.length()) { |
| 403 // Characters are near the end of viewText, ellipsize the st
art |
| 404 textView.setEllipsize(TruncateAt.START); |
| 405 } else { |
| 406 // Characters are somwhere in the middle of the viewText, ch
ange the textView's |
| 407 // text so the characters are on screen; both sides of the t
ext will be |
| 408 // ellipsized |
| 409 SpannableStringBuilder newText = new SpannableStringBuilder(
viewText, |
| 410 subStringStartPos, viewText.length()); |
| 411 newText.insert(0, HORIZONTAL_ELLIPSIS); |
| 412 textView.setText(newText); |
| 413 } |
| 414 } |
| 415 } |
| 416 |
| 417 private boolean isCharacterOnScreen(Layout textViewLayout, int charPosit
ion) { |
| 418 int ellipsisPosition = textViewLayout.getEllipsisStart(0); |
| 419 return ellipsisPosition == 0 || charPosition < ellipsisPosition; |
| 420 } |
| 421 |
| 422 @Override |
| 423 public int getCount() { |
| 424 return mResultList.size(); |
| 425 } |
| 426 |
| 427 @Override |
| 428 public BookmarkMatch getItem(int position) { |
| 429 return mResultList.get(position); |
| 430 } |
| 431 |
| 432 @Override |
| 433 public long getItemId(int position) { |
| 434 return position; |
| 435 } |
| 436 |
| 437 // SalientImageDrawableFactory implementation |
| 438 |
| 439 @Override |
| 440 public Drawable getSalientDrawable(int color) { |
| 441 return new CircularDrawable(color); |
| 442 } |
| 443 |
| 444 @Override |
| 445 public Drawable getSalientDrawable(Bitmap bitmap) { |
| 446 return new CircularDrawable(bitmap); |
| 447 } |
| 448 } |
| 449 |
| 450 /** |
| 451 * A custom {@link ViewSwitcher} that makes specific assumptions about the c
orresponding xml. It |
| 452 * assumes it has two children: a {@link ListView} and a {@link ViewSwitcher
}, and in the other |
| 453 * {@link ViewSwitcher}, it contains a {@link TextView} and a {@link ListVie
w}. |
| 454 */ |
| 455 public static class HistoryResultSwitcher extends ViewSwitcher { |
| 456 ViewSwitcher mResultEmptySwitcher; |
| 457 |
| 458 /** |
| 459 * Constructor for xml inflation. |
| 460 */ |
| 461 public HistoryResultSwitcher(Context context, AttributeSet attrs) { |
| 462 super(context, attrs); |
| 463 } |
| 464 |
| 465 @Override |
| 466 protected void onFinishInflate() { |
| 467 super.onFinishInflate(); |
| 468 mResultEmptySwitcher = (ViewSwitcher) findViewById(R.id.result_empty
_switcher); |
| 469 } |
| 470 |
| 471 void showHistory() { |
| 472 if (getCurrentView().getId() == R.id.eb_history_list) return; |
| 473 showNext(); |
| 474 } |
| 475 |
| 476 void showResult() { |
| 477 // If currently showing history, toggle. |
| 478 if (getCurrentView().getId() == R.id.eb_history_list) showNext(); |
| 479 // If currently showing empty view, toggle. |
| 480 if (mResultEmptySwitcher.getCurrentView().getId() == R.id.eb_search_
empty_view) { |
| 481 mResultEmptySwitcher.showNext(); |
| 482 } |
| 483 } |
| 484 |
| 485 void showEmpty() { |
| 486 // If currently showing history, toggle. |
| 487 if (getCurrentView().getId() == R.id.eb_history_list) showNext(); |
| 488 // If currently showing result list, toggle. |
| 489 if (mResultEmptySwitcher.getCurrentView().getId() == R.id.eb_result_
list) { |
| 490 mResultEmptySwitcher.showNext(); |
| 491 } |
| 492 } |
| 493 } |
| 494 |
| 495 |
| 496 } |
OLD | NEW |