Get rid of drawing hacks for search dialog suggestions
Before, SuggestionsAdapter parsed every HTML formatted
string three times, to support state-dependent colors
in <font> tags. Now that there is support in Html
for color resources (added in
https://android-git.corp.google.com/g/7441),
we can get rid of this code.
Also, SuggestionsAdapter had a special purpose view
for drawing background colors when suggestion items
were not selected or pressed. This change replaces that
code with a StateListDrawable of ColorDrawables.
Before this change, HTML parsing used ~17% (uncontrolled benchmark,
just did some random searching) of the system_process CPU.
This change should reduce that by 2/3, i.e. about ~11% total
system_process reduction.
diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java
index 58e66b6..a30d911 100644
--- a/core/java/android/app/SuggestionsAdapter.java
+++ b/core/java/android/app/SuggestionsAdapter.java
@@ -23,22 +23,20 @@
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.database.Cursor;
-import android.graphics.Canvas;
+import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.server.search.SearchableInfo;
import android.text.Html;
import android.text.TextUtils;
-import android.util.DisplayMetrics;
import android.util.Log;
-import android.util.TypedValue;
+import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.AbsListView;
import android.widget.ImageView;
import android.widget.ResourceCursorAdapter;
import android.widget.TextView;
@@ -63,6 +61,7 @@
private SearchableInfo mSearchable;
private Context mProviderContext;
private WeakHashMap<String, Drawable> mOutsideDrawablesCache;
+ private SparseArray<Drawable> mBackgroundsCache;
private boolean mGlobalSearchMode;
// Cached column indexes, updated when the cursor changes.
@@ -106,6 +105,7 @@
mProviderContext = mSearchable.getProviderContext(mContext, activityContext);
mOutsideDrawablesCache = outsideDrawablesCache;
+ mBackgroundsCache = new SparseArray<Drawable>();
mGlobalSearchMode = globalSearchMode;
mStartSpinnerRunnable = new Runnable() {
@@ -256,7 +256,7 @@
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
- View v = new SuggestionItemView(context, cursor);
+ View v = super.newView(context, cursor, parent);
v.setTag(new ChildViewCache(v));
return v;
}
@@ -301,18 +301,13 @@
if (mBackgroundColorCol != -1) {
backgroundColor = cursor.getInt(mBackgroundColorCol);
}
- ((SuggestionItemView)view).setColor(backgroundColor);
+ Drawable background = getItemBackground(backgroundColor);
+ view.setBackgroundDrawable(background);
final boolean isHtml = mFormatCol > 0 && "html".equals(cursor.getString(mFormatCol));
- String text1 = null;
- if (mText1Col >= 0) {
- text1 = cursor.getString(mText1Col);
- }
- String text2 = null;
- if (mText2Col >= 0) {
- text2 = cursor.getString(mText2Col);
- }
- ((SuggestionItemView)view).setTextStrings(text1, text2, isHtml, mProviderContext);
+ setViewText(cursor, views.mText1, mText1Col, isHtml);
+ setViewText(cursor, views.mText2, mText2Col, isHtml);
+
if (views.mIcon1 != null) {
setViewDrawable(views.mIcon1, getIcon1(cursor));
}
@@ -321,6 +316,57 @@
}
}
+ /**
+ * Gets a drawable with no color when selected or pressed, and the given color when
+ * neither selected nor pressed.
+ *
+ * @return A drawable, or {@code null} if the given color is transparent.
+ */
+ private Drawable getItemBackground(int backgroundColor) {
+ if (backgroundColor == 0) {
+ return null;
+ } else {
+ Drawable cachedBg = mBackgroundsCache.get(backgroundColor);
+ if (cachedBg != null) {
+ if (DBG) Log.d(LOG_TAG, "Background cache hit for color " + backgroundColor);
+ // copy the drawable so that they don't share states
+ return cachedBg.getConstantState().newDrawable();
+ }
+ if (DBG) Log.d(LOG_TAG, "Creating new background for color " + backgroundColor);
+ ColorDrawable transparent = new ColorDrawable(0);
+ ColorDrawable background = new ColorDrawable(backgroundColor);
+ StateListDrawable newBg = new StateListDrawable();
+ newBg.addState(new int[]{android.R.attr.state_selected}, transparent);
+ newBg.addState(new int[]{android.R.attr.state_pressed}, transparent);
+ newBg.addState(new int[]{}, background);
+ mBackgroundsCache.put(backgroundColor, newBg);
+ return newBg;
+ }
+ }
+
+ private void setViewText(Cursor cursor, TextView v, int textCol, boolean isHtml) {
+ if (v == null) {
+ return;
+ }
+ CharSequence text = null;
+ if (textCol >= 0) {
+ String str = cursor.getString(textCol);
+ if (isHtml && !TextUtils.isEmpty(str)) {
+ text = Html.fromHtml(str);
+ } else {
+ text = str;
+ }
+ }
+ // Set the text even if it's null, since we need to clear any previous text.
+ v.setText(text);
+
+ if (TextUtils.isEmpty(text)) {
+ v.setVisibility(View.GONE);
+ } else {
+ v.setVisibility(View.VISIBLE);
+ }
+ }
+
private Drawable getIcon1(Cursor cursor) {
if (mIconName1Col < 0) {
return null;
@@ -594,179 +640,4 @@
return cursor.getString(col);
}
- /**
- * A parent viewgroup class which holds the actual suggestion item as a child.
- *
- * The sole purpose of this class is to draw the given background color when the item is in
- * normal state and not draw the background color when it is pressed, so that when pressed the
- * list view's selection highlight will be displayed properly (if we draw our background it
- * draws on top of the list view selection highlight).
- */
- private class SuggestionItemView extends ViewGroup {
- /**
- * Parses a given HTMl string and manages Spannable variants of the string for different
- * states of the suggestion item (selected, pressed and normal). Colors for these different
- * states are specified in the html font tag color attribute in the format '@<RESOURCEID>'
- * where RESOURCEID is the ID of a ColorStateList or Color resource.
- */
- private class MultiStateText {
- private CharSequence mNormal = null; // text to display in normal state.
- private CharSequence mSelected = null; // text to display in selected state.
- private CharSequence mPressed = null; // text to display in pressed state.
- private String mPlainText = null; // valid if the text is stateless plain text.
-
- public MultiStateText(boolean isHtml, String text, Context context) {
- if (!isHtml || text == null) {
- mPlainText = text;
- return;
- }
-
- String textNormal = text;
- String textSelected = text;
- String textPressed = text;
- int textLength = text.length();
- int start = text.indexOf("\"@");
-
- // For each font color attribute which has the value in the form '@<RESOURCEID>',
- // try to load the resource and create the display strings for the 3 states.
- while (start >= 0) {
- start++;
- int end = text.indexOf("\"", start);
- if (end == -1) break;
-
- String colorIdString = text.substring(start, end);
- int colorId = Integer.parseInt(colorIdString.substring(1));
- try {
- // The following call works both for color lists and colors.
- ColorStateList csl = context.getResources().getColorStateList(colorId);
- int normalColor = csl.getColorForState(
- View.EMPTY_STATE_SET, csl.getDefaultColor());
- int selectedColor = csl.getColorForState(
- View.SELECTED_STATE_SET, csl.getDefaultColor());
- int pressedColor = csl.getColorForState(
- View.PRESSED_STATE_SET, csl.getDefaultColor());
-
- // Convert the int color values into a hex string, and strip the first 2
- // characters which will be the alpha (html doesn't want this).
- textNormal = textNormal.replace(colorIdString,
- "#" + Integer.toHexString(normalColor).substring(2));
- textSelected = textSelected.replace(colorIdString,
- "#" + Integer.toHexString(selectedColor).substring(2));
- textPressed = textPressed.replace(colorIdString,
- "#" + Integer.toHexString(pressedColor).substring(2));
- } catch (Resources.NotFoundException e) {
- // Nothing to do.
- }
-
- start = text.indexOf("\"@", end);
- }
- mNormal = Html.fromHtml(textNormal);
- mSelected = Html.fromHtml(textSelected);
- mPressed = Html.fromHtml(textPressed);
- }
- public CharSequence normal() {
- return (mPlainText != null) ? mPlainText : mNormal;
- }
- public CharSequence selected() {
- return (mPlainText != null) ? mPlainText : mSelected;
- }
- public CharSequence pressed() {
- return (mPlainText != null) ? mPlainText : mPressed;
- }
- }
-
- private int mBackgroundColor; // the background color to draw in normal state.
- private View mView; // the suggestion item's view.
- private MultiStateText mText1Strings = null;
- private MultiStateText mText2Strings = null;
-
- protected SuggestionItemView(Context context, Cursor cursor) {
- // Initialize ourselves
- super(context);
- mBackgroundColor = 0; // transparent by default.
-
- // For our layout use the default list item height from the current theme.
- TypedValue lineHeight = new TypedValue();
- context.getTheme().resolveAttribute(
- com.android.internal.R.attr.searchResultListItemHeight, lineHeight, true);
- DisplayMetrics metrics = new DisplayMetrics();
- metrics.setToDefaults();
- AbsListView.LayoutParams layout = new AbsListView.LayoutParams(
- AbsListView.LayoutParams.FILL_PARENT,
- (int)lineHeight.getDimension(metrics));
-
- setLayoutParams(layout);
-
- // Initialize the child view
- mView = SuggestionsAdapter.super.newView(context, cursor, this);
- if (mView != null) {
- addView(mView, layout.width, layout.height);
- mView.setVisibility(View.VISIBLE);
- }
- }
-
- private void setInitialTextForView(TextView view, MultiStateText multiState,
- String plainText) {
- // Set the text even if it's null, since we need to clear any previous text.
- CharSequence text = (multiState != null) ? multiState.normal() : plainText;
- view.setText(text);
-
- if (TextUtils.isEmpty(text)) {
- view.setVisibility(View.GONE);
- } else {
- view.setVisibility(View.VISIBLE);
- }
- }
-
- public void setTextStrings(String text1, String text2, boolean isHtml, Context context) {
- mText1Strings = new MultiStateText(isHtml, text1, context);
- mText2Strings = new MultiStateText(isHtml, text2, context);
-
- ChildViewCache views = (ChildViewCache) getTag();
- setInitialTextForView(views.mText1, mText1Strings, text1);
- setInitialTextForView(views.mText2, mText2Strings, text2);
- }
-
- public void updateTextViewContentIfRequired() {
- // Check if the pressed or selected state has changed since the last call.
- boolean isPressedNow = isPressed();
- boolean isSelectedNow = isSelected();
-
- ChildViewCache views = (ChildViewCache) getTag();
- views.mText1.setText((isPressedNow ? mText1Strings.pressed() :
- (isSelectedNow ? mText1Strings.selected() : mText1Strings.normal())));
- views.mText2.setText((isPressedNow ? mText2Strings.pressed() :
- (isSelectedNow ? mText2Strings.selected() : mText2Strings.normal())));
- }
-
- public void setColor(int backgroundColor) {
- mBackgroundColor = backgroundColor;
- }
-
- @Override
- public void dispatchDraw(Canvas canvas) {
- updateTextViewContentIfRequired();
-
- if (mBackgroundColor != 0 && !isPressed() && !isSelected()) {
- canvas.drawColor(mBackgroundColor);
- }
- super.dispatchDraw(canvas);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- if (mView != null) {
- mView.measure(widthMeasureSpec, heightMeasureSpec);
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- if (mView != null) {
- mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
- }
- }
- }
-
}