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