Index: chrome/android/java_staging/src/org/chromium/chrome/browser/enhancedbookmarks/EnhancedBookmarkSearchView.java |
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/enhancedbookmarks/EnhancedBookmarkSearchView.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/enhancedbookmarks/EnhancedBookmarkSearchView.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..a150f447b5a75b7ec31cd5da483ecc7c7a80f67b |
--- /dev/null |
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/enhancedbookmarks/EnhancedBookmarkSearchView.java |
@@ -0,0 +1,496 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.chrome.browser.enhancedbookmarks; |
+ |
+import android.content.Context; |
+import android.graphics.Bitmap; |
+import android.graphics.Typeface; |
+import android.graphics.drawable.Drawable; |
+import android.text.Editable; |
+import android.text.Layout; |
+import android.text.SpannableString; |
+import android.text.SpannableStringBuilder; |
+import android.text.TextUtils; |
+import android.text.TextUtils.TruncateAt; |
+import android.text.TextWatcher; |
+import android.text.style.StyleSpan; |
+import android.util.AttributeSet; |
+import android.util.Pair; |
+import android.view.KeyEvent; |
+import android.view.LayoutInflater; |
+import android.view.View; |
+import android.view.ViewGroup; |
+import android.view.inputmethod.InputMethodManager; |
+import android.widget.AdapterView; |
+import android.widget.AdapterView.OnItemClickListener; |
+import android.widget.ArrayAdapter; |
+import android.widget.BaseAdapter; |
+import android.widget.EditText; |
+import android.widget.ImageButton; |
+import android.widget.LinearLayout; |
+import android.widget.ListView; |
+import android.widget.TextView; |
+import android.widget.ViewSwitcher; |
+ |
+import com.google.android.apps.chrome.R; |
+ |
+import org.chromium.base.ApiCompatibilityUtils; |
+import org.chromium.chrome.browser.BookmarksBridge.BookmarkItem; |
+import org.chromium.chrome.browser.BookmarksBridge.BookmarkModelObserver; |
+import org.chromium.chrome.browser.enhanced_bookmarks.EnhancedBookmarksBridge.FiltersObserver; |
+import org.chromium.chrome.browser.enhanced_bookmarks.EnhancedBookmarksModel; |
+import org.chromium.chrome.browser.enhanced_bookmarks.LaunchLocation; |
+import org.chromium.chrome.browser.enhancedbookmarks.EnhancedBookmarkSalientImageView.SalientImageDrawableFactory; |
+import org.chromium.chrome.browser.widget.CustomShapeDrawable.CircularDrawable; |
+import org.chromium.components.bookmarks.BookmarkId; |
+import org.chromium.components.bookmarks.BookmarkMatch; |
+ |
+import java.util.ArrayList; |
+import java.util.List; |
+ |
+/** |
+ * UI implementation for search in enhanced bookmark. Search results will be updated when user is |
+ * typing. Before typing, a list of search history is shown. |
+ */ |
+public class EnhancedBookmarkSearchView extends LinearLayout implements View.OnClickListener, |
+ OnItemClickListener, FiltersObserver, EnhancedBookmarkUIObserver { |
+ |
+ private static enum UIState {HISTORY, RESULT, EMPTY} |
+ private static final int HISTORY_ITEM_PADDING_START_DP = 72; |
+ private static final int MAXIMUM_NUMBER_OF_RESULTS = 500; |
+ private static final String HORIZONTAL_ELLIPSIS = "\u2026"; |
+ private EnhancedBookmarksModel mEnhancedBookmarksModel; |
+ private EnhancedBookmarkDelegate mDelegate; |
+ private ImageButton mBackButton; |
+ private EditText mSearchText; |
+ private ListView mResultList; |
+ private ListView mHistoryList; |
+ private HistoryResultSwitcher mHistoryResultSwitcher; |
+ private UIState mCurrentUIState; |
+ |
+ private BookmarkModelObserver mModelObserver = new BookmarkModelObserver() { |
+ @Override |
+ public void bookmarkModelChanged() { |
+ if (mCurrentUIState == UIState.RESULT || mCurrentUIState == UIState.EMPTY) { |
+ sendLocalSearchQuery(); |
+ } |
+ } |
+ |
+ @Override |
+ public void bookmarkNodeRemoved(BookmarkItem parent, int oldIndex, BookmarkItem node, |
+ boolean isDoingExtensiveChanges) { |
+ // If isDoingExtensiveChanges is false, it will fall back to bookmarkModelChange() |
+ if (isDoingExtensiveChanges && mCurrentUIState == UIState.RESULT) { |
+ sendLocalSearchQuery(); |
+ } |
+ } |
+ }; |
+ |
+ /** |
+ * Constructor for inflating from XML. |
+ */ |
+ public EnhancedBookmarkSearchView(Context context, AttributeSet attrs) { |
+ super(context, attrs); |
+ } |
+ |
+ @Override |
+ protected void onFinishInflate() { |
+ super.onFinishInflate(); |
+ mBackButton = (ImageButton) findViewById(R.id.eb_search_back); |
+ mSearchText = (EditText) findViewById(R.id.eb_search_text); |
+ mResultList = (ListView) findViewById(R.id.eb_result_list); |
+ mHistoryList = (ListView) findViewById(R.id.eb_history_list); |
+ mHistoryResultSwitcher = (HistoryResultSwitcher) findViewById(R.id.history_result_switcher); |
+ |
+ mHistoryList.setOnItemClickListener(this); |
+ mResultList.setOnItemClickListener(this); |
+ mBackButton.setOnClickListener(this); |
+ mSearchText.addTextChangedListener(new TextWatcher() { |
+ @Override |
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) { |
+ } |
+ |
+ @Override |
+ public void onTextChanged(CharSequence s, int start, int before, int count) { |
+ } |
+ |
+ @Override |
+ public void afterTextChanged(Editable s) { |
+ if (TextUtils.isEmpty(s.toString().trim())) { |
+ resetUI(); |
+ } else { |
+ sendLocalSearchQuery(); |
+ } |
+ } |
+ }); |
+ mCurrentUIState = UIState.HISTORY; |
+ } |
+ |
+ @Override |
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |
+ if (parent == mHistoryList && view instanceof TextView) { |
+ List<BookmarkId> bookmarkIds = mEnhancedBookmarksModel |
+ .getBookmarksForFilter((String) parent.getAdapter().getItem(position)); |
+ List<BookmarkMatch> bookmarkMatches = new ArrayList<BookmarkMatch>(); |
+ for (BookmarkId bookmarkId : bookmarkIds) { |
+ bookmarkMatches.add(new BookmarkMatch(bookmarkId, null, null)); |
+ } |
+ populateResultListView(bookmarkMatches); |
+ } else if (parent == mResultList) { |
+ mDelegate.openBookmark( |
+ ((BookmarkMatch) parent.getAdapter().getItem(position)).getBookmarkId(), |
+ LaunchLocation.SEARCH); |
+ mDelegate.closeSearchUI(); |
+ } |
+ } |
+ |
+ private void resetUI() { |
+ setUIState(UIState.HISTORY); |
+ mResultList.setAdapter(null); |
+ if (!mSearchText.getText().toString().isEmpty()) mSearchText.setText(""); |
+ } |
+ |
+ private void sendLocalSearchQuery() { |
+ String currentText = mSearchText.getText().toString().trim(); |
+ if (TextUtils.isEmpty(currentText)) return; |
+ |
+ List<BookmarkMatch> results = mEnhancedBookmarksModel.getLocalSearchForQuery(currentText, |
+ MAXIMUM_NUMBER_OF_RESULTS); |
+ if (results != null) populateResultListView(results); |
+ } |
+ |
+ /** |
+ * Make result list visible and popuplate the list with given list of bookmarks. |
+ */ |
+ private void populateResultListView(List<BookmarkMatch> ids) { |
+ if (ids.isEmpty()) { |
+ setUIState(UIState.EMPTY); |
+ } else { |
+ setUIState(UIState.RESULT); |
+ mResultList.setAdapter(new ResultListAdapter(ids, mEnhancedBookmarksModel)); |
+ } |
+ } |
+ |
+ @Override |
+ protected void onVisibilityChanged(View changedView, int visibility) { |
+ super.onVisibilityChanged(changedView, visibility); |
+ // This method might be called very early. Null check on bookmark model here. |
+ if (mEnhancedBookmarksModel == null) return; |
+ InputMethodManager imm = (InputMethodManager) getContext().getSystemService( |
+ Context.INPUT_METHOD_SERVICE); |
+ if (visibility != View.VISIBLE) { |
+ imm.hideSoftInputFromWindow(mSearchText.getWindowToken(), 0); |
+ mEnhancedBookmarksModel.removeFiltersObserver(this); |
+ mEnhancedBookmarksModel.removeModelObserver(mModelObserver); |
+ resetUI(); |
+ clearFocus(); |
+ } else { |
+ mEnhancedBookmarksModel.addModelObserver(mModelObserver); |
+ mEnhancedBookmarksModel.addFiltersObserver(this); |
+ onFiltersChanged(); |
+ mSearchText.requestFocus(); |
+ imm.showSoftInput(mSearchText, 0); |
+ } |
+ } |
+ |
+ private void setUIState(UIState state) { |
+ if (mCurrentUIState == state) return; |
+ mCurrentUIState = state; |
+ if (state == UIState.HISTORY) { |
+ mHistoryResultSwitcher.showHistory(); |
+ } else if (state == UIState.RESULT) { |
+ mHistoryResultSwitcher.showResult(); |
+ } else if (state == UIState.EMPTY) { |
+ mHistoryResultSwitcher.showEmpty(); |
+ } |
+ } |
+ |
+ @Override |
+ public boolean dispatchKeyEvent(KeyEvent event) { |
+ // To intercept hardware key, a view must have focus. |
+ if (mDelegate == null) return super.dispatchKeyEvent(event); |
+ |
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { |
+ KeyEvent.DispatcherState state = getKeyDispatcherState(); |
+ if (state != null) { |
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { |
+ state.startTracking(event, this); |
+ return true; |
+ } else if (event.getAction() == KeyEvent.ACTION_UP && !event.isCanceled() |
+ && state.isTracking(event)) { |
+ onClick(mBackButton); |
+ return true; |
+ } |
+ } |
+ } |
+ |
+ return super.dispatchKeyEvent(event); |
+ } |
+ |
+ /** |
+ * Click listener for back button. |
+ */ |
+ @Override |
+ public void onClick(View v) { |
+ assert v == mBackButton; |
+ if (mCurrentUIState == UIState.HISTORY) { |
+ mDelegate.closeSearchUI(); |
+ } else { |
+ resetUI(); |
+ } |
+ } |
+ |
+ @Override |
+ public void onFiltersChanged() { |
+ // We use android default list and textviews for history. Only start padding is modified. |
+ mHistoryList.setAdapter(new ArrayAdapter<String>(getContext(), |
+ android.R.layout.simple_list_item_1, android.R.id.text1, |
+ mEnhancedBookmarksModel.getFilters()) { |
+ @Override |
+ public View getView(int position, View convertView, ViewGroup parent) { |
+ View textView = super.getView(position, convertView, parent); |
+ // Set padding start to specific size. |
+ int paddingStart = (int) (HISTORY_ITEM_PADDING_START_DP |
+ * getResources().getDisplayMetrics().density); |
+ ApiCompatibilityUtils.setPaddingRelative(textView, paddingStart, |
+ textView.getPaddingTop(), textView.getPaddingRight(), |
+ textView.getPaddingBottom()); |
+ return textView; |
+ } |
+ }); |
+ } |
+ |
+ // EnhancedBookmarkUIObserver implementation |
+ |
+ @Override |
+ public void onEnhancedBookmarkDelegateInitialized(EnhancedBookmarkDelegate delegate) { |
+ mDelegate = delegate; |
+ mDelegate.addUIObserver(this); |
+ mEnhancedBookmarksModel = mDelegate.getModel(); |
+ } |
+ |
+ @Override |
+ public void onDestroy() { |
+ mEnhancedBookmarksModel.removeFiltersObserver(this); |
+ mEnhancedBookmarksModel.removeModelObserver(mModelObserver); |
+ mDelegate.removeUIObserver(this); |
+ } |
+ |
+ @Override |
+ public void onAllBookmarksStateSet() { |
+ } |
+ |
+ @Override |
+ public void onFolderStateSet(BookmarkId folder) { |
+ } |
+ |
+ @Override |
+ public void onFilterStateSet(String filter) { |
+ } |
+ |
+ @Override |
+ public void onSelectionStateChange(List<BookmarkId> selectedBookmarks) { |
+ } |
+ |
+ @Override |
+ public void onListModeChange(boolean isListModeEnabled) {} |
+ |
+ /** |
+ * List Adapter that organizes search results. |
+ */ |
+ private static class ResultListAdapter extends BaseAdapter implements |
+ SalientImageDrawableFactory { |
+ |
+ private EnhancedBookmarksModel mBookmarksModel; |
+ private List<BookmarkMatch> mResultList; |
+ |
+ public ResultListAdapter(List<BookmarkMatch> bookmarkMatches, |
+ EnhancedBookmarksModel enhancedBookmarksModel) { |
+ mBookmarksModel = enhancedBookmarksModel; |
+ mResultList = bookmarkMatches; |
+ } |
+ |
+ @Override |
+ public View getView(int position, View convertView, ViewGroup parent) { |
+ final BookmarkMatch bookmarkMatch = getItem(position); |
+ BookmarkItem bookmarkItem = mBookmarksModel.getBookmarkById( |
+ bookmarkMatch.getBookmarkId()); |
+ if (convertView == null) { |
+ convertView = LayoutInflater.from(parent.getContext()).inflate( |
+ R.layout.eb_search_result_item, parent, false); |
+ } |
+ final TextView titleTextView = (TextView) convertView.findViewById(R.id.title); |
+ final TextView urlTextView = (TextView) convertView.findViewById(R.id.url); |
+ EnhancedBookmarkSalientImageView imageView = (EnhancedBookmarkSalientImageView) |
+ convertView.findViewById(R.id.round_image); |
+ imageView.load(mBookmarksModel, bookmarkItem.getUrl(), |
+ EnhancedBookmarkUtils.generateBackgroundColor(bookmarkItem), this); |
+ |
+ final SpannableString title = boldMatchPositions(bookmarkItem.getTitle(), |
+ bookmarkMatch.getTitleMatchPositions()); |
+ final SpannableString url = boldMatchPositions(bookmarkItem.getUrl(), |
+ bookmarkMatch.getUrlMatchPositions()); |
+ |
+ titleTextView.setText(title); |
+ urlTextView.setText(url); |
+ |
+ // Ensure the first matched search term is visible on the screen by calling |
+ // moveCharactersOnScreen after the titleTextView.onLayout() has been executed |
+ if (bookmarkMatch.getTitleMatchPositions() != null |
+ && bookmarkMatch.getTitleMatchPositions().size() > 0) { |
+ titleTextView.addOnLayoutChangeListener(new OnLayoutChangeListener(){ |
+ @Override |
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, |
+ int oldLeft, int oldTop, int oldRight, int oldBottom) { |
+ moveCharactersOnScreen(titleTextView, title, |
+ bookmarkMatch.getTitleMatchPositions().get(0).first, |
+ bookmarkMatch.getTitleMatchPositions().get(0).second); |
+ } |
+ }); |
+ } |
+ |
+ // Ensure the first matched search term is visible on the screen by calling |
+ // moveCharactersOnScreen after the urlTextView.onLayout() has been executed |
+ if (bookmarkMatch.getUrlMatchPositions() != null |
+ && bookmarkMatch.getUrlMatchPositions().size() > 0) { |
+ urlTextView.addOnLayoutChangeListener(new OnLayoutChangeListener(){ |
+ @Override |
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, |
+ int oldLeft, int oldTop, int oldRight, int oldBottom) { |
+ moveCharactersOnScreen(urlTextView, url, |
+ bookmarkMatch.getUrlMatchPositions().get(0).first, |
+ bookmarkMatch.getUrlMatchPositions().get(0).second); |
+ } |
+ }); |
+ } |
+ |
+ return convertView; |
+ } |
+ |
+ private SpannableString boldMatchPositions(String input, |
+ List<Pair<Integer, Integer>> matches) { |
+ SpannableString output = new SpannableString(input); |
+ if (matches != null) { |
+ for (Pair<Integer, Integer> match : matches) { |
+ StyleSpan boldSpan = new StyleSpan(Typeface.BOLD); |
+ output.setSpan(boldSpan, match.first, match.second, 0); |
+ } |
+ } |
+ |
+ return output; |
+ } |
+ |
+ /** |
+ * Ensures the characters from [charStartPos, charEndPos) are on screen. |
+ * |
+ * @param textView The textView to manipulate |
+ * @param viewText A SpannableString containing the textView's text |
+ * @param charStartPos The starting position for characters that should be on screen |
+ * @param charEndPos The ending position for characters that should be on screen |
+ */ |
+ private void moveCharactersOnScreen(TextView textView, SpannableString viewText, |
+ int charStartPos, int charEndPos) { |
+ if (textView.getLayout() == null) return; |
+ |
+ if (!isCharacterOnScreen(textView.getLayout(), charEndPos - 1)) { |
+ int subStringLength = textView.getLayout().getEllipsisStart(0); |
+ int termLength = charEndPos - charStartPos; |
+ int subStringStartPos = charStartPos - ((subStringLength - termLength) / 2); |
+ |
+ if (subStringStartPos + subStringLength > viewText.length()) { |
+ // Characters are near the end of viewText, ellipsize the start |
+ textView.setEllipsize(TruncateAt.START); |
+ } else { |
+ // Characters are somwhere in the middle of the viewText, change the textView's |
+ // text so the characters are on screen; both sides of the text will be |
+ // ellipsized |
+ SpannableStringBuilder newText = new SpannableStringBuilder(viewText, |
+ subStringStartPos, viewText.length()); |
+ newText.insert(0, HORIZONTAL_ELLIPSIS); |
+ textView.setText(newText); |
+ } |
+ } |
+ } |
+ |
+ private boolean isCharacterOnScreen(Layout textViewLayout, int charPosition) { |
+ int ellipsisPosition = textViewLayout.getEllipsisStart(0); |
+ return ellipsisPosition == 0 || charPosition < ellipsisPosition; |
+ } |
+ |
+ @Override |
+ public int getCount() { |
+ return mResultList.size(); |
+ } |
+ |
+ @Override |
+ public BookmarkMatch getItem(int position) { |
+ return mResultList.get(position); |
+ } |
+ |
+ @Override |
+ public long getItemId(int position) { |
+ return position; |
+ } |
+ |
+ // SalientImageDrawableFactory implementation |
+ |
+ @Override |
+ public Drawable getSalientDrawable(int color) { |
+ return new CircularDrawable(color); |
+ } |
+ |
+ @Override |
+ public Drawable getSalientDrawable(Bitmap bitmap) { |
+ return new CircularDrawable(bitmap); |
+ } |
+ } |
+ |
+ /** |
+ * A custom {@link ViewSwitcher} that makes specific assumptions about the corresponding xml. It |
+ * assumes it has two children: a {@link ListView} and a {@link ViewSwitcher}, and in the other |
+ * {@link ViewSwitcher}, it contains a {@link TextView} and a {@link ListView}. |
+ */ |
+ public static class HistoryResultSwitcher extends ViewSwitcher { |
+ ViewSwitcher mResultEmptySwitcher; |
+ |
+ /** |
+ * Constructor for xml inflation. |
+ */ |
+ public HistoryResultSwitcher(Context context, AttributeSet attrs) { |
+ super(context, attrs); |
+ } |
+ |
+ @Override |
+ protected void onFinishInflate() { |
+ super.onFinishInflate(); |
+ mResultEmptySwitcher = (ViewSwitcher) findViewById(R.id.result_empty_switcher); |
+ } |
+ |
+ void showHistory() { |
+ if (getCurrentView().getId() == R.id.eb_history_list) return; |
+ showNext(); |
+ } |
+ |
+ void showResult() { |
+ // If currently showing history, toggle. |
+ if (getCurrentView().getId() == R.id.eb_history_list) showNext(); |
+ // If currently showing empty view, toggle. |
+ if (mResultEmptySwitcher.getCurrentView().getId() == R.id.eb_search_empty_view) { |
+ mResultEmptySwitcher.showNext(); |
+ } |
+ } |
+ |
+ void showEmpty() { |
+ // If currently showing history, toggle. |
+ if (getCurrentView().getId() == R.id.eb_history_list) showNext(); |
+ // If currently showing result list, toggle. |
+ if (mResultEmptySwitcher.getCurrentView().getId() == R.id.eb_result_list) { |
+ mResultEmptySwitcher.showNext(); |
+ } |
+ } |
+ } |
+ |
+ |
+} |