// 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 android.graphics.Color; import android.graphics.Paint; import android.text.Html; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextPaint; import android.text.style.AbsoluteSizeSpan; import android.text.style.ForegroundColorSpan; import android.text.style.MetricAffectingSpan; import android.util.Log; import java.util.List; /** * Helper class that builds Spannables to represent the styled text in answers from Answers in * Suggest. */ class AnswerTextBuilder { private static final String TAG = "AnswerTextBuilder"; // Types, sizes and colors specified at http://goto.google.com/ais_api. // Deprecated: ANSWERS_ANSWER_TEXT_TYPE = 1; // Deprecated: ANSWERS_HEADLINE_TEXT_TYPE = 2; private static final int ANSWERS_TOP_ALIGNED_TEXT_TYPE = 3; // Deprecated: ANSWERS_DESCRIPTION_TEXT_TYPE = 4; private static final int ANSWERS_DESCRIPTION_TEXT_NEGATIVE_TYPE = 5; private static final int ANSWERS_DESCRIPTION_TEXT_POSITIVE_TYPE = 6; // Deprecated: ANSWERS_MORE_INFO_TEXT_TYPE = 7; private static final int ANSWERS_SUGGESTION_TEXT_TYPE = 8; // Deprecated: ANSWERS_SUGGESTION_TEXT_POSITIVE_TYPE = 9; // Deprecated: ANSWERS_SUGGESTION_TEXT_NEGATIVE_TYPE = 10; // Deprecated: ANSWERS_SUGGESTION_LINK_COLOR_TYPE = 11; // Deprecated: ANSWERS_STATUS_TEXT_TYPE = 12; private static final int ANSWERS_PERSONALIZED_SUGGESTION_TEXT_TYPE = 13; // Deprecated: ANSWERS_IMMERSIVE_DESCRIPTION_TEXT = 14; // Deprecated: ANSWERS_DATE_TEXT = 15; // Deprecated: ANSWERS_PREVIEW_TEXT = 16; private static final int ANSWERS_ANSWER_TEXT_MEDIUM_TYPE = 17; private static final int ANSWERS_ANSWER_TEXT_LARGE_TYPE = 18; private static final int ANSWERS_SECONDARY_TEXT_SMALL_TYPE = 19; private static final int ANSWERS_SECONDARY_TEXT_MEDIUM_TYPE = 20; private static final int ANSWERS_TOP_ALIGNED_TEXT_SIZE_SP = 12; private static final int ANSWERS_DESCRIPTION_TEXT_NEGATIVE_SIZE_SP = 16; private static final int ANSWERS_DESCRIPTION_TEXT_POSITIVE_SIZE_SP = 16; private static final int ANSWERS_SUGGESTION_TEXT_SIZE_SP = 16; private static final int ANSWERS_PERSONALIZED_SUGGESTION_TEXT_SIZE_SP = 15; private static final int ANSWERS_ANSWER_TEXT_MEDIUM_SIZE_SP = 20; private static final int ANSWERS_ANSWER_TEXT_LARGE_SIZE_SP = 24; private static final int ANSWERS_SECONDARY_TEXT_SMALL_SIZE_SP = 12; private static final int ANSWERS_SECONDARY_TEXT_MEDIUM_SIZE_SP = 14; private static final int ANSWERS_TOP_ALIGNED_TEXT_COLOR = 0xFF8A8A8A; // These two colors deviate from the AIS spec because they provide better // contrast over the background in Chrome, but they do come from the // Google pallette. private static final int ANSWERS_DESCRIPTION_TEXT_NEGATIVE_COLOR = 0xFFC53929; private static final int ANSWERS_DESCRIPTION_TEXT_POSITIVE_COLOR = 0xFF0B8043; private static final int ANSWERS_SUGGESTION_TEXT_COLOR = SuggestionView.TITLE_COLOR_STANDARD_FONT_DARK; private static final int ANSWERS_PERSONALIZED_SUGGESTION_TEXT_COLOR = Color.BLACK; private static final int ANSWERS_ANSWER_TEXT_MEDIUM_COLOR = SuggestionView.TITLE_COLOR_STANDARD_FONT_DARK; private static final int ANSWERS_ANSWER_TEXT_LARGE_COLOR = SuggestionView.TITLE_COLOR_STANDARD_FONT_DARK; private static final int ANSWERS_SECONDARY_TEXT_SMALL_COLOR = 0xFF8A8A8A; private static final int ANSWERS_SECONDARY_TEXT_MEDIUM_COLOR = 0xFF8A8A8A; /** * Builds a Spannable containing all of the styled text in the supplied ImageLine. * * @param line All text fields within this line will be added to the returned Spannable. * types. * @param metrics Font metrics which will be used to properly size and layout images and top- * aligned text. * @param density Screen density which will be used to properly size and layout images and top- * aligned text. */ static Spannable buildSpannable( SuggestionAnswer.ImageLine line, Paint.FontMetrics metrics, float density) { SpannableStringBuilder builder = new SpannableStringBuilder(); // Determine the height of the largest text element in the line. This // will be used to top-align text and scale images. int maxTextHeightSp = getMaxTextHeightSp(line); List<SuggestionAnswer.TextField> textFields = line.getTextFields(); for (int i = 0; i < textFields.size(); i++) { appendAndStyleText(builder, textFields.get(i), maxTextHeightSp, metrics, density); } if (line.hasAdditionalText()) { builder.append(" "); SuggestionAnswer.TextField additionalText = line.getAdditionalText(); appendAndStyleText(builder, additionalText, maxTextHeightSp, metrics, density); } if (line.hasStatusText()) { builder.append(" "); SuggestionAnswer.TextField statusText = line.getStatusText(); appendAndStyleText(builder, statusText, maxTextHeightSp, metrics, density); } return builder; } /** * Determine the height of the largest text field in the entire line. * * @param line An ImageLine containing the text fields. * @return The height in SP. */ static int getMaxTextHeightSp(SuggestionAnswer.ImageLine line) { int maxHeightSp = 0; List<SuggestionAnswer.TextField> textFields = line.getTextFields(); for (int i = 0; i < textFields.size(); i++) { int height = getAnswerTextSizeSp(textFields.get(i).getType()); if (height > maxHeightSp) { maxHeightSp = height; } } if (line.hasAdditionalText()) { int height = getAnswerTextSizeSp(line.getAdditionalText().getType()); if (height > maxHeightSp) { maxHeightSp = height; } } if (line.hasStatusText()) { int height = getAnswerTextSizeSp(line.getStatusText().getType()); if (height > maxHeightSp) { maxHeightSp = height; } } return maxHeightSp; } /** * Append the styled text in textField to the supplied builder. * * @param builder The builder to append the text to. * @param textField The text field (with text and type) to append. * @param maxTextHeightSp The height in SP of the largest text field in the entire line. Used to * top-align text when specified. * @param metrics Font metrics which will be used to properly size and layout images and top- * aligned text. * @param density Screen density which will be used to properly size and layout images and top- * aligned text. */ private static void appendAndStyleText( SpannableStringBuilder builder, SuggestionAnswer.TextField textField, int maxTextHeightSp, Paint.FontMetrics metrics, float density) { String text = textField.getText(); int type = textField.getType(); // Unescape HTML entities (e.g. """, ">"). text = Html.fromHtml(text).toString(); // Append as HTML (answer responses contain simple markup). int start = builder.length(); builder.append(Html.fromHtml(text)); int end = builder.length(); // Apply styles according to the type. AbsoluteSizeSpan sizeSpan = new AbsoluteSizeSpan(getAnswerTextSizeSp(type), true); builder.setSpan(sizeSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ForegroundColorSpan colorSpan = new ForegroundColorSpan(getAnswerTextColor(type)); builder.setSpan(colorSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); if (type == ANSWERS_TOP_ALIGNED_TEXT_TYPE) { TopAlignedSpan topAlignedSpan = new TopAlignedSpan( ANSWERS_TOP_ALIGNED_TEXT_SIZE_SP, maxTextHeightSp, metrics, density); builder.setSpan(topAlignedSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } /** * Return the SP text height for the specified answer text type. * * @param type The answer type as specified at http://goto.google.com/ais_api. */ private static int getAnswerTextSizeSp(int type) { switch (type) { case ANSWERS_TOP_ALIGNED_TEXT_TYPE: return ANSWERS_TOP_ALIGNED_TEXT_SIZE_SP; case ANSWERS_DESCRIPTION_TEXT_NEGATIVE_TYPE: return ANSWERS_DESCRIPTION_TEXT_NEGATIVE_SIZE_SP; case ANSWERS_DESCRIPTION_TEXT_POSITIVE_TYPE: return ANSWERS_DESCRIPTION_TEXT_POSITIVE_SIZE_SP; case ANSWERS_SUGGESTION_TEXT_TYPE: return ANSWERS_SUGGESTION_TEXT_SIZE_SP; case ANSWERS_PERSONALIZED_SUGGESTION_TEXT_TYPE: return ANSWERS_PERSONALIZED_SUGGESTION_TEXT_SIZE_SP; case ANSWERS_ANSWER_TEXT_MEDIUM_TYPE: return ANSWERS_ANSWER_TEXT_MEDIUM_SIZE_SP; case ANSWERS_ANSWER_TEXT_LARGE_TYPE: return ANSWERS_ANSWER_TEXT_LARGE_SIZE_SP; case ANSWERS_SECONDARY_TEXT_SMALL_TYPE: return ANSWERS_SECONDARY_TEXT_SMALL_SIZE_SP; case ANSWERS_SECONDARY_TEXT_MEDIUM_TYPE: return ANSWERS_SECONDARY_TEXT_MEDIUM_SIZE_SP; default: Log.w(TAG, "Unknown answer type: " + type); return ANSWERS_SUGGESTION_TEXT_SIZE_SP; } } /** * Return the color code for the specified answer text type. * * @param type The answer type as specified at http://goto.google.com/ais_api. */ private static int getAnswerTextColor(int type) { switch (type) { case ANSWERS_TOP_ALIGNED_TEXT_TYPE: return ANSWERS_TOP_ALIGNED_TEXT_COLOR; case ANSWERS_DESCRIPTION_TEXT_NEGATIVE_TYPE: return ANSWERS_DESCRIPTION_TEXT_NEGATIVE_COLOR; case ANSWERS_DESCRIPTION_TEXT_POSITIVE_TYPE: return ANSWERS_DESCRIPTION_TEXT_POSITIVE_COLOR; case ANSWERS_SUGGESTION_TEXT_TYPE: return ANSWERS_SUGGESTION_TEXT_COLOR; case ANSWERS_PERSONALIZED_SUGGESTION_TEXT_TYPE: return ANSWERS_PERSONALIZED_SUGGESTION_TEXT_COLOR; case ANSWERS_ANSWER_TEXT_MEDIUM_TYPE: return ANSWERS_ANSWER_TEXT_MEDIUM_COLOR; case ANSWERS_ANSWER_TEXT_LARGE_TYPE: return ANSWERS_ANSWER_TEXT_LARGE_COLOR; case ANSWERS_SECONDARY_TEXT_SMALL_TYPE: return ANSWERS_SECONDARY_TEXT_SMALL_COLOR; case ANSWERS_SECONDARY_TEXT_MEDIUM_TYPE: return ANSWERS_SECONDARY_TEXT_MEDIUM_COLOR; default: Log.w(TAG, "Unknown answer type: " + type); return ANSWERS_SUGGESTION_TEXT_COLOR; } } /** * Aligns the top of the spanned text with the top of some other specified text height. This is * done by calculating the ascent of both text heights and shifting the baseline of the spanned * text by the difference. As a result, "top aligned" means the top of the ascents are * aligned, which looks as expected in most cases (some glyphs in some fonts are drawn above * the top of the ascent). */ private static class TopAlignedSpan extends MetricAffectingSpan { private int mBaselineShift; /** * Constructor for TopAlignedSpan. * * @param textHeightSp The total height in SP of the text covered by this span. * @param maxTextHeightSp The total height in SP of the text we wish to top-align with. * @param metrics The font metrics used to determine what proportion of the font height is * the ascent. * @param density The display density. */ public TopAlignedSpan( int textHeightSp, int maxTextHeightSp, Paint.FontMetrics metrics, float density) { float ascentProportion = metrics.ascent / (metrics.top - metrics.bottom); int textAscentPx = (int) (textHeightSp * ascentProportion * density); int maxTextAscentPx = (int) (maxTextHeightSp * ascentProportion * density); this.mBaselineShift = -(maxTextAscentPx - textAscentPx); // Up is -y. } @Override public void updateDrawState(TextPaint tp) { tp.baselineShift += mBaselineShift; } @Override public void updateMeasureState(TextPaint tp) { tp.baselineShift += mBaselineShift; } } }