1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.widget.picker; 17 18 import static android.animation.ValueAnimator.areAnimatorsEnabled; 19 20 import android.content.Context; 21 import android.content.res.TypedArray; 22 import android.graphics.drawable.Drawable; 23 import android.os.Bundle; 24 import android.text.TextUtils; 25 import android.util.AttributeSet; 26 import android.view.View; 27 import android.view.accessibility.AccessibilityNodeInfo; 28 import android.widget.ImageView; 29 import android.widget.LinearLayout; 30 import android.widget.TextView; 31 32 import androidx.annotation.Nullable; 33 import androidx.annotation.UiThread; 34 35 import com.android.launcher3.DeviceProfile; 36 import com.android.launcher3.LauncherAppState; 37 import com.android.launcher3.R; 38 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; 39 import com.android.launcher3.icons.PlaceHolderIconDrawable; 40 import com.android.launcher3.model.data.ItemInfoWithIcon; 41 import com.android.launcher3.model.data.PackageItemInfo; 42 import com.android.launcher3.util.CancellableTask; 43 import com.android.launcher3.views.ActivityContext; 44 import com.android.launcher3.widget.model.WidgetsListHeaderEntry; 45 46 /** 47 * A UI represents a header of an app shown in the full widgets tray. 48 * 49 * It is a {@link LinearLayout} which contains an app icon, an app name, a subtitle and a checkbox 50 * which indicates if the widgets content view underneath this header should be shown. 51 */ 52 public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpdateReceiver { 53 54 private static final int[] EXPANDED_DRAWABLE_STATE = new int[] {android.R.attr.state_expanded}; 55 56 private final int mIconSize; 57 /** 58 * Indicates if the header is collapsable. For example, when displayed in a two pane layout, 59 * widget apps aren't collapsable. 60 */ 61 private final boolean mIsCollapsable; 62 @Nullable private CancellableTask mIconLoadRequest; 63 @Nullable private Drawable mIconDrawable; 64 @Nullable private WidgetsListDrawableState mListDrawableState; 65 private ImageView mAppIcon; 66 private TextView mTitle; 67 private TextView mSubtitle; 68 private boolean mEnableIconUpdateAnimation = false; 69 private boolean mIsExpanded = false; 70 WidgetsListHeader(Context context)71 public WidgetsListHeader(Context context) { 72 this(context, /* attrs= */ null); 73 } 74 WidgetsListHeader(Context context, @Nullable AttributeSet attrs)75 public WidgetsListHeader(Context context, @Nullable AttributeSet attrs) { 76 this(context, attrs, /* defStyle= */ 0); 77 } 78 WidgetsListHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr)79 public WidgetsListHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 80 super(context, attrs, defStyleAttr); 81 82 ActivityContext activity = ActivityContext.lookupContext(context); 83 DeviceProfile grid = activity.getDeviceProfile(); 84 TypedArray a = context.obtainStyledAttributes(attrs, 85 R.styleable.WidgetsListRowHeader, defStyleAttr, /* defStyleRes= */ 0); 86 mIconSize = a.getDimensionPixelSize(R.styleable.WidgetsListRowHeader_appIconSize, 87 grid.iconSizePx); 88 mIsCollapsable = a.getBoolean(R.styleable.WidgetsListRowHeader_collapsable, true); 89 } 90 91 @Override onFinishInflate()92 protected void onFinishInflate() { 93 super.onFinishInflate(); 94 mAppIcon = findViewById(R.id.app_icon); 95 mTitle = findViewById(R.id.app_title); 96 mSubtitle = findViewById(R.id.app_subtitle); 97 // Lists that cannot collapse, don't need EXPAND or COLLAPSE accessibility actions. 98 if (mIsCollapsable) { 99 setAccessibilityDelegate(new AccessibilityDelegate() { 100 101 @Override 102 public void onInitializeAccessibilityNodeInfo(View host, 103 AccessibilityNodeInfo info) { 104 if (mIsExpanded) { 105 info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND); 106 info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE); 107 } else { 108 info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE); 109 info.addAction(AccessibilityNodeInfo.ACTION_EXPAND); 110 } 111 super.onInitializeAccessibilityNodeInfo(host, info); 112 } 113 114 @Override 115 public boolean performAccessibilityAction(View host, int action, Bundle args) { 116 switch (action) { 117 case AccessibilityNodeInfo.ACTION_EXPAND: 118 case AccessibilityNodeInfo.ACTION_COLLAPSE: 119 callOnClick(); 120 return true; 121 default: 122 return super.performAccessibilityAction(host, action, args); 123 } 124 } 125 }); 126 } 127 } 128 129 /** Sets the expand toggle to expand / collapse. */ 130 @UiThread setExpanded(boolean isExpanded)131 public void setExpanded(boolean isExpanded) { 132 this.mIsExpanded = isExpanded; 133 refreshDrawableState(); 134 } 135 136 /** @return true if this header is expanded. */ isExpanded()137 public boolean isExpanded() { 138 return mIsExpanded; 139 } 140 141 /** Sets the {@link WidgetsListDrawableState} and refreshes the background drawable. */ 142 @UiThread setListDrawableState(WidgetsListDrawableState state)143 public void setListDrawableState(WidgetsListDrawableState state) { 144 if (state == mListDrawableState) return; 145 this.mListDrawableState = state; 146 refreshDrawableState(); 147 } 148 149 /** Apply app icon, labels and tag using a generic {@link WidgetsListHeaderEntry}. */ 150 @UiThread applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry)151 public void applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry) { 152 PackageItemInfo info = entry.mPkgItem; 153 setIcon(info.newIcon(getContext())); 154 setTitles(entry); 155 setExpanded(entry.isWidgetListShown()); 156 157 super.setTag(info); 158 159 verifyHighRes(); 160 } 161 setIcon(Drawable icon)162 void setIcon(Drawable icon) { 163 applyDrawables(icon); 164 mIconDrawable = icon; 165 if (mIconDrawable != null) { 166 mIconDrawable.setVisible( 167 /* visible= */ getWindowVisibility() == VISIBLE && isShown(), 168 /* restart= */ false); 169 } 170 } 171 applyDrawables(Drawable icon)172 private void applyDrawables(Drawable icon) { 173 icon.setBounds(0, 0, mIconSize, mIconSize); 174 175 LinearLayout.LayoutParams layoutParams = 176 (LinearLayout.LayoutParams) mAppIcon.getLayoutParams(); 177 layoutParams.width = mIconSize; 178 layoutParams.height = mIconSize; 179 mAppIcon.setLayoutParams(layoutParams); 180 mAppIcon.setImageDrawable(icon); 181 182 // If the current icon is a placeholder color, animate its update. 183 if (mIconDrawable != null 184 && mIconDrawable instanceof PlaceHolderIconDrawable 185 && mEnableIconUpdateAnimation) { 186 ((PlaceHolderIconDrawable) mIconDrawable).animateIconUpdate(icon); 187 } 188 } 189 setTitles(WidgetsListHeaderEntry entry)190 private void setTitles(WidgetsListHeaderEntry entry) { 191 mTitle.setText(entry.mPkgItem.title); 192 193 String subtitle = entry.getSubtitle(getContext()); 194 if (TextUtils.isEmpty(subtitle)) { 195 mSubtitle.setVisibility(GONE); 196 } else { 197 mSubtitle.setText(subtitle); 198 mSubtitle.setVisibility(VISIBLE); 199 } 200 } 201 202 @Override reapplyItemInfo(ItemInfoWithIcon info)203 public void reapplyItemInfo(ItemInfoWithIcon info) { 204 if (getTag() == info) { 205 mIconLoadRequest = null; 206 mEnableIconUpdateAnimation = areAnimatorsEnabled(); 207 208 // Optimization: Starting in N, pre-uploads the bitmap to RenderThread. 209 info.bitmap.icon.prepareToDraw(); 210 211 setIcon(info.newIcon(getContext())); 212 213 mEnableIconUpdateAnimation = false; 214 } 215 } 216 217 @Override onCreateDrawableState(int extraSpace)218 protected int[] onCreateDrawableState(int extraSpace) { 219 // We create a drawable state with an additional two spaces to be able to fit expanded state 220 // and the list drawable state. 221 int[] drawableState = super.onCreateDrawableState(extraSpace + 2); 222 if (mIsExpanded) { 223 mergeDrawableStates(drawableState, EXPANDED_DRAWABLE_STATE); 224 } 225 if (mListDrawableState != null) { 226 mergeDrawableStates(drawableState, mListDrawableState.mStateSet); 227 } 228 return drawableState; 229 } 230 231 /** Verifies that the current icon is high-res otherwise posts a request to load the icon. */ verifyHighRes()232 public void verifyHighRes() { 233 if (mIconLoadRequest != null) { 234 mIconLoadRequest.cancel(); 235 mIconLoadRequest = null; 236 } 237 if (getTag() instanceof ItemInfoWithIcon) { 238 ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); 239 if (info.usingLowResIcon()) { 240 mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache() 241 .updateIconInBackground(this, info); 242 } 243 } 244 } 245 } 246