1 /* 2 * Copyright (C) 2015 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.systemui.statusbar.notification.row.wrapper; 18 19 import static com.android.systemui.statusbar.notification.TransformState.TRANSFORM_Y; 20 21 import android.app.Notification; 22 import android.content.Context; 23 import android.util.ArraySet; 24 import android.view.NotificationHeaderView; 25 import android.view.NotificationTopLineView; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.view.animation.Interpolator; 29 import android.view.animation.PathInterpolator; 30 import android.widget.DateTimeView; 31 import android.widget.ImageButton; 32 import android.widget.ImageView; 33 import android.widget.TextView; 34 35 import androidx.annotation.Nullable; 36 37 import com.android.app.animation.Interpolators; 38 import com.android.internal.widget.CachingIconView; 39 import com.android.internal.widget.NotificationExpandButton; 40 import com.android.systemui.R; 41 import com.android.systemui.statusbar.TransformableView; 42 import com.android.systemui.statusbar.ViewTransformationHelper; 43 import com.android.systemui.statusbar.notification.CustomInterpolatorTransformation; 44 import com.android.systemui.statusbar.notification.FeedbackIcon; 45 import com.android.systemui.statusbar.notification.ImageTransformState; 46 import com.android.systemui.statusbar.notification.Roundable; 47 import com.android.systemui.statusbar.notification.RoundableState; 48 import com.android.systemui.statusbar.notification.TransformState; 49 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 50 51 import java.util.Stack; 52 53 /** 54 * Wraps a notification view which may or may not include a header. 55 */ 56 public class NotificationHeaderViewWrapper extends NotificationViewWrapper implements Roundable { 57 58 private final RoundableState mRoundableState; 59 private static final Interpolator LOW_PRIORITY_HEADER_CLOSE 60 = new PathInterpolator(0.4f, 0f, 0.7f, 1f); 61 protected final ViewTransformationHelper mTransformationHelper; 62 private CachingIconView mIcon; 63 private NotificationExpandButton mExpandButton; 64 private View mAltExpandTarget; 65 private View mIconContainer; 66 protected NotificationHeaderView mNotificationHeader; 67 protected NotificationTopLineView mNotificationTopLine; 68 private TextView mHeaderText; 69 private TextView mAppNameText; 70 private ImageView mWorkProfileImage; 71 private View mAudiblyAlertedIcon; 72 private View mFeedbackIcon; 73 private boolean mIsLowPriority; 74 private boolean mTransformLowPriorityTitle; 75 private RoundnessChangedListener mRoundnessChangedListener; 76 NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row)77 protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) { 78 super(ctx, view, row); 79 mRoundableState = new RoundableState( 80 mView, 81 this, 82 ctx.getResources().getDimension(R.dimen.notification_corner_radius) 83 ); 84 mTransformationHelper = new ViewTransformationHelper(); 85 86 // we want to avoid that the header clashes with the other text when transforming 87 // low-priority 88 mTransformationHelper.setCustomTransformation( 89 new CustomInterpolatorTransformation(TRANSFORMING_VIEW_TITLE) { 90 91 @Override 92 public Interpolator getCustomInterpolator( 93 int interpolationType, 94 boolean isFrom) { 95 boolean isLowPriority = mView instanceof NotificationHeaderView; 96 if (interpolationType == TRANSFORM_Y) { 97 if (isLowPriority && !isFrom 98 || !isLowPriority && isFrom) { 99 return Interpolators.LINEAR_OUT_SLOW_IN; 100 } else { 101 return LOW_PRIORITY_HEADER_CLOSE; 102 } 103 } 104 return null; 105 } 106 107 @Override 108 protected boolean hasCustomTransformation() { 109 return mIsLowPriority && mTransformLowPriorityTitle; 110 } 111 }, 112 TRANSFORMING_VIEW_TITLE); 113 resolveHeaderViews(); 114 addFeedbackOnClickListener(row); 115 } 116 117 @Override getRoundableState()118 public RoundableState getRoundableState() { 119 return mRoundableState; 120 } 121 122 @Override getClipHeight()123 public int getClipHeight() { 124 return mView.getHeight(); 125 } 126 127 @Override applyRoundnessAndInvalidate()128 public void applyRoundnessAndInvalidate() { 129 if (mRoundnessChangedListener != null) { 130 // We cannot apply the rounded corner to this View, so our parents (in drawChild()) will 131 // clip our canvas. So we should invalidate our parent. 132 mRoundnessChangedListener.applyRoundnessAndInvalidate(); 133 } 134 Roundable.super.applyRoundnessAndInvalidate(); 135 } 136 setOnRoundnessChangedListener(RoundnessChangedListener listener)137 public void setOnRoundnessChangedListener(RoundnessChangedListener listener) { 138 mRoundnessChangedListener = listener; 139 } 140 resolveHeaderViews()141 protected void resolveHeaderViews() { 142 mIcon = mView.findViewById(com.android.internal.R.id.icon); 143 mHeaderText = mView.findViewById(com.android.internal.R.id.header_text); 144 mAppNameText = mView.findViewById(com.android.internal.R.id.app_name_text); 145 mExpandButton = mView.findViewById(com.android.internal.R.id.expand_button); 146 mAltExpandTarget = mView.findViewById(com.android.internal.R.id.alternate_expand_target); 147 mIconContainer = mView.findViewById(com.android.internal.R.id.conversation_icon_container); 148 mWorkProfileImage = mView.findViewById(com.android.internal.R.id.profile_badge); 149 mNotificationHeader = mView.findViewById(com.android.internal.R.id.notification_header); 150 mNotificationTopLine = mView.findViewById(com.android.internal.R.id.notification_top_line); 151 mAudiblyAlertedIcon = mView.findViewById(com.android.internal.R.id.alerted_icon); 152 mFeedbackIcon = mView.findViewById(com.android.internal.R.id.feedback); 153 } 154 addFeedbackOnClickListener(ExpandableNotificationRow row)155 private void addFeedbackOnClickListener(ExpandableNotificationRow row) { 156 View.OnClickListener listener = row.getFeedbackOnClickListener(); 157 if (mNotificationTopLine != null) { 158 mNotificationTopLine.setFeedbackOnClickListener(listener); 159 } 160 if (mFeedbackIcon != null) { 161 mFeedbackIcon.setOnClickListener(listener); 162 } 163 } 164 165 /** 166 * Shows the given feedback icon, or hides the icon if null. 167 */ 168 @Override setFeedbackIcon(@ullable FeedbackIcon icon)169 public void setFeedbackIcon(@Nullable FeedbackIcon icon) { 170 if (mFeedbackIcon != null) { 171 mFeedbackIcon.setVisibility(icon != null ? View.VISIBLE : View.GONE); 172 if (icon != null) { 173 if (mFeedbackIcon instanceof ImageButton) { 174 ((ImageButton) mFeedbackIcon).setImageResource(icon.getIconRes()); 175 } 176 mFeedbackIcon.setContentDescription( 177 mView.getContext().getString(icon.getContentDescRes())); 178 } 179 } 180 } 181 182 @Override onContentUpdated(ExpandableNotificationRow row)183 public void onContentUpdated(ExpandableNotificationRow row) { 184 super.onContentUpdated(row); 185 mIsLowPriority = row.getEntry().isAmbient(); 186 mTransformLowPriorityTitle = !row.isChildInGroup() && !row.isSummaryWithChildren(); 187 ArraySet<View> previousViews = mTransformationHelper.getAllTransformingViews(); 188 189 // Reinspect the notification. 190 resolveHeaderViews(); 191 updateTransformedTypes(); 192 addRemainingTransformTypes(); 193 updateCropToPaddingForImageViews(); 194 Notification notification = row.getEntry().getSbn().getNotification(); 195 mIcon.setTag(ImageTransformState.ICON_TAG, notification.getSmallIcon()); 196 197 // We need to reset all views that are no longer transforming in case a view was previously 198 // transformed, but now we decided to transform its container instead. 199 ArraySet<View> currentViews = mTransformationHelper.getAllTransformingViews(); 200 for (int i = 0; i < previousViews.size(); i++) { 201 View view = previousViews.valueAt(i); 202 if (!currentViews.contains(view)) { 203 mTransformationHelper.resetTransformedView(view); 204 } 205 } 206 } 207 208 /** 209 * Adds the remaining TransformTypes to the TransformHelper. This is done to make sure that each 210 * child is faded automatically and doesn't have to be manually added. 211 * The keys used for the views are the ids. 212 */ addRemainingTransformTypes()213 private void addRemainingTransformTypes() { 214 mTransformationHelper.addRemainingTransformTypes(mView); 215 } 216 217 /** 218 * Since we are deactivating the clipping when transforming the ImageViews don't get clipped 219 * anymore during these transitions. We can avoid that by using 220 * {@link ImageView#setCropToPadding(boolean)} on all ImageViews. 221 */ updateCropToPaddingForImageViews()222 private void updateCropToPaddingForImageViews() { 223 Stack<View> stack = new Stack<>(); 224 stack.push(mView); 225 while (!stack.isEmpty()) { 226 View child = stack.pop(); 227 if (child instanceof ImageView 228 // Skip the importance ring for conversations, disabled cropping is needed for 229 // its animation 230 && child.getId() != com.android.internal.R.id.conversation_icon_badge_ring) { 231 ((ImageView) child).setCropToPadding(true); 232 } else if (child instanceof ViewGroup) { 233 ViewGroup group = (ViewGroup) child; 234 for (int i = 0; i < group.getChildCount(); i++) { 235 stack.push(group.getChildAt(i)); 236 } 237 } 238 } 239 } 240 updateTransformedTypes()241 protected void updateTransformedTypes() { 242 mTransformationHelper.reset(); 243 mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ICON, mIcon); 244 mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_EXPANDER, 245 mExpandButton); 246 if (mIsLowPriority && mHeaderText != null) { 247 mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE, 248 mHeaderText); 249 } 250 addViewsTransformingToSimilar(mWorkProfileImage, mAudiblyAlertedIcon, mFeedbackIcon); 251 } 252 253 @Override updateExpandability( boolean expandable, View.OnClickListener onClickListener, boolean requestLayout)254 public void updateExpandability( 255 boolean expandable, 256 View.OnClickListener onClickListener, 257 boolean requestLayout) { 258 mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE); 259 mExpandButton.setOnClickListener(expandable ? onClickListener : null); 260 if (mAltExpandTarget != null) { 261 mAltExpandTarget.setOnClickListener(expandable ? onClickListener : null); 262 } 263 if (mIconContainer != null) { 264 mIconContainer.setOnClickListener(expandable ? onClickListener : null); 265 } 266 if (mNotificationHeader != null) { 267 mNotificationHeader.setOnClickListener(expandable ? onClickListener : null); 268 } 269 // Unfortunately, the NotificationContentView has to layout its children in order to 270 // determine their heights, and that affects the button visibility. If that happens 271 // (thankfully it is rare) then we need to request layout of the expand button's parent 272 // in order to ensure it gets laid out correctly. 273 if (requestLayout) { 274 mExpandButton.getParent().requestLayout(); 275 } 276 } 277 278 @Override setExpanded(boolean expanded)279 public void setExpanded(boolean expanded) { 280 mExpandButton.setExpanded(expanded); 281 } 282 283 @Override setRecentlyAudiblyAlerted(boolean audiblyAlerted)284 public void setRecentlyAudiblyAlerted(boolean audiblyAlerted) { 285 if (mAudiblyAlertedIcon != null) { 286 mAudiblyAlertedIcon.setVisibility(audiblyAlerted ? View.VISIBLE : View.GONE); 287 } 288 } 289 290 @Override getNotificationHeader()291 public NotificationHeaderView getNotificationHeader() { 292 return mNotificationHeader; 293 } 294 295 @Override getExpandButton()296 public View getExpandButton() { 297 return mExpandButton; 298 } 299 300 @Override getIcon()301 public CachingIconView getIcon() { 302 return mIcon; 303 } 304 305 @Override getOriginalIconColor()306 public int getOriginalIconColor() { 307 return mIcon.getOriginalIconColor(); 308 } 309 310 @Override getShelfTransformationTarget()311 public View getShelfTransformationTarget() { 312 return mIcon; 313 } 314 315 @Override getCurrentState(int fadingView)316 public TransformState getCurrentState(int fadingView) { 317 return mTransformationHelper.getCurrentState(fadingView); 318 } 319 320 @Override transformTo(TransformableView notification, Runnable endRunnable)321 public void transformTo(TransformableView notification, Runnable endRunnable) { 322 mTransformationHelper.transformTo(notification, endRunnable); 323 } 324 325 @Override transformTo(TransformableView notification, float transformationAmount)326 public void transformTo(TransformableView notification, float transformationAmount) { 327 mTransformationHelper.transformTo(notification, transformationAmount); 328 } 329 330 @Override transformFrom(TransformableView notification)331 public void transformFrom(TransformableView notification) { 332 mTransformationHelper.transformFrom(notification); 333 } 334 335 @Override transformFrom(TransformableView notification, float transformationAmount)336 public void transformFrom(TransformableView notification, float transformationAmount) { 337 mTransformationHelper.transformFrom(notification, transformationAmount); 338 } 339 340 @Override setIsChildInGroup(boolean isChildInGroup)341 public void setIsChildInGroup(boolean isChildInGroup) { 342 super.setIsChildInGroup(isChildInGroup); 343 mTransformLowPriorityTitle = !isChildInGroup; 344 } 345 346 @Override setVisible(boolean visible)347 public void setVisible(boolean visible) { 348 super.setVisible(visible); 349 mTransformationHelper.setVisible(visible); 350 } 351 352 /*** 353 * Set Notification when value 354 * @param whenMillis 355 */ setNotificationWhen(long whenMillis)356 public void setNotificationWhen(long whenMillis) { 357 if (mNotificationHeader == null) { 358 return; 359 } 360 361 final View timeView = mNotificationHeader.findViewById(com.android.internal.R.id.time); 362 363 if (timeView instanceof DateTimeView) { 364 ((DateTimeView) timeView).setTime(whenMillis); 365 } 366 } addTransformedViews(View... views)367 protected void addTransformedViews(View... views) { 368 for (View view : views) { 369 if (view != null) { 370 mTransformationHelper.addTransformedView(view); 371 } 372 } 373 } 374 addViewsTransformingToSimilar(View... views)375 protected void addViewsTransformingToSimilar(View... views) { 376 for (View view : views) { 377 if (view != null) { 378 mTransformationHelper.addViewTransformingToSimilar(view); 379 } 380 } 381 } 382 383 /** 384 * Interface that handle the Roundness changes 385 */ 386 public interface RoundnessChangedListener { 387 /** 388 * This method will be called when this class call applyRoundnessAndInvalidate() 389 */ applyRoundnessAndInvalidate()390 void applyRoundnessAndInvalidate(); 391 } 392 } 393