1 /* 2 * Copyright (C) 2017 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 17 package com.android.launcher3.notification; 18 19 import static com.android.launcher3.Utilities.mapToRange; 20 import static com.android.launcher3.anim.Interpolators.LINEAR; 21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DISMISSED; 22 23 import android.animation.AnimatorSet; 24 import android.animation.ValueAnimator; 25 import android.annotation.TargetApi; 26 import android.content.Context; 27 import android.graphics.Outline; 28 import android.graphics.Rect; 29 import android.graphics.drawable.GradientDrawable; 30 import android.os.Build; 31 import android.text.TextUtils; 32 import android.util.AttributeSet; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.ViewOutlineProvider; 36 import android.widget.LinearLayout; 37 import android.widget.TextView; 38 39 import androidx.annotation.Nullable; 40 41 import com.android.launcher3.R; 42 import com.android.launcher3.Utilities; 43 import com.android.launcher3.model.data.ItemInfo; 44 import com.android.launcher3.popup.PopupDataProvider; 45 import com.android.launcher3.util.Themes; 46 import com.android.launcher3.views.ActivityContext; 47 48 /** 49 * A {@link android.widget.FrameLayout} that contains a single notification, 50 * e.g. icon + title + text. 51 */ 52 @TargetApi(Build.VERSION_CODES.N) 53 public class NotificationMainView extends LinearLayout { 54 55 // This is used only to track the notification view, so that it can be properly logged. 56 public static final ItemInfo NOTIFICATION_ITEM_INFO = new ItemInfo(); 57 58 // Value when the primary notification main view will be gone (zero alpha). 59 private static final float PRIMARY_GONE_PROGRESS = 0.7f; 60 private static final float PRIMARY_MIN_PROGRESS = 0.40f; 61 private static final float PRIMARY_MAX_PROGRESS = 0.60f; 62 private static final float SECONDARY_MIN_PROGRESS = 0.30f; 63 private static final float SECONDARY_MAX_PROGRESS = 0.50f; 64 private static final float SECONDARY_CONTENT_MAX_PROGRESS = 0.6f; 65 66 private NotificationInfo mNotificationInfo; 67 private int mBackgroundColor; 68 private TextView mTitleView; 69 private TextView mTextView; 70 private View mIconView; 71 72 private View mHeader; 73 private View mMainView; 74 75 private TextView mHeaderCount; 76 private final Rect mOutline = new Rect(); 77 78 // Space between notifications during swipe 79 private final int mNotificationSpace; 80 private final int mMaxTransX; 81 private final int mMaxElevation; 82 83 private final GradientDrawable mBackground; 84 NotificationMainView(Context context)85 public NotificationMainView(Context context) { 86 this(context, null, 0); 87 } 88 NotificationMainView(Context context, AttributeSet attrs)89 public NotificationMainView(Context context, AttributeSet attrs) { 90 this(context, attrs, 0); 91 } 92 NotificationMainView(Context context, AttributeSet attrs, int defStyle)93 public NotificationMainView(Context context, AttributeSet attrs, int defStyle) { 94 this(context, attrs, defStyle, 0); 95 } 96 NotificationMainView(Context context, AttributeSet attrs, int defStyle, int defStylRes)97 public NotificationMainView(Context context, AttributeSet attrs, int defStyle, int defStylRes) { 98 super(context, attrs, defStyle, defStylRes); 99 100 float outlineRadius = Themes.getDialogCornerRadius(context); 101 102 mBackground = new GradientDrawable(); 103 mBackground.setColor(Themes.getAttrColor(context, R.attr.popupColorPrimary)); 104 mBackground.setCornerRadius(outlineRadius); 105 setBackground(mBackground); 106 107 mMaxElevation = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_elevation); 108 setElevation(mMaxElevation); 109 110 mMaxTransX = getResources().getDimensionPixelSize(R.dimen.notification_max_trans); 111 mNotificationSpace = getResources().getDimensionPixelSize(R.dimen.notification_space); 112 113 setClipToOutline(true); 114 setOutlineProvider(new ViewOutlineProvider() { 115 @Override 116 public void getOutline(View view, Outline outline) { 117 outline.setRoundRect(mOutline, outlineRadius); 118 } 119 }); 120 } 121 122 /** 123 * Updates the header text. 124 * @param notificationCount The number of notifications. 125 */ updateHeader(int notificationCount)126 public void updateHeader(int notificationCount) { 127 final String text; 128 final int visibility; 129 if (notificationCount <= 1) { 130 text = ""; 131 visibility = View.INVISIBLE; 132 } else { 133 text = String.valueOf(notificationCount); 134 visibility = View.VISIBLE; 135 136 } 137 mHeaderCount.setText(text); 138 mHeaderCount.setVisibility(visibility); 139 } 140 141 @Override onFinishInflate()142 protected void onFinishInflate() { 143 super.onFinishInflate(); 144 145 ViewGroup textAndBackground = findViewById(R.id.text_and_background); 146 mTitleView = textAndBackground.findViewById(R.id.title); 147 mTextView = textAndBackground.findViewById(R.id.text); 148 mIconView = findViewById(R.id.popup_item_icon); 149 mHeaderCount = findViewById(R.id.notification_count); 150 151 mHeader = findViewById(R.id.header); 152 mMainView = findViewById(R.id.main_view); 153 } 154 155 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)156 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 157 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 158 mOutline.set(0, 0, getWidth(), getHeight()); 159 invalidateOutline(); 160 } 161 updateBackgroundColor(int color)162 private void updateBackgroundColor(int color) { 163 mBackgroundColor = color; 164 mBackground.setColor(color); 165 if (mNotificationInfo != null) { 166 mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(), 167 mBackgroundColor)); 168 } 169 } 170 171 /** 172 * Animates the background color to a new color. 173 * @param color The color to change to. 174 * @param animatorSetOut The AnimatorSet where we add the color animator to. 175 */ updateBackgroundColor(int color, AnimatorSet animatorSetOut)176 public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) { 177 int oldColor = mBackgroundColor; 178 ValueAnimator colors = ValueAnimator.ofArgb(oldColor, color); 179 colors.addUpdateListener(valueAnimator -> { 180 int newColor = (int) valueAnimator.getAnimatedValue(); 181 updateBackgroundColor(newColor); 182 }); 183 animatorSetOut.play(colors); 184 } 185 186 /** 187 * Sets the content of this view, animating it after a new icon shifts up if necessary. 188 */ applyNotificationInfo(NotificationInfo notificationInfo)189 public void applyNotificationInfo(NotificationInfo notificationInfo) { 190 mNotificationInfo = notificationInfo; 191 if (notificationInfo == null) { 192 return; 193 } 194 NotificationListener listener = NotificationListener.getInstanceIfConnected(); 195 if (listener != null) { 196 listener.setNotificationsShown(new String[] {mNotificationInfo.notificationKey}); 197 } 198 CharSequence title = mNotificationInfo.title; 199 CharSequence text = mNotificationInfo.text; 200 if (!TextUtils.isEmpty(title) && !TextUtils.isEmpty(text)) { 201 mTitleView.setText(title.toString()); 202 mTextView.setText(text.toString()); 203 } else { 204 mTitleView.setMaxLines(2); 205 mTitleView.setText(TextUtils.isEmpty(title) ? text.toString() : title.toString()); 206 mTextView.setVisibility(GONE); 207 } 208 mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(), 209 mBackgroundColor)); 210 if (mNotificationInfo.intent != null) { 211 setOnClickListener(mNotificationInfo); 212 } 213 214 // Add a stub ItemInfo so that logging populates the correct container and item types 215 // instead of DEFAULT_CONTAINERTYPE and DEFAULT_ITEMTYPE, respectively. 216 setTag(NOTIFICATION_ITEM_INFO); 217 } 218 219 /** 220 * Sets the alpha of only the child views. 221 */ setContentAlpha(float alpha)222 public void setContentAlpha(float alpha) { 223 mHeader.setAlpha(alpha); 224 mMainView.setAlpha(alpha); 225 } 226 227 /** 228 * Sets the translation of only the child views. 229 */ setContentTranslationX(float transX)230 public void setContentTranslationX(float transX) { 231 mHeader.setTranslationX(transX); 232 mMainView.setTranslationX(transX); 233 } 234 235 /** 236 * Updates the alpha, content alpha, and elevation of this view. 237 * 238 * @param progress Range from [0, 1] or [-1, 0] 239 * When 0: Full alpha 240 * When 1/-1: zero alpha 241 */ onPrimaryDrag(float progress)242 public void onPrimaryDrag(float progress) { 243 float absProgress = Math.abs(progress); 244 final int width = getWidth(); 245 246 float min = PRIMARY_MIN_PROGRESS; 247 float max = PRIMARY_MAX_PROGRESS; 248 249 if (absProgress < min) { 250 setAlpha(1f); 251 setContentAlpha(1); 252 setElevation(mMaxElevation); 253 } else if (absProgress < max) { 254 setAlpha(1f); 255 setContentAlpha(mapToRange(absProgress, min, max, 1f, 0f, LINEAR)); 256 setElevation(Utilities.mapToRange(absProgress, min, max, mMaxElevation, 0, LINEAR)); 257 } else { 258 setAlpha(mapToRange(absProgress, max, PRIMARY_GONE_PROGRESS, 1f, 0f, LINEAR)); 259 setContentAlpha(0f); 260 setElevation(0f); 261 } 262 263 setTranslationX(width * progress); 264 } 265 266 /** 267 * Updates the alpha, content alpha, elevation, and clipping of this view. 268 * @param progress Range from [0, 1] or [-1, 0] 269 * When 0: Smallest clipping, zero alpha 270 * When 1/-1: Full clip, full alpha 271 */ onSecondaryDrag(float progress)272 public void onSecondaryDrag(float progress) { 273 final float absProgress = Math.abs(progress); 274 275 float min = SECONDARY_MIN_PROGRESS; 276 float max = SECONDARY_MAX_PROGRESS; 277 float contentMax = SECONDARY_CONTENT_MAX_PROGRESS; 278 279 if (absProgress < min) { 280 setAlpha(0f); 281 setContentAlpha(0); 282 setElevation(0f); 283 } else if (absProgress < max) { 284 setAlpha(mapToRange(absProgress, min, max, 0, 1f, LINEAR)); 285 setContentAlpha(0f); 286 setElevation(0f); 287 } else { 288 setAlpha(1f); 289 setContentAlpha(absProgress > contentMax 290 ? 1f 291 : mapToRange(absProgress, max, contentMax, 0, 1f, LINEAR)); 292 setElevation(Utilities.mapToRange(absProgress, max, 1, 0, mMaxElevation, LINEAR)); 293 } 294 295 final int width = getWidth(); 296 int crop = (int) (width * absProgress); 297 int space = (int) (absProgress > PRIMARY_GONE_PROGRESS 298 ? mapToRange(absProgress, PRIMARY_GONE_PROGRESS, 1f, mNotificationSpace, 0, LINEAR) 299 : mNotificationSpace); 300 if (progress < 0) { 301 mOutline.left = Math.max(0, getWidth() - crop + space); 302 mOutline.right = getWidth(); 303 } else { 304 mOutline.right = Math.min(getWidth(), crop - space); 305 mOutline.left = 0; 306 } 307 308 float contentTransX = mMaxTransX * (1f - absProgress); 309 setContentTranslationX(progress < 0 310 ? contentTransX 311 : -contentTransX); 312 invalidateOutline(); 313 } 314 315 public @Nullable NotificationInfo getNotificationInfo() { 316 return mNotificationInfo; 317 } 318 319 public boolean canChildBeDismissed() { 320 return mNotificationInfo != null && mNotificationInfo.dismissable; 321 } 322 323 public void onChildDismissed() { 324 ActivityContext activityContext = ActivityContext.lookupContext(getContext()); 325 PopupDataProvider popupDataProvider = activityContext.getPopupDataProvider(); 326 if (popupDataProvider == null) { 327 return; 328 } 329 popupDataProvider.cancelNotification(mNotificationInfo.notificationKey); 330 activityContext.getStatsLogManager().logger().log(LAUNCHER_NOTIFICATION_DISMISSED); 331 } 332 } 333