From c5657c5eae0c049c99753a18ad38acd0f15a6f9c Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Mon, 11 Dec 2023 06:57:52 -0800 Subject: Use a bespoke label view Create a bespoke label view that centers text more precisely in the presense of a end-side badge. The view is used under a flag. One unflagged trivial refactoring was made: some of the ResolverListAdapter$ViewHolder's methods that were only used inside ChooserListAdapter were moved there for a better cohesion; the reset method is split similarly. Bug: 302188527 Test: The view is tested in both Chooser and a stand-alone test app Flag: ACONFIG com.android.intentresolver.bespoke_label_view DEVELOPMENT Change-Id: Iec871afcdc634f2ce50a4b31a8cc34b88ebb496c --- .../android/intentresolver/ChooserActivity.java | 3 +- .../android/intentresolver/ChooserListAdapter.java | 73 ++++++++++++++---- .../intentresolver/ResolverListAdapter.java | 23 +----- .../android/intentresolver/v2/ChooserActivity.java | 3 +- .../android/intentresolver/widget/BadgeTextView.kt | 88 ++++++++++++++++++++++ 5 files changed, 153 insertions(+), 37 deletions(-) create mode 100644 java/src/com/android/intentresolver/widget/BadgeTextView.kt (limited to 'java/src/com') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 9000ab3a..fc011aef 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -1252,7 +1252,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements maxTargetsPerRow, initialIntentsUserSpace, targetDataLoader, - null); + null, + mFeatureFlags); } @Override diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index 876ad5c3..94a89722 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -54,6 +54,7 @@ import com.android.intentresolver.chooser.SelectableTargetInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.icons.TargetDataLoader; import com.android.intentresolver.logging.EventLog; +import com.android.intentresolver.widget.BadgeTextView; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; @@ -109,6 +110,7 @@ public class ChooserListAdapter extends ResolverListAdapter { // Reserve spots for incoming direct share targets by adding placeholders private final TargetInfo mPlaceHolderTargetInfo; private final TargetDataLoader mTargetDataLoader; + private final boolean mUseBadgeTextViewForLabels; private final List mServiceTargets = new ArrayList<>(); private final List mCallerTargets = new ArrayList<>(); @@ -166,7 +168,8 @@ public class ChooserListAdapter extends ResolverListAdapter { int maxRankedTargets, UserHandle initialIntentsUserSpace, TargetDataLoader targetDataLoader, - @Nullable PackageChangeCallback packageChangeCallback) { + @Nullable PackageChangeCallback packageChangeCallback, + FeatureFlags featureFlags) { this( context, payloadIntents, @@ -185,7 +188,8 @@ public class ChooserListAdapter extends ResolverListAdapter { targetDataLoader, packageChangeCallback, AsyncTask.SERIAL_EXECUTOR, - context.getMainExecutor()); + context.getMainExecutor(), + featureFlags); } @VisibleForTesting @@ -207,7 +211,8 @@ public class ChooserListAdapter extends ResolverListAdapter { TargetDataLoader targetDataLoader, @Nullable PackageChangeCallback packageChangeCallback, Executor bgExecutor, - Executor mainExecutor) { + Executor mainExecutor, + FeatureFlags featureFlags) { // Don't send the initial intents through the shared ResolverActivity path, // we want to separate them into a different section. super( @@ -231,6 +236,7 @@ public class ChooserListAdapter extends ResolverListAdapter { mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context); mTargetDataLoader = targetDataLoader; mPackageChangeCallback = packageChangeCallback; + mUseBadgeTextViewForLabels = featureFlags.bespokeLabelView(); createPlaceHolders(); mEventLog = eventLog; mShortcutSelectionLogic = new ShortcutSelectionLogic( @@ -332,7 +338,12 @@ public class ChooserListAdapter extends ResolverListAdapter { @Override View onCreateView(ViewGroup parent) { - return mInflater.inflate(R.layout.resolve_grid_item, parent, false); + return mInflater.inflate( + mUseBadgeTextViewForLabels + ? R.layout.chooser_grid_item + : R.layout.resolve_grid_item, + parent, + false); } @VisibleForTesting @@ -340,7 +351,7 @@ public class ChooserListAdapter extends ResolverListAdapter { public void onBindView(View view, TargetInfo info, int position) { final ViewHolder holder = (ViewHolder) view.getTag(); - holder.reset(); + resetViewHolder(holder); // Always remove the spacing listener, attach as needed to direct share targets below. holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener); @@ -377,16 +388,18 @@ public class ChooserListAdapter extends ResolverListAdapter { contentDescription, mContext.getResources().getString(R.string.pinned)); } - holder.updateContentDescription(contentDescription); + updateContentDescription(holder, contentDescription); if (!info.hasDisplayIcon()) { loadDirectShareIcon((SelectableTargetInfo) info); } } else if (info.isDisplayResolveInfo()) { if (info.isPinned()) { - holder.updateContentDescription(String.join( - ". ", - info.getDisplayLabel(), - mContext.getResources().getString(R.string.pinned))); + updateContentDescription( + holder, + String.join( + ". ", + info.getDisplayLabel(), + mContext.getResources().getString(R.string.pinned))); } DisplayResolveInfo dri = (DisplayResolveInfo) info; if (!dri.hasDisplayIcon()) { @@ -398,22 +411,56 @@ public class ChooserListAdapter extends ResolverListAdapter { } if (info.isPlaceHolderTargetInfo()) { - holder.bindPlaceholder(); + bindPlaceholder(holder); } if (info.isMultiDisplayResolveInfo()) { // If the target is grouped show an indicator - holder.bindGroupIndicator( + bindGroupIndicator( + holder, mContext.getDrawable(R.drawable.chooser_group_background)); } else if (info.isPinned() && (getPositionTargetType(position) == TARGET_STANDARD || getPositionTargetType(position) == TARGET_SERVICE)) { // If the appShare or directShare target is pinned and in the suggested row show a // pinned indicator - holder.bindPinnedIndicator(mContext.getDrawable(R.drawable.chooser_pinned_background)); + bindPinnedIndicator(holder, mContext.getDrawable(R.drawable.chooser_pinned_background)); holder.text.addOnLayoutChangeListener(mPinTextSpacingListener); } } + private void resetViewHolder(ViewHolder holder) { + holder.reset(); + holder.itemView.setBackground(holder.defaultItemViewBackground); + + if (mUseBadgeTextViewForLabels) { + ((BadgeTextView) holder.text).setBadgeDrawable(null); + } + holder.text.setBackground(null); + holder.text.setPaddingRelative(0, 0, 0, 0); + } + + private void updateContentDescription(ViewHolder holder, String description) { + holder.itemView.setContentDescription(description); + } + + private void bindPlaceholder(ViewHolder holder) { + holder.itemView.setBackground(null); + } + + private void bindGroupIndicator(ViewHolder holder, Drawable indicator) { + if (mUseBadgeTextViewForLabels) { + ((BadgeTextView) holder.text).setBadgeDrawable(indicator); + } else { + holder.text.setPaddingRelative(0, 0, /*end = */indicator.getIntrinsicWidth(), 0); + holder.text.setBackground(indicator); + } + } + + private void bindPinnedIndicator(ViewHolder holder, Drawable indicator) { + holder.text.setPaddingRelative(/*start = */indicator.getIntrinsicWidth(), 0, 0, 0); + holder.text.setBackground(indicator); + } + private void loadDirectShareIcon(SelectableTargetInfo info) { if (mRequestedIcons.add(info)) { mTargetDataLoader.loadDirectShareIcon( diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index 564d8d19..262d180a 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -25,7 +25,6 @@ import android.content.pm.ResolveInfo; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.AsyncTask; import android.os.RemoteException; import android.os.Trace; @@ -930,7 +929,7 @@ public class ResolverListAdapter extends BaseAdapter { @VisibleForTesting public static class ViewHolder { public View itemView; - public Drawable defaultItemViewBackground; + public final Drawable defaultItemViewBackground; public TextView text; public TextView text2; @@ -940,8 +939,6 @@ public class ResolverListAdapter extends BaseAdapter { text.setText(""); text.setMaxLines(2); text.setMaxWidth(Integer.MAX_VALUE); - text.setBackground(null); - text.setPaddingRelative(0, 0, 0, 0); text2.setVisibility(View.GONE); text2.setText(""); @@ -982,10 +979,6 @@ public class ResolverListAdapter extends BaseAdapter { itemView.setContentDescription(null); } - public void updateContentDescription(String description) { - itemView.setContentDescription(description); - } - /** * Bind view holder to a TargetInfo. */ @@ -998,19 +991,5 @@ public class ResolverListAdapter extends BaseAdapter { icon.setColorFilter(null); } } - - public void bindPlaceholder() { - itemView.setBackground(null); - } - - public void bindGroupIndicator(Drawable indicator) { - text.setPaddingRelative(0, 0, /*end = */indicator.getIntrinsicWidth(), 0); - text.setBackground(indicator); - } - - public void bindPinnedIndicator(Drawable indicator) { - text.setPaddingRelative(/*start = */indicator.getIntrinsicWidth(), 0, 0, 0); - text.setBackground(indicator); - } } } diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index 70812642..d3c97075 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -1263,7 +1263,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements if (record != null && record.shortcutLoader != null) { record.shortcutLoader.reset(); } - }); + }, + mFeatureFlags); } @Override diff --git a/java/src/com/android/intentresolver/widget/BadgeTextView.kt b/java/src/com/android/intentresolver/widget/BadgeTextView.kt new file mode 100644 index 00000000..b6cadd86 --- /dev/null +++ b/java/src/com/android/intentresolver/widget/BadgeTextView.kt @@ -0,0 +1,88 @@ +package com.android.intentresolver.widget + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.Gravity +import android.widget.TextView + +/** + * A TextView that supports a badge at the end of the text. If the text, when centered in the view, + * leaves enough room for the badge, the badge is just displayed at the end of the view. Otherwise, + * the necessary amount of space for the badge is reserved and the text gets centered in the + * remaining free space. + */ +class BadgeTextView : TextView { + constructor(context: Context) : this(context, null) + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int + ) : this(context, attrs, defStyleAttr, 0) + + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) { + super.setGravity(Gravity.CENTER) + defaultPaddingLeft = paddingLeft + defaultPaddingRight = paddingRight + } + + private var defaultPaddingLeft = 0 + private var defaultPaddingRight = 0 + + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + super.setPadding(left, top, right, bottom) + defaultPaddingLeft = paddingLeft + defaultPaddingRight = paddingRight + } + + override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) { + super.setPaddingRelative(start, top, end, bottom) + defaultPaddingLeft = paddingLeft + defaultPaddingRight = paddingRight + } + + /** Sets end-sided badge. */ + var badgeDrawable: Drawable? = null + set(value) { + if (field !== value) { + field = value + super.setBackground(value) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.setPadding(defaultPaddingLeft, paddingTop, defaultPaddingRight, paddingBottom) + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val badge = badgeDrawable ?: return + if (badge.intrinsicWidth <= paddingEnd) return + var maxLineWidth = 0f + for (i in 0 until layout.lineCount) { + maxLineWidth = maxOf(maxLineWidth, layout.getLineWidth(i)) + } + val sideSpace = (measuredWidth - maxLineWidth) / 2 + if (sideSpace < badge.intrinsicWidth) { + super.setPaddingRelative( + paddingStart, + paddingTop, + paddingEnd + badge.intrinsicWidth - sideSpace.toInt(), + paddingBottom + ) + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + } + + override fun setBackground(background: Drawable?) { + badgeDrawable = null + super.setBackground(background) + } + + override fun setGravity(gravity: Int): Unit = error("Not supported") +} -- cgit v1.2.3-59-g8ed1b