Fix several issues related to CircularIconsPreference
* Sometimes would cause an unending stream of accessibility events (particularly if starting off-screen). This would break TalkBack and anything that depends on UiAutomator (although the system itself took it like a champ). * Sometimes would not load images (because the ViewTreeObserver would never fire onGlobalLayout after being added because measured width was 0, even though a new width was calculated later). * Would not recalculate the number of icons that fit if the width changes after the first layout. Combining ViewHolders with waiting for measuring and/or ViewTreeObservers was always a wonky approach, even though it should've worked in theory. This should be more robust. Also fixes the unwanted animation on mode screen load related to the absence of the header name being applied a tad too late. Fixes: 359948417 Fixes: 360072876 Fixes: 360328804 Test: atest SettingsRoboTests + manual + adb shell uiautomator events Flag: android.app.modes_ui Change-Id: I7e5dfbdab220d1ebc1c68e5e87ce544ee86b6a65
This commit is contained in:
@@ -16,236 +16,72 @@
|
||||
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.RestrictedPreference;
|
||||
|
||||
import com.google.common.base.Equivalence;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class CircularIconsPreference extends RestrictedPreference {
|
||||
|
||||
private static final float DISABLED_ITEM_ALPHA = 0.3f;
|
||||
|
||||
record LoadedIcons(ImmutableList<Drawable> icons, int extraItems) {
|
||||
static final LoadedIcons EMPTY = new LoadedIcons(ImmutableList.of(), 0);
|
||||
}
|
||||
|
||||
private Executor mUiExecutor;
|
||||
|
||||
// Chronologically, fields will be set top-to-bottom.
|
||||
@Nullable private CircularIconSet<?> mIconSet;
|
||||
@Nullable private ListenableFuture<List<Drawable>> mPendingLoadIconsFuture;
|
||||
@Nullable private LoadedIcons mLoadedIcons;
|
||||
private CircularIconSet<?> mIconSet = CircularIconSet.EMPTY;
|
||||
|
||||
public CircularIconsPreference(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
public CircularIconsPreference(Context context, Executor uiExecutor) {
|
||||
this(context);
|
||||
mUiExecutor = uiExecutor;
|
||||
init();
|
||||
}
|
||||
|
||||
public CircularIconsPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
mUiExecutor = context.getMainExecutor();
|
||||
private void init() {
|
||||
setLayoutResource(R.layout.preference_circular_icons);
|
||||
}
|
||||
|
||||
<T> void displayIcons(CircularIconSet<T> iconSet) {
|
||||
displayIcons(iconSet, null);
|
||||
<T> void setIcons(CircularIconSet<T> iconSet) {
|
||||
setIcons(iconSet, null);
|
||||
}
|
||||
|
||||
<T> void displayIcons(CircularIconSet<T> iconSet, @Nullable Equivalence<T> itemEquivalence) {
|
||||
if (mIconSet != null && mIconSet.hasSameItemsAs(iconSet, itemEquivalence)) {
|
||||
<T> void setIcons(CircularIconSet<T> iconSet, @Nullable Equivalence<T> itemEquivalence) {
|
||||
if (mIconSet.hasSameItemsAs(iconSet, itemEquivalence)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mIconSet = iconSet;
|
||||
|
||||
mLoadedIcons = null;
|
||||
if (mPendingLoadIconsFuture != null) {
|
||||
mPendingLoadIconsFuture.cancel(true);
|
||||
mPendingLoadIconsFuture = null;
|
||||
}
|
||||
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
CircularIconsView iconContainer = checkNotNull(
|
||||
(CircularIconsView) holder.findViewById(R.id.circles_container));
|
||||
|
||||
LinearLayout iconContainer = checkNotNull(
|
||||
(LinearLayout) holder.findViewById(R.id.circles_container));
|
||||
bindIconContainer(iconContainer);
|
||||
}
|
||||
|
||||
private void bindIconContainer(LinearLayout container) {
|
||||
if (mLoadedIcons != null) {
|
||||
// We have the icons ready to display already, show them.
|
||||
setDrawables(container, mLoadedIcons);
|
||||
} else if (mIconSet != null) {
|
||||
// We know what icons we want, but haven't yet loaded them.
|
||||
if (mIconSet.size() == 0) {
|
||||
container.setVisibility(View.GONE);
|
||||
mLoadedIcons = LoadedIcons.EMPTY;
|
||||
return;
|
||||
}
|
||||
container.setVisibility(View.VISIBLE);
|
||||
if (container.getMeasuredWidth() != 0) {
|
||||
startLoadingIcons(container, mIconSet);
|
||||
} else {
|
||||
container.getViewTreeObserver().addOnGlobalLayoutListener(
|
||||
new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
container.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startLoadingIcons(LinearLayout container, CircularIconSet<?> iconSet) {
|
||||
Resources res = getContext().getResources();
|
||||
int availableSpace = container.getMeasuredWidth();
|
||||
int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter)
|
||||
+ res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between);
|
||||
int numIconsThatFit = availableSpace / iconHorizontalSpace;
|
||||
|
||||
List<ListenableFuture<Drawable>> iconFutures;
|
||||
int extraItems;
|
||||
if (iconSet.size() > numIconsThatFit) {
|
||||
// Reserve one space for the (+xx) textview.
|
||||
int numIconsToShow = numIconsThatFit - 1;
|
||||
if (numIconsToShow < 0) {
|
||||
numIconsToShow = 0;
|
||||
}
|
||||
iconFutures = iconSet.getIcons(numIconsToShow);
|
||||
extraItems = iconSet.size() - numIconsToShow;
|
||||
} else {
|
||||
// Fit exactly or with remaining space.
|
||||
iconFutures = iconSet.getIcons();
|
||||
extraItems = 0;
|
||||
}
|
||||
|
||||
// Display icons when all are ready (more consistent than randomly loading).
|
||||
mPendingLoadIconsFuture = Futures.allAsList(iconFutures);
|
||||
FutureUtil.whenDone(
|
||||
mPendingLoadIconsFuture,
|
||||
icons -> {
|
||||
mLoadedIcons = new LoadedIcons(ImmutableList.copyOf(icons), extraItems);
|
||||
notifyChanged(); // So that view is rebound and icons actually shown.
|
||||
},
|
||||
mUiExecutor);
|
||||
}
|
||||
|
||||
private void setDrawables(LinearLayout container, LoadedIcons loadedIcons) {
|
||||
// Rearrange child views until we have <numImages> ImageViews...
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
int numImages = loadedIcons.icons.size();
|
||||
int numImageViews = getChildCount(container, ImageView.class);
|
||||
if (numImages > numImageViews) {
|
||||
for (int i = 0; i < numImages - numImageViews; i++) {
|
||||
ImageView imageView = (ImageView) inflater.inflate(
|
||||
R.layout.preference_circular_icons_item, container, false);
|
||||
container.addView(imageView, 0);
|
||||
}
|
||||
} else if (numImageViews > numImages) {
|
||||
for (int i = 0; i < numImageViews - numImages; i++) {
|
||||
container.removeViewAt(0);
|
||||
}
|
||||
}
|
||||
// ... plus 0/1 TextViews at the end.
|
||||
if (loadedIcons.extraItems > 0 && !(getLastChild(container) instanceof TextView)) {
|
||||
TextView plusView = (TextView) inflater.inflate(
|
||||
R.layout.preference_circular_icons_plus_item, container, false);
|
||||
container.addView(plusView);
|
||||
} else if (loadedIcons.extraItems == 0 && (getLastChild(container) instanceof TextView)) {
|
||||
container.removeViewAt(container.getChildCount() - 1);
|
||||
}
|
||||
|
||||
// Show images (and +n if needed).
|
||||
for (int i = 0; i < numImages; i++) {
|
||||
ImageView imageView = (ImageView) container.getChildAt(i);
|
||||
imageView.setImageDrawable(loadedIcons.icons.get(i));
|
||||
}
|
||||
if (loadedIcons.extraItems > 0) {
|
||||
TextView textView = (TextView) checkNotNull(getLastChild(container));
|
||||
textView.setText(getContext().getString(R.string.zen_mode_plus_n_items,
|
||||
loadedIcons.extraItems));
|
||||
}
|
||||
|
||||
// Apply enabled/disabled style.
|
||||
for (int i = 0; i < container.getChildCount(); i++) {
|
||||
View child = container.getChildAt(i);
|
||||
child.setAlpha(isEnabled() ? 1.0f : DISABLED_ITEM_ALPHA);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getChildCount(ViewGroup parent, Class<? extends View> childClass) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < parent.getChildCount(); i++) {
|
||||
if (childClass.isInstance(parent.getChildAt(i))) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static View getLastChild(ViewGroup parent) {
|
||||
if (parent.getChildCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
return parent.getChildAt(parent.getChildCount() - 1);
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
@Nullable
|
||||
LoadedIcons getLoadedIcons() {
|
||||
return mLoadedIcons;
|
||||
iconContainer.setVisibility(mIconSet != null && mIconSet.size() == 0 ? GONE : VISIBLE);
|
||||
iconContainer.setEnabled(isEnabled());
|
||||
iconContainer.setIcons(mIconSet);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user