Files
Lawnchair/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
T
Stevie Kideckel 803927bdae Use drawable states for list positions instead of swapping drawables
Replacing drawables interrupts touch feedback of ripples. Implementing
custom state lists allows altering the corner radius without cutting off
ripples and also means we reduce unnecessary object creation.

Fix: 190467676
Test: locally
Change-Id: I10eec042aae57d278f8254327d42df945767c7ac
2021-06-15 16:32:36 +00:00

321 lines
12 KiB
Java

/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.widget.picker;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
import com.android.launcher3.icons.PlaceHolderIconDrawable;
import com.android.launcher3.icons.cache.HandlerRunnable;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
import java.util.stream.Collectors;
/**
* A UI represents a header of an app shown in the full widgets tray.
*
* It is a {@link LinearLayout} which contains an app icon, an app name, a subtitle and a checkbox
* which indicates if the widgets content view underneath this header should be shown.
*/
public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpdateReceiver {
private boolean mEnableIconUpdateAnimation = false;
@Nullable private HandlerRunnable mIconLoadRequest;
@Nullable private Drawable mIconDrawable;
private final int mIconSize;
private final int mBottomMarginSize;
private ImageView mAppIcon;
private TextView mTitle;
private TextView mSubtitle;
private CheckBox mExpandToggle;
private boolean mIsExpanded = false;
@Nullable private WidgetsListDrawableState mListDrawableState;
public WidgetsListHeader(Context context) {
this(context, /* attrs= */ null);
}
public WidgetsListHeader(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, /* defStyle= */ 0);
}
public WidgetsListHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ActivityContext activity = ActivityContext.lookupContext(context);
DeviceProfile grid = activity.getDeviceProfile();
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.WidgetsListRowHeader, defStyleAttr, /* defStyleRes= */ 0);
mIconSize = a.getDimensionPixelSize(R.styleable.WidgetsListRowHeader_appIconSize,
grid.iconSizePx);
mBottomMarginSize =
getResources().getDimensionPixelSize(R.dimen.widget_list_entry_bottom_margin);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mAppIcon = findViewById(R.id.app_icon);
mTitle = findViewById(R.id.app_title);
mSubtitle = findViewById(R.id.app_subtitle);
mExpandToggle = findViewById(R.id.toggle);
findViewById(R.id.app_container).setAccessibilityDelegate(new AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
if (mIsExpanded) {
info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND);
info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
} else {
info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
info.addAction(AccessibilityNodeInfo.ACTION_EXPAND);
}
super.onInitializeAccessibilityNodeInfo(host, info);
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
switch (action) {
case AccessibilityNodeInfo.ACTION_EXPAND:
case AccessibilityNodeInfo.ACTION_COLLAPSE:
callOnClick();
return true;
default:
return super.performAccessibilityAction(host, action, args);
}
}
});
}
/**
* Sets a {@link OnExpansionChangeListener} to get a callback when this app widgets section
* expands / collapses.
*/
@UiThread
public void setOnExpandChangeListener(
@Nullable OnExpansionChangeListener onExpandChangeListener) {
// Use the entire touch area of this view to expand / collapse an app widgets section.
setOnClickListener(view -> {
setExpanded(!mIsExpanded);
if (onExpandChangeListener != null) {
onExpandChangeListener.onExpansionChange(mIsExpanded);
}
});
}
/** Sets the expand toggle to expand / collapse. */
@UiThread
public void setExpanded(boolean isExpanded) {
this.mIsExpanded = isExpanded;
mExpandToggle.setChecked(isExpanded);
if (getLayoutParams() instanceof RecyclerView.LayoutParams) {
int bottomMargin = isExpanded ? 0 : mBottomMarginSize;
RecyclerView.LayoutParams layoutParams =
((RecyclerView.LayoutParams) getLayoutParams());
layoutParams.bottomMargin = bottomMargin;
setLayoutParams(layoutParams);
}
}
/** Sets the {@link WidgetsListDrawableState} and refreshes the background drawable. */
@UiThread
public void setListDrawableState(WidgetsListDrawableState state) {
if (state == mListDrawableState) return;
this.mListDrawableState = state;
refreshDrawableState();
}
/** Apply app icon, labels and tag using a generic {@link WidgetsListHeaderEntry}. */
@UiThread
public void applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry) {
applyIconAndLabel(entry);
}
@UiThread
private void applyIconAndLabel(WidgetsListHeaderEntry entry) {
PackageItemInfo info = entry.mPkgItem;
setIcon(info);
setTitles(entry);
setExpanded(entry.isWidgetListShown());
super.setTag(info);
verifyHighRes();
}
private void setIcon(PackageItemInfo info) {
Drawable icon;
switch (info.category) {
case PackageItemInfo.CONVERSATIONS:
icon = getContext().getDrawable(R.drawable.ic_conversations_widget_category);
break;
default:
icon = info.newIcon(getContext());
}
applyDrawables(icon);
mIconDrawable = icon;
if (mIconDrawable != null) {
mIconDrawable.setVisible(
/* visible= */ getWindowVisibility() == VISIBLE && isShown(),
/* restart= */ false);
}
}
private void applyDrawables(Drawable icon) {
icon.setBounds(0, 0, mIconSize, mIconSize);
LinearLayout.LayoutParams layoutParams =
(LinearLayout.LayoutParams) mAppIcon.getLayoutParams();
layoutParams.width = mIconSize;
layoutParams.height = mIconSize;
mAppIcon.setLayoutParams(layoutParams);
mAppIcon.setImageDrawable(icon);
// If the current icon is a placeholder color, animate its update.
if (mIconDrawable != null
&& mIconDrawable instanceof PlaceHolderIconDrawable
&& mEnableIconUpdateAnimation) {
((PlaceHolderIconDrawable) mIconDrawable).animateIconUpdate(icon);
}
}
private void setTitles(WidgetsListHeaderEntry entry) {
mTitle.setText(entry.mPkgItem.title);
Resources resources = getContext().getResources();
if (entry.widgetsCount == 0 && entry.shortcutsCount == 0) {
mSubtitle.setVisibility(GONE);
return;
}
String subtitle;
if (entry.widgetsCount > 0 && entry.shortcutsCount > 0) {
String widgetsCount = resources.getQuantityString(R.plurals.widgets_count,
entry.widgetsCount, entry.widgetsCount);
String shortcutsCount = resources.getQuantityString(R.plurals.shortcuts_count,
entry.shortcutsCount, entry.shortcutsCount);
subtitle = resources.getString(R.string.widgets_and_shortcuts_count, widgetsCount,
shortcutsCount);
} else if (entry.widgetsCount > 0) {
subtitle = resources.getQuantityString(R.plurals.widgets_count,
entry.widgetsCount, entry.widgetsCount);
} else {
subtitle = resources.getQuantityString(R.plurals.shortcuts_count,
entry.shortcutsCount, entry.shortcutsCount);
}
mSubtitle.setText(subtitle);
mSubtitle.setVisibility(VISIBLE);
}
/** Apply app icon, labels and tag using a generic {@link WidgetsListSearchHeaderEntry}. */
@UiThread
public void applyFromItemInfoWithIcon(WidgetsListSearchHeaderEntry entry) {
applyIconAndLabel(entry);
}
@UiThread
private void applyIconAndLabel(WidgetsListSearchHeaderEntry entry) {
PackageItemInfo info = entry.mPkgItem;
setIcon(info);
setTitles(entry);
setExpanded(entry.isWidgetListShown());
super.setTag(info);
verifyHighRes();
}
private void setTitles(WidgetsListSearchHeaderEntry entry) {
mTitle.setText(entry.mPkgItem.title);
mSubtitle.setText(entry.mWidgets.stream()
.map(item -> item.label).sorted().collect(Collectors.joining(", ")));
mSubtitle.setVisibility(VISIBLE);
}
@Override
public void reapplyItemInfo(ItemInfoWithIcon info) {
if (getTag() == info) {
mIconLoadRequest = null;
mEnableIconUpdateAnimation = true;
// Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
info.bitmap.icon.prepareToDraw();
setIcon((PackageItemInfo) info);
mEnableIconUpdateAnimation = false;
}
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
if (mListDrawableState == null) return super.onCreateDrawableState(extraSpace);
// Augment the state set from the super implementation with the custom states from
// mListDrawableState.
int[] drawableState =
super.onCreateDrawableState(extraSpace + mListDrawableState.mStateSet.length);
mergeDrawableStates(drawableState, mListDrawableState.mStateSet);
return drawableState;
}
/** Verifies that the current icon is high-res otherwise posts a request to load the icon. */
public void verifyHighRes() {
if (mIconLoadRequest != null) {
mIconLoadRequest.cancel();
mIconLoadRequest = null;
}
if (getTag() instanceof ItemInfoWithIcon) {
ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
if (info.usingLowResIcon()) {
mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
.updateIconInBackground(this, info);
}
}
}
/** A listener for the widget section expansion / collapse events. */
public interface OnExpansionChangeListener {
/** Notifies that the widget section is expanded or collapsed. */
void onExpansionChange(boolean isExpanded);
}
}