| Index: chrome/android/java_staging/src/org/chromium/chrome/browser/omnibox/SuggestionView.java
|
| diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/omnibox/SuggestionView.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/omnibox/SuggestionView.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..12cb7c6ed11ca5cddac3c595bcbdc15a4de762e6
|
| --- /dev/null
|
| +++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/omnibox/SuggestionView.java
|
| @@ -0,0 +1,995 @@
|
| +// 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.omnibox;
|
| +
|
| +import static org.chromium.chrome.browser.omnibox.OmniboxSuggestion.Type.HISTORY_URL;
|
| +
|
| +import android.annotation.SuppressLint;
|
| +import android.content.Context;
|
| +import android.content.DialogInterface;
|
| +import android.content.res.TypedArray;
|
| +import android.graphics.Bitmap;
|
| +import android.graphics.Canvas;
|
| +import android.graphics.Color;
|
| +import android.graphics.PorterDuff;
|
| +import android.graphics.drawable.Drawable;
|
| +import android.support.v7.app.AlertDialog;
|
| +import android.text.Spannable;
|
| +import android.text.SpannableString;
|
| +import android.text.TextPaint;
|
| +import android.text.TextUtils;
|
| +import android.text.style.StyleSpan;
|
| +import android.view.MotionEvent;
|
| +import android.view.View;
|
| +import android.view.ViewGroup;
|
| +import android.widget.ImageView;
|
| +import android.widget.TextView;
|
| +import android.widget.TextView.BufferType;
|
| +
|
| +import com.google.android.apps.chrome.R;
|
| +
|
| +import org.chromium.base.ApiCompatibilityUtils;
|
| +import org.chromium.base.metrics.RecordUserAction;
|
| +import org.chromium.chrome.browser.omnibox.OmniboxResultsAdapter.OmniboxResultItem;
|
| +import org.chromium.chrome.browser.omnibox.OmniboxResultsAdapter.OmniboxSuggestionDelegate;
|
| +import org.chromium.chrome.browser.omnibox.OmniboxSuggestion.Type;
|
| +import org.chromium.chrome.browser.widget.TintedDrawable;
|
| +import org.chromium.ui.base.DeviceFormFactor;
|
| +
|
| +import java.util.Locale;
|
| +
|
| +/**
|
| + * Container view for omnibox suggestions made very specific for omnibox suggestions to minimize
|
| + * any unnecessary measures and layouts.
|
| + */
|
| +class SuggestionView extends ViewGroup {
|
| + private enum SuggestionIconType {
|
| + BOOKMARK,
|
| + HISTORY,
|
| + GLOBE,
|
| + MAGNIFIER,
|
| + VOICE
|
| + }
|
| +
|
| + private static final int FIRST_LINE_TEXT_SIZE_SP = 17;
|
| + private static final int SECOND_LINE_TEXT_SIZE_SP = 14;
|
| +
|
| + private static final long RELAYOUT_DELAY_MS = 20;
|
| +
|
| + private static final int TITLE_COLOR_STANDARD_FONT_DARK = Color.rgb(51, 51, 51);
|
| + private static final int TITLE_COLOR_STANDARD_FONT_LIGHT = Color.rgb(255, 255, 255);
|
| + private static final int URL_COLOR = Color.rgb(85, 149, 254);
|
| +
|
| + private static final int ANSWER_IMAGE_HORIZONTAL_SPACING_DP = 4;
|
| + private static final int ANSWER_IMAGE_VERTICAL_SPACING_DP = 5;
|
| + private static final float ANSWER_IMAGE_SCALING_FACTOR = 1.15f;
|
| +
|
| + private LocationBar mLocationBar;
|
| + private UrlBar mUrlBar;
|
| + private ImageView mNavigationButton;
|
| +
|
| + private int mSuggestionHeight;
|
| + private int mSuggestionAnswerHeight;
|
| +
|
| + private OmniboxResultItem mSuggestionItem;
|
| + private OmniboxSuggestion mSuggestion;
|
| + private OmniboxSuggestionDelegate mSuggestionDelegate;
|
| + private Boolean mUseDarkColors;
|
| + private int mPosition;
|
| +
|
| + private SuggestionContentsContainer mContentsView;
|
| +
|
| + private int mRefineWidth;
|
| + private View mRefineView;
|
| + private TintedDrawable mRefineIcon;
|
| +
|
| + private final int[] mViewPositionHolder = new int[2];
|
| +
|
| + /**
|
| + * Constructs a new omnibox suggestion view.
|
| + *
|
| + * @param context The context used to construct the suggestion view.
|
| + * @param locationBar The location bar showing these suggestions.
|
| + */
|
| + public SuggestionView(Context context, LocationBar locationBar) {
|
| + super(context);
|
| + mLocationBar = locationBar;
|
| +
|
| + mSuggestionHeight =
|
| + context.getResources().getDimensionPixelOffset(R.dimen.omnibox_suggestion_height);
|
| + mSuggestionAnswerHeight =
|
| + context.getResources().getDimensionPixelOffset(
|
| + R.dimen.omnibox_suggestion_answer_height);
|
| +
|
| + TypedArray a = getContext().obtainStyledAttributes(
|
| + new int [] {R.attr.selectableItemBackground});
|
| + Drawable itemBackground = a.getDrawable(0);
|
| + a.recycle();
|
| +
|
| + mContentsView = new SuggestionContentsContainer(context, itemBackground);
|
| + addView(mContentsView);
|
| +
|
| + mRefineView = new View(context) {
|
| + @Override
|
| + protected void onDraw(Canvas canvas) {
|
| + super.onDraw(canvas);
|
| +
|
| + if (mRefineIcon == null) return;
|
| + canvas.save();
|
| + canvas.translate(
|
| + (getMeasuredWidth() - mRefineIcon.getIntrinsicWidth()) / 2f,
|
| + (getMeasuredHeight() - mRefineIcon.getIntrinsicHeight()) / 2f);
|
| + mRefineIcon.draw(canvas);
|
| + canvas.restore();
|
| + }
|
| +
|
| + @Override
|
| + public void setVisibility(int visibility) {
|
| + super.setVisibility(visibility);
|
| +
|
| + if (visibility == VISIBLE) {
|
| + setClickable(true);
|
| + setFocusable(true);
|
| + } else {
|
| + setClickable(false);
|
| + setFocusable(false);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + protected void drawableStateChanged() {
|
| + super.drawableStateChanged();
|
| +
|
| + if (mRefineIcon != null && mRefineIcon.isStateful()) {
|
| + mRefineIcon.setState(getDrawableState());
|
| + }
|
| + }
|
| + };
|
| + mRefineView.setContentDescription(getContext().getString(
|
| + R.string.accessibility_omnibox_btn_refine));
|
| +
|
| + // Although this has the same background as the suggestion view, it can not be shared as
|
| + // it will result in the state of the drawable being shared and always showing up in the
|
| + // refine view.
|
| + mRefineView.setBackground(itemBackground.getConstantState().newDrawable());
|
| + mRefineView.setId(R.id.refine_view_id);
|
| + mRefineView.setClickable(true);
|
| + mRefineView.setFocusable(true);
|
| + mRefineView.setLayoutParams(new LayoutParams(0, 0));
|
| + addView(mRefineView);
|
| +
|
| + mRefineWidth = (int) (getResources().getDisplayMetrics().density * 48);
|
| +
|
| + mUrlBar = (UrlBar) locationBar.getContainerView().findViewById(R.id.url_bar);
|
| + }
|
| +
|
| + @Override
|
| + protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
| + if (getMeasuredWidth() == 0) return;
|
| +
|
| + if (mSuggestion.getType() != Type.SEARCH_SUGGEST_TAIL) {
|
| + mContentsView.resetTextWidths();
|
| + }
|
| +
|
| + boolean refineVisible = mRefineView.getVisibility() == VISIBLE;
|
| + boolean isRtl = ApiCompatibilityUtils.isLayoutRtl(this);
|
| + int contentsViewOffsetX = isRtl ? mRefineWidth : 0;
|
| + if (!refineVisible) contentsViewOffsetX = 0;
|
| + mContentsView.layout(
|
| + contentsViewOffsetX,
|
| + 0,
|
| + contentsViewOffsetX + mContentsView.getMeasuredWidth(),
|
| + mContentsView.getMeasuredHeight());
|
| + int refineViewOffsetX = isRtl ? 0 : getMeasuredWidth() - mRefineWidth;
|
| + mRefineView.layout(
|
| + refineViewOffsetX,
|
| + 0,
|
| + refineViewOffsetX + mRefineWidth,
|
| + mContentsView.getMeasuredHeight());
|
| + }
|
| +
|
| + @Override
|
| + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
| + int width = MeasureSpec.getSize(widthMeasureSpec);
|
| + int height = mSuggestionHeight;
|
| + if (!TextUtils.isEmpty(mSuggestion.getAnswerContents())) {
|
| + height = mSuggestionAnswerHeight;
|
| + }
|
| + setMeasuredDimension(width, height);
|
| +
|
| + // The width will be specified as 0 when determining the height of the popup, so exit early
|
| + // after setting the height.
|
| + if (width == 0) return;
|
| +
|
| + boolean refineVisible = mRefineView.getVisibility() == VISIBLE;
|
| + int refineWidth = refineVisible ? mRefineWidth : 0;
|
| + mContentsView.measure(
|
| + MeasureSpec.makeMeasureSpec(width - refineWidth, MeasureSpec.EXACTLY),
|
| + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
|
| + mContentsView.getLayoutParams().width = mContentsView.getMeasuredWidth();
|
| + mContentsView.getLayoutParams().height = mContentsView.getMeasuredHeight();
|
| +
|
| + mRefineView.measure(
|
| + MeasureSpec.makeMeasureSpec(mRefineWidth, MeasureSpec.EXACTLY),
|
| + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
|
| + mRefineView.getLayoutParams().width = mRefineView.getMeasuredWidth();
|
| + mRefineView.getLayoutParams().height = mRefineView.getMeasuredHeight();
|
| + }
|
| +
|
| + @Override
|
| + public void invalidate() {
|
| + super.invalidate();
|
| + mContentsView.invalidate();
|
| + }
|
| +
|
| + @Override
|
| + public boolean dispatchTouchEvent(MotionEvent ev) {
|
| + // Whenever the suggestion dropdown is touched, we dispatch onGestureDown which is
|
| + // used to let autocomplete controller know that it should stop updating suggestions.
|
| + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) mSuggestionDelegate.onGestureDown();
|
| + return super.dispatchTouchEvent(ev);
|
| + }
|
| +
|
| + /**
|
| + * Sets the contents and state of the view for the given suggestion.
|
| + *
|
| + * @param suggestionItem The omnibox suggestion item this view represents.
|
| + * @param suggestionDelegate The suggestion delegate.
|
| + * @param position Position of the suggestion in the dropdown list.
|
| + * @param useDarkColors Whether dark colors should be used for fonts and icons.
|
| + */
|
| + public void init(OmniboxResultItem suggestionItem,
|
| + OmniboxSuggestionDelegate suggestionDelegate,
|
| + int position, boolean useDarkColors) {
|
| + // Update the position unconditionally.
|
| + mPosition = position;
|
| + jumpDrawablesToCurrentState();
|
| + boolean colorsChanged = mUseDarkColors == null || mUseDarkColors != useDarkColors;
|
| + if (suggestionItem.equals(mSuggestionItem) && !colorsChanged) return;
|
| + mUseDarkColors = useDarkColors;
|
| + if (colorsChanged) {
|
| + mContentsView.mTextLine1.setTextColor(getStandardFontColor());
|
| + setRefineIcon(true);
|
| + }
|
| +
|
| + mSuggestionItem = suggestionItem;
|
| + mSuggestion = suggestionItem.getSuggestion();
|
| + mSuggestionDelegate = suggestionDelegate;
|
| + // Reset old computations.
|
| + mContentsView.resetTextWidths();
|
| + mContentsView.mAnswerImage.setVisibility(GONE);
|
| + mContentsView.mAnswerImage.getLayoutParams().height = 0;
|
| + mContentsView.mAnswerImage.getLayoutParams().width = 0;
|
| + mContentsView.mAnswerImage.setImageDrawable(null);
|
| + mContentsView.mAnswerImageMaxSize = 0;
|
| + mContentsView.mTextLine1.setTextSize(FIRST_LINE_TEXT_SIZE_SP);
|
| + mContentsView.mTextLine2.setTextSize(SECOND_LINE_TEXT_SIZE_SP);
|
| +
|
| + // Suggestions with attached answers are rendered with rich results regardless of which
|
| + // suggestion type they are.
|
| + if (mSuggestion.hasAnswer()) {
|
| + setAnswer(mSuggestion.getAnswer());
|
| + mContentsView.setSuggestionIcon(SuggestionIconType.MAGNIFIER, colorsChanged);
|
| + mContentsView.mTextLine2.setVisibility(VISIBLE);
|
| + setRefinable(true);
|
| + return;
|
| + }
|
| +
|
| + boolean sameAsTyped =
|
| + suggestionItem.getMatchedQuery().equalsIgnoreCase(mSuggestion.getDisplayText());
|
| + Type suggestionType = mSuggestion.getType();
|
| + switch (suggestionType) {
|
| + case HISTORY_URL:
|
| + case URL_WHAT_YOU_TYPED:
|
| + case NAVSUGGEST:
|
| + case HISTORY_TITLE:
|
| + case HISTORY_BODY:
|
| + case HISTORY_KEYWORD:
|
| + case OPEN_HISTORY_PAGE:
|
| + if (mSuggestion.isStarred()) {
|
| + mContentsView.setSuggestionIcon(SuggestionIconType.BOOKMARK, colorsChanged);
|
| + } else if (suggestionType == HISTORY_URL) {
|
| + mContentsView.setSuggestionIcon(SuggestionIconType.HISTORY, colorsChanged);
|
| + } else {
|
| + mContentsView.setSuggestionIcon(SuggestionIconType.GLOBE, colorsChanged);
|
| + }
|
| + boolean urlShown = !TextUtils.isEmpty(mSuggestion.getUrl());
|
| + boolean urlHighlighted = false;
|
| + if (urlShown) {
|
| + urlHighlighted = setUrlText(suggestionItem);
|
| + } else {
|
| + mContentsView.mTextLine2.setVisibility(INVISIBLE);
|
| + }
|
| + setSuggestedQuery(suggestionItem, true, urlShown, urlHighlighted);
|
| + setRefinable(!sameAsTyped);
|
| + break;
|
| + case SEARCH_WHAT_YOU_TYPED:
|
| + case SEARCH_HISTORY:
|
| + case SEARCH_SUGGEST:
|
| + case SEARCH_OTHER_ENGINE:
|
| + case SEARCH_SUGGEST_ENTITY:
|
| + case SEARCH_SUGGEST_TAIL:
|
| + case SEARCH_SUGGEST_PERSONALIZED:
|
| + case SEARCH_SUGGEST_PROFILE:
|
| + case VOICE_SUGGEST:
|
| + SuggestionIconType suggestionIcon = SuggestionIconType.MAGNIFIER;
|
| + if (suggestionType == Type.VOICE_SUGGEST) {
|
| + suggestionIcon = SuggestionIconType.VOICE;
|
| + } else if ((suggestionType == Type.SEARCH_SUGGEST_PERSONALIZED)
|
| + || (suggestionType == Type.SEARCH_HISTORY)) {
|
| + // Show history icon for suggestions based on user queries.
|
| + suggestionIcon = SuggestionIconType.HISTORY;
|
| + }
|
| + mContentsView.setSuggestionIcon(suggestionIcon, colorsChanged);
|
| + setRefinable(!sameAsTyped);
|
| + setSuggestedQuery(suggestionItem, false, false, false);
|
| + if ((suggestionType == Type.SEARCH_SUGGEST_ENTITY)
|
| + || (suggestionType == Type.SEARCH_SUGGEST_PROFILE)) {
|
| + showDescriptionLine(
|
| + SpannableString.valueOf(mSuggestion.getDescription()),
|
| + getStandardFontColor());
|
| + } else {
|
| + mContentsView.mTextLine2.setVisibility(INVISIBLE);
|
| + }
|
| + break;
|
| + default:
|
| + assert false : "Suggestion type (" + mSuggestion.getType() + ") is not handled";
|
| + break;
|
| + }
|
| + }
|
| +
|
| + private void setRefinable(boolean refinable) {
|
| + if (refinable) {
|
| + mRefineView.setVisibility(VISIBLE);
|
| + mRefineView.setOnClickListener(new OnClickListener() {
|
| + @Override
|
| + public void onClick(View v) {
|
| + // Post the refine action to the end of the UI thread to allow the refine view
|
| + // a chance to update its background selection state.
|
| + PerformRefineSuggestion performRefine = new PerformRefineSuggestion();
|
| + if (!post(performRefine)) performRefine.run();
|
| + }
|
| + });
|
| + } else {
|
| + mRefineView.setOnClickListener(null);
|
| + mRefineView.setVisibility(GONE);
|
| + }
|
| + }
|
| +
|
| + private int getStandardFontColor() {
|
| + return (mUseDarkColors == null || mUseDarkColors)
|
| + ? TITLE_COLOR_STANDARD_FONT_DARK : TITLE_COLOR_STANDARD_FONT_LIGHT;
|
| + }
|
| +
|
| + @Override
|
| + public void setSelected(boolean selected) {
|
| + super.setSelected(selected);
|
| + if (selected && !isInTouchMode()) {
|
| + mSuggestionDelegate.onSetUrlToSuggestion(mSuggestion);
|
| + }
|
| + }
|
| +
|
| + private void setRefineIcon(boolean invalidateIcon) {
|
| + if (!invalidateIcon && mRefineIcon != null) return;
|
| +
|
| + mRefineIcon = TintedDrawable.constructTintedDrawable(
|
| + getResources(), R.drawable.btn_suggestion_refine);
|
| + mRefineIcon.setTint(getResources().getColorStateList(mUseDarkColors
|
| + ? R.color.dark_mode_tint
|
| + : R.color.light_mode_tint));
|
| + mRefineIcon.setBounds(
|
| + 0, 0,
|
| + mRefineIcon.getIntrinsicWidth(),
|
| + mRefineIcon.getIntrinsicHeight());
|
| + mRefineIcon.setState(mRefineView.getDrawableState());
|
| + mRefineView.postInvalidateOnAnimation();
|
| + }
|
| +
|
| + /**
|
| + * Sets (and highlights) the URL text of the second line of the omnibox suggestion.
|
| + *
|
| + * @param suggestion The suggestion containing the URL.
|
| + * @return Whether the URL was highlighted based on the user query.
|
| + */
|
| + private boolean setUrlText(OmniboxResultItem suggestion) {
|
| + String query = suggestion.getMatchedQuery();
|
| + String url = suggestion.getSuggestion().getFormattedUrl();
|
| + int index = url.indexOf(query);
|
| + Spannable str = SpannableString.valueOf(url);
|
| + if (index >= 0) {
|
| + // Bold the part of the URL that matches the user query.
|
| + str.setSpan(new StyleSpan(android.graphics.Typeface.BOLD),
|
| + index, index + query.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
| + }
|
| + showDescriptionLine(str, URL_COLOR);
|
| + return index >= 0;
|
| + }
|
| +
|
| + /**
|
| + * Sets a description line for the omnibox suggestion.
|
| + *
|
| + * @param str The description text.
|
| + */
|
| + private void showDescriptionLine(Spannable str, int textColor) {
|
| + if (mContentsView.mTextLine2.getVisibility() != VISIBLE) {
|
| + mContentsView.mTextLine2.setVisibility(VISIBLE);
|
| + }
|
| + mContentsView.mTextLine2.setTextColor(textColor);
|
| + mContentsView.mTextLine2.setText(str, BufferType.SPANNABLE);
|
| + }
|
| +
|
| + /**
|
| + * Sets the text of the first line of the omnibox suggestion.
|
| + *
|
| + * @param suggestionItem The item containing the suggestion data.
|
| + * @param showDescriptionIfPresent Whether to show the description text of the suggestion if
|
| + * the item contains valid data.
|
| + * @param isUrlQuery Whether this suggestion is showing an URL.
|
| + * @param isUrlHighlighted Whether the URL contains any highlighted matching sections.
|
| + */
|
| + private void setSuggestedQuery(
|
| + OmniboxResultItem suggestionItem, boolean showDescriptionIfPresent,
|
| + boolean isUrlQuery, boolean isUrlHighlighted) {
|
| + String userQuery = suggestionItem.getMatchedQuery();
|
| + String suggestedQuery = null;
|
| + OmniboxSuggestion suggestion = suggestionItem.getSuggestion();
|
| + if (showDescriptionIfPresent && !TextUtils.isEmpty(suggestion.getUrl())
|
| + && !TextUtils.isEmpty(suggestion.getDescription())) {
|
| + suggestedQuery = suggestion.getDescription();
|
| + } else {
|
| + suggestedQuery = suggestion.getDisplayText();
|
| + }
|
| + if (suggestedQuery == null) {
|
| + assert false : "Invalid suggestion sent with no displayable text";
|
| + suggestedQuery = "";
|
| + } else if (suggestedQuery.equals(suggestion.getUrl())) {
|
| + // This is a navigation match with the title defaulted to the URL, display formatted URL
|
| + // so that they continue matching.
|
| + suggestedQuery = suggestion.getFormattedUrl();
|
| + }
|
| +
|
| + if (mSuggestion.getType() == Type.SEARCH_SUGGEST_TAIL) {
|
| + String fillIntoEdit = mSuggestion.getFillIntoEdit();
|
| + // Data sanity checks.
|
| + if (fillIntoEdit.startsWith(userQuery)
|
| + && fillIntoEdit.endsWith(suggestedQuery)
|
| + && fillIntoEdit.length() < userQuery.length() + suggestedQuery.length()) {
|
| + String ignoredPrefix = fillIntoEdit.substring(
|
| + 0, fillIntoEdit.length() - suggestedQuery.length());
|
| + final String ellipsisPrefix = "\u2026 ";
|
| + suggestedQuery = ellipsisPrefix + suggestedQuery;
|
| + if (userQuery.startsWith(ignoredPrefix)) {
|
| + userQuery = ellipsisPrefix + userQuery.substring(ignoredPrefix.length());
|
| + }
|
| + if (DeviceFormFactor.isTablet(getContext())) {
|
| + TextPaint tp = mContentsView.mTextLine1.getPaint();
|
| + mContentsView.mRequiredWidth =
|
| + tp.measureText(fillIntoEdit, 0, fillIntoEdit.length());
|
| + mContentsView.mMatchContentsWidth =
|
| + tp.measureText(suggestedQuery, 0, suggestedQuery.length());
|
| +
|
| + // Update the max text widths values in SuggestionList. These will be passed to
|
| + // the contents view on layout.
|
| + mSuggestionDelegate.onTextWidthsUpdated(
|
| + mContentsView.mRequiredWidth, mContentsView.mMatchContentsWidth);
|
| + }
|
| + }
|
| + }
|
| +
|
| + Spannable str = SpannableString.valueOf(suggestedQuery);
|
| + int userQueryIndex = isUrlHighlighted ? -1
|
| + : suggestedQuery.toLowerCase(Locale.getDefault()).indexOf(
|
| + userQuery.toLowerCase(Locale.getDefault()));
|
| + if (userQueryIndex != -1) {
|
| + int spanStart = 0;
|
| + int spanEnd = 0;
|
| + if (isUrlQuery) {
|
| + spanStart = userQueryIndex;
|
| + spanEnd = userQueryIndex + userQuery.length();
|
| + } else {
|
| + spanStart = userQueryIndex + userQuery.length();
|
| + spanEnd = str.length();
|
| + }
|
| + spanStart = Math.min(spanStart, str.length());
|
| + spanEnd = Math.min(spanEnd, str.length());
|
| + if (spanStart != spanEnd) {
|
| + str.setSpan(
|
| + new StyleSpan(android.graphics.Typeface.BOLD),
|
| + spanStart, spanEnd,
|
| + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
| + }
|
| + }
|
| + mContentsView.mTextLine1.setText(str, BufferType.SPANNABLE);
|
| + }
|
| +
|
| + /**
|
| + * Sets both lines of the Omnibox suggestion based on an Answers in Suggest result.
|
| + *
|
| + * @param answer The answer to be displayed.
|
| + */
|
| + private void setAnswer(SuggestionAnswer answer) {
|
| + float density = getResources().getDisplayMetrics().density;
|
| +
|
| + SuggestionAnswer.ImageLine firstLine = answer.getFirstLine();
|
| + mContentsView.mTextLine1.setTextSize(AnswerTextBuilder.getMaxTextHeightSp(firstLine));
|
| + Spannable firstLineText = AnswerTextBuilder.buildSpannable(
|
| + firstLine, mContentsView.mTextLine1.getPaint().getFontMetrics(), density);
|
| + mContentsView.mTextLine1.setText(firstLineText, BufferType.SPANNABLE);
|
| +
|
| + SuggestionAnswer.ImageLine secondLine = answer.getSecondLine();
|
| + mContentsView.mTextLine2.setTextSize(AnswerTextBuilder.getMaxTextHeightSp(secondLine));
|
| + Spannable secondLineText = AnswerTextBuilder.buildSpannable(
|
| + secondLine, mContentsView.mTextLine2.getPaint().getFontMetrics(), density);
|
| + mContentsView.mTextLine2.setText(secondLineText, BufferType.SPANNABLE);
|
| +
|
| + if (secondLine.hasImage()) {
|
| + mContentsView.mAnswerImage.setVisibility(VISIBLE);
|
| +
|
| + float textSize = mContentsView.mTextLine2.getTextSize();
|
| + int imageSize = (int) (textSize * ANSWER_IMAGE_SCALING_FACTOR);
|
| + mContentsView.mAnswerImage.getLayoutParams().height = imageSize;
|
| + mContentsView.mAnswerImage.getLayoutParams().width = imageSize;
|
| + mContentsView.mAnswerImageMaxSize = imageSize;
|
| +
|
| + String url = "https:" + secondLine.getImage().replace("\\/", "/");
|
| + AnswersImage.requestAnswersImage(
|
| + mLocationBar.getCurrentTab().getProfile(),
|
| + url,
|
| + new AnswersImage.AnswersImageObserver() {
|
| + @Override
|
| + public void onAnswersImageChanged(Bitmap bitmap) {
|
| + mContentsView.mAnswerImage.setImageBitmap(bitmap);
|
| + }
|
| + });
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Handles triggering a selection request for the suggestion rendered by this view.
|
| + */
|
| + private class PerformSelectSuggestion implements Runnable {
|
| + @Override
|
| + public void run() {
|
| + mSuggestionDelegate.onSelection(mSuggestion, mPosition);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Handles triggering a refine request for the suggestion rendered by this view.
|
| + */
|
| + private class PerformRefineSuggestion implements Runnable {
|
| + @Override
|
| + public void run() {
|
| + mSuggestionDelegate.onRefineSuggestion(mSuggestion);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * @return The left offset for the suggestion text that will align it with the url text.
|
| + */
|
| + private int getUrlBarTextLeftPosition() {
|
| + mUrlBar.getLocationOnScreen(mViewPositionHolder);
|
| + return mViewPositionHolder[0] + mUrlBar.getPaddingLeft();
|
| + }
|
| +
|
| + /**
|
| + * @return The right offset for the suggestion text that will align it with the url text.
|
| + */
|
| + private int getUrlBarTextRightPosition() {
|
| + mUrlBar.getLocationOnScreen(mViewPositionHolder);
|
| + return mViewPositionHolder[0] + mUrlBar.getWidth() - mUrlBar.getPaddingRight();
|
| + }
|
| +
|
| + /**
|
| + * Container view for the contents of the suggestion (the search query, URL, and suggestion type
|
| + * icon).
|
| + */
|
| + private class SuggestionContentsContainer extends ViewGroup implements OnLayoutChangeListener {
|
| + private int mSuggestionIconLeft = Integer.MIN_VALUE;
|
| + private int mTextLeft = Integer.MIN_VALUE;
|
| + private int mTextRight = Integer.MIN_VALUE;
|
| + private Drawable mSuggestionIcon;
|
| + private SuggestionIconType mSuggestionIconType;
|
| +
|
| + private final TextView mTextLine1;
|
| + private final TextView mTextLine2;
|
| + private final ImageView mAnswerImage;
|
| +
|
| + private int mAnswerImageMaxSize; // getMaxWidth() is API 16+, so store it locally.
|
| + private float mRequiredWidth;
|
| + private float mMatchContentsWidth;
|
| + private boolean mForceIsFocused;
|
| +
|
| + private final Runnable mRelayoutRunnable = new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + requestLayout();
|
| + }
|
| + };
|
| +
|
| + @SuppressLint("InlinedApi")
|
| + SuggestionContentsContainer(Context context, Drawable backgroundDrawable) {
|
| + super(context);
|
| +
|
| + ApiCompatibilityUtils.setLayoutDirection(this, View.LAYOUT_DIRECTION_INHERIT);
|
| +
|
| + setBackground(backgroundDrawable);
|
| + setClickable(true);
|
| + setFocusable(true);
|
| + setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, mSuggestionHeight));
|
| + setOnClickListener(new OnClickListener() {
|
| + @Override
|
| + public void onClick(View v) {
|
| + // Post the selection action to the end of the UI thread to allow the suggestion
|
| + // view a chance to update their background selection state.
|
| + PerformSelectSuggestion performSelection = new PerformSelectSuggestion();
|
| + if (!post(performSelection)) performSelection.run();
|
| + }
|
| + });
|
| + setOnLongClickListener(new OnLongClickListener() {
|
| + @Override
|
| + public boolean onLongClick(View v) {
|
| + RecordUserAction.record("MobileOmniboxDeleteGesture");
|
| + if (!mSuggestion.isDeletable()) return true;
|
| +
|
| + AlertDialog.Builder b =
|
| + new AlertDialog.Builder(getContext(), R.style.AlertDialogTheme);
|
| + b.setTitle(mSuggestion.getDisplayText());
|
| + b.setMessage(R.string.omnibox_confirm_delete);
|
| + DialogInterface.OnClickListener okListener =
|
| + new DialogInterface.OnClickListener() {
|
| + @Override
|
| + public void onClick(DialogInterface dialog, int which) {
|
| + RecordUserAction.record("MobileOmniboxDeleteRequested");
|
| + mSuggestionDelegate.onDeleteSuggestion(mPosition);
|
| + }
|
| + };
|
| + b.setPositiveButton(android.R.string.ok, okListener);
|
| + DialogInterface.OnClickListener cancelListener =
|
| + new DialogInterface.OnClickListener() {
|
| + @Override
|
| + public void onClick(DialogInterface dialog, int which) {
|
| + dialog.cancel();
|
| + }
|
| + };
|
| + b.setNegativeButton(android.R.string.cancel, cancelListener);
|
| +
|
| + AlertDialog dialog = b.create();
|
| + dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
|
| + @Override
|
| + public void onDismiss(DialogInterface dialog) {
|
| + mSuggestionDelegate.onHideModal();
|
| + }
|
| + });
|
| +
|
| + mSuggestionDelegate.onShowModal();
|
| + dialog.show();
|
| + return true;
|
| + }
|
| + });
|
| +
|
| + mTextLine1 = new TextView(context);
|
| + mTextLine1.setLayoutParams(
|
| + new LayoutParams(LayoutParams.WRAP_CONTENT, mSuggestionHeight));
|
| + mTextLine1.setSingleLine();
|
| + mTextLine1.setTextColor(getStandardFontColor());
|
| + addView(mTextLine1);
|
| +
|
| + mTextLine2 = new TextView(context);
|
| + mTextLine2.setLayoutParams(
|
| + new LayoutParams(LayoutParams.WRAP_CONTENT, mSuggestionHeight));
|
| + mTextLine2.setSingleLine();
|
| + mTextLine2.setVisibility(INVISIBLE);
|
| + addView(mTextLine2);
|
| +
|
| + mAnswerImage = new ImageView(context);
|
| + mAnswerImage.setVisibility(GONE);
|
| + mAnswerImage.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
| + mAnswerImage.setLayoutParams(new LayoutParams(0, 0));
|
| + mAnswerImageMaxSize = 0;
|
| + addView(mAnswerImage);
|
| + }
|
| +
|
| + private void resetTextWidths() {
|
| + mRequiredWidth = 0;
|
| + mMatchContentsWidth = 0;
|
| + }
|
| +
|
| + @Override
|
| + protected void onDraw(Canvas canvas) {
|
| + super.onDraw(canvas);
|
| +
|
| + if (DeviceFormFactor.isTablet(getContext())) {
|
| + // Use the same image transform matrix as the navigation icon to ensure the same
|
| + // scaling, which requires centering vertically based on the height of the
|
| + // navigation icon view and not the image itself.
|
| + canvas.save();
|
| + mSuggestionIconLeft = getSuggestionIconLeftPosition();
|
| + canvas.translate(
|
| + mSuggestionIconLeft,
|
| + (getMeasuredHeight() - mNavigationButton.getMeasuredHeight()) / 2f);
|
| + canvas.concat(mNavigationButton.getImageMatrix());
|
| + mSuggestionIcon.draw(canvas);
|
| + canvas.restore();
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
|
| + if (child != mTextLine1 && child != mTextLine2 && child != mAnswerImage) {
|
| + return super.drawChild(canvas, child, drawingTime);
|
| + }
|
| +
|
| + int height = getMeasuredHeight();
|
| + int line1Height = mTextLine1.getMeasuredHeight();
|
| + int line2Height = mTextLine2.getVisibility() == VISIBLE
|
| + ? mTextLine2.getMeasuredHeight() : 0;
|
| +
|
| + int verticalOffset = 0;
|
| + if (line1Height + line2Height > height) {
|
| + // The text lines total height is larger than this view, snap them to the top and
|
| + // bottom of the view.
|
| + if (child == mTextLine1) {
|
| + verticalOffset = 0;
|
| + } else {
|
| + verticalOffset = height - line2Height;
|
| + }
|
| + } else {
|
| + // The text lines fit comfortably, so vertically center them.
|
| + verticalOffset = (height - line1Height - line2Height) / 2;
|
| + if (child == mTextLine2) verticalOffset += line1Height;
|
| +
|
| + // When one line is larger than the other, it contains extra vertical padding. This
|
| + // produces more apparent whitespace above or below the text lines. Add a small
|
| + // offset to compensate.
|
| + if (line1Height != line2Height) {
|
| + verticalOffset += (line2Height - line1Height) / 10;
|
| + }
|
| +
|
| + // The image is positioned vertically aligned with the second text line but
|
| + // requires a small additional offset to align with the ascent of the text instead
|
| + // of the top of the text which includes some whitespace.
|
| + if (child == mAnswerImage) {
|
| + verticalOffset += ANSWER_IMAGE_VERTICAL_SPACING_DP
|
| + * getResources().getDisplayMetrics().density;
|
| + }
|
| + }
|
| +
|
| + canvas.save();
|
| + canvas.translate(0, verticalOffset);
|
| + boolean retVal = super.drawChild(canvas, child, drawingTime);
|
| + canvas.restore();
|
| + return retVal;
|
| + }
|
| +
|
| + @Override
|
| + protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
| + View locationBarView = mLocationBar.getContainerView();
|
| + if (mUrlBar == null) {
|
| + mUrlBar = (UrlBar) locationBarView.findViewById(R.id.url_bar);
|
| + mUrlBar.addOnLayoutChangeListener(this);
|
| + }
|
| + if (mNavigationButton == null) {
|
| + mNavigationButton =
|
| + (ImageView) locationBarView.findViewById(R.id.navigation_button);
|
| + mNavigationButton.addOnLayoutChangeListener(this);
|
| + }
|
| +
|
| + // Align the text to be pixel perfectly aligned with the text in the url bar.
|
| + mTextLeft = getSuggestionTextLeftPosition();
|
| + mTextRight = getSuggestionTextRightPosition();
|
| + boolean isRTL = ApiCompatibilityUtils.isLayoutRtl(this);
|
| + if (DeviceFormFactor.isTablet(getContext())) {
|
| + int textWidth = isRTL ? mTextRight : (r - l - mTextLeft);
|
| + final float maxRequiredWidth = mSuggestionDelegate.getMaxRequiredWidth();
|
| + final float maxMatchContentsWidth = mSuggestionDelegate.getMaxMatchContentsWidth();
|
| + float paddingStart = (textWidth > maxRequiredWidth)
|
| + ? (mRequiredWidth - mMatchContentsWidth)
|
| + : Math.max(textWidth - maxMatchContentsWidth, 0);
|
| + ApiCompatibilityUtils.setPaddingRelative(
|
| + mTextLine1, (int) paddingStart, mTextLine1.getPaddingTop(),
|
| + 0, // TODO(skanuj) : Change to ApiCompatibilityUtils.getPaddingEnd(...).
|
| + mTextLine1.getPaddingBottom());
|
| + }
|
| +
|
| + int imageWidth = mAnswerImageMaxSize;
|
| + int imageSpacing = 0;
|
| + if (mAnswerImage.getVisibility() == VISIBLE && imageWidth > 0) {
|
| + float density = getResources().getDisplayMetrics().density;
|
| + imageSpacing = (int) (ANSWER_IMAGE_HORIZONTAL_SPACING_DP * density);
|
| + }
|
| + if (isRTL) {
|
| + mTextLine1.layout(0, t, mTextRight, b);
|
| + mAnswerImage.layout(mTextRight - imageWidth , t, mTextRight, b);
|
| + mTextLine2.layout(0, t, mTextRight - (imageWidth + imageSpacing), b);
|
| + } else {
|
| + mTextLine1.layout(mTextLeft, t, r - l, b);
|
| + mAnswerImage.layout(mTextLeft, t, mTextLeft + imageWidth, b);
|
| + mTextLine2.layout(mTextLeft + imageWidth + imageSpacing, t, r - l, b);
|
| + }
|
| +
|
| + int suggestionIconPosition = getSuggestionIconLeftPosition();
|
| + if (mSuggestionIconLeft != suggestionIconPosition
|
| + && mSuggestionIconLeft != Integer.MIN_VALUE) {
|
| + mContentsView.postInvalidateOnAnimation();
|
| + }
|
| + mSuggestionIconLeft = suggestionIconPosition;
|
| + }
|
| +
|
| + /**
|
| + * @return The left offset for the suggestion text that will align it with the url text.
|
| + */
|
| + private int getSuggestionTextLeftPosition() {
|
| + if (mLocationBar == null) return 0;
|
| +
|
| + int textLeftPosition = getUrlBarTextLeftPosition();
|
| + getLocationOnScreen(mViewPositionHolder);
|
| + return textLeftPosition - mViewPositionHolder[0];
|
| + }
|
| +
|
| + /**
|
| + * @return The right offset for the suggestion text that will align it with the url text.
|
| + */
|
| + private int getSuggestionTextRightPosition() {
|
| + if (mLocationBar == null) return 0;
|
| +
|
| + int textRightPosition = getUrlBarTextRightPosition();
|
| + getLocationOnScreen(mViewPositionHolder);
|
| + return textRightPosition - mViewPositionHolder[0];
|
| + }
|
| +
|
| + /**
|
| + * @return The left offset for the suggestion type icon that aligns it with the url bar.
|
| + */
|
| + private int getSuggestionIconLeftPosition() {
|
| + if (mNavigationButton == null) return 0;
|
| +
|
| + // Ensure the suggestion icon matches the location of the navigation icon in the omnibox
|
| + // perfectly.
|
| + mNavigationButton.getLocationOnScreen(mViewPositionHolder);
|
| + int navButtonXPosition = mViewPositionHolder[0] + mNavigationButton.getPaddingLeft();
|
| +
|
| + getLocationOnScreen(mViewPositionHolder);
|
| +
|
| + return navButtonXPosition - mViewPositionHolder[0];
|
| + }
|
| +
|
| + @Override
|
| + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
| + super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
| +
|
| + int width = MeasureSpec.getSize(widthMeasureSpec);
|
| + int height = MeasureSpec.getSize(heightMeasureSpec);
|
| +
|
| + if (mTextLine1.getMeasuredWidth() != width
|
| + || mTextLine1.getMeasuredHeight() != height) {
|
| + mTextLine1.measure(
|
| + MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.AT_MOST),
|
| + MeasureSpec.makeMeasureSpec(mSuggestionHeight, MeasureSpec.AT_MOST));
|
| + }
|
| +
|
| + if (mTextLine2.getMeasuredWidth() != width
|
| + || mTextLine2.getMeasuredHeight() != height) {
|
| + mTextLine2.measure(
|
| + MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.AT_MOST),
|
| + MeasureSpec.makeMeasureSpec(mSuggestionHeight, MeasureSpec.AT_MOST));
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void invalidate() {
|
| + if (getSuggestionTextLeftPosition() != mTextLeft
|
| + || getSuggestionTextRightPosition() != mTextRight) {
|
| + // When the text position is changed, it typically is caused by the suggestions
|
| + // appearing while the URL bar on the phone is gaining focus (if you trigger an
|
| + // intent that will result in suggestions being shown before focusing the omnibox).
|
| + // Triggering a relayout will cause any animations to stutter, so we continually
|
| + // push the relayout to end of the UI queue until the animation is complete.
|
| + removeCallbacks(mRelayoutRunnable);
|
| + postDelayed(mRelayoutRunnable, RELAYOUT_DELAY_MS);
|
| + } else {
|
| + super.invalidate();
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public boolean isFocused() {
|
| + return mForceIsFocused || super.isFocused();
|
| + }
|
| +
|
| + @Override
|
| + protected int[] onCreateDrawableState(int extraSpace) {
|
| + // When creating the drawable states, treat selected as focused to get the proper
|
| + // highlight when in non-touch mode (i.e. physical keyboard). This is because only
|
| + // a single view in a window can have focus, and the these will only appear if
|
| + // the omnibox has focus, so we trick the drawable state into believing it has it.
|
| + mForceIsFocused = isSelected() && !isInTouchMode();
|
| + int[] drawableState = super.onCreateDrawableState(extraSpace);
|
| + mForceIsFocused = false;
|
| + return drawableState;
|
| + }
|
| +
|
| + private void setSuggestionIcon(SuggestionIconType type, boolean invalidateCurrentIcon) {
|
| + if (mSuggestionIconType == type && !invalidateCurrentIcon) return;
|
| +
|
| + int drawableId = R.drawable.ic_omnibox_page;
|
| + switch (type) {
|
| + case BOOKMARK:
|
| + drawableId = R.drawable.btn_star;
|
| + break;
|
| + case MAGNIFIER:
|
| + drawableId = R.drawable.ic_suggestion_magnifier;
|
| + break;
|
| + case HISTORY:
|
| + drawableId = R.drawable.ic_suggestion_history;
|
| + break;
|
| + case VOICE:
|
| + drawableId = R.drawable.btn_mic;
|
| + break;
|
| + default:
|
| + break;
|
| + }
|
| + mSuggestionIcon = ApiCompatibilityUtils.getDrawable(getResources(), drawableId);
|
| + mSuggestionIcon.setColorFilter(mUseDarkColors
|
| + ? getResources().getColor(R.color.light_normal_color)
|
| + : Color.WHITE, PorterDuff.Mode.SRC_IN);
|
| + mSuggestionIcon.setBounds(
|
| + 0, 0,
|
| + mSuggestionIcon.getIntrinsicWidth(),
|
| + mSuggestionIcon.getIntrinsicHeight());
|
| + mSuggestionIconType = type;
|
| + invalidate();
|
| + }
|
| +
|
| + @Override
|
| + public void onLayoutChange(
|
| + View v, int left, int top, int right, int bottom, int oldLeft,
|
| + int oldTop, int oldRight, int oldBottom) {
|
| + boolean needsInvalidate = false;
|
| + if (v == mNavigationButton) {
|
| + if (mSuggestionIconLeft != getSuggestionIconLeftPosition()
|
| + && mSuggestionIconLeft != Integer.MIN_VALUE) {
|
| + needsInvalidate = true;
|
| + }
|
| + } else {
|
| + if (mTextLeft != getSuggestionTextLeftPosition()
|
| + && mTextLeft != Integer.MIN_VALUE) {
|
| + needsInvalidate = true;
|
| + }
|
| + if (mTextRight != getSuggestionTextRightPosition()
|
| + && mTextRight != Integer.MIN_VALUE) {
|
| + needsInvalidate = true;
|
| + }
|
| + }
|
| + if (needsInvalidate) {
|
| + removeCallbacks(mRelayoutRunnable);
|
| + postDelayed(mRelayoutRunnable, RELAYOUT_DELAY_MS);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + protected void onAttachedToWindow() {
|
| + super.onAttachedToWindow();
|
| + if (mNavigationButton != null) mNavigationButton.addOnLayoutChangeListener(this);
|
| + if (mUrlBar != null) mUrlBar.addOnLayoutChangeListener(this);
|
| + if (mLocationBar != null) {
|
| + mLocationBar.getContainerView().addOnLayoutChangeListener(this);
|
| + }
|
| + getRootView().addOnLayoutChangeListener(this);
|
| + }
|
| +
|
| + @Override
|
| + protected void onDetachedFromWindow() {
|
| + if (mNavigationButton != null) mNavigationButton.removeOnLayoutChangeListener(this);
|
| + if (mUrlBar != null) mUrlBar.removeOnLayoutChangeListener(this);
|
| + if (mLocationBar != null) {
|
| + mLocationBar.getContainerView().removeOnLayoutChangeListener(this);
|
| + }
|
| + getRootView().removeOnLayoutChangeListener(this);
|
| +
|
| + super.onDetachedFromWindow();
|
| + }
|
| + }
|
| +}
|
|
|