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