1 /* 2 * Copyright (C) 2014 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; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.content.Context; 22 import android.util.AttributeSet; 23 import android.view.View; 24 import android.view.animation.Interpolator; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 import com.android.systemui.animation.Interpolators; 28 29 import java.util.function.Consumer; 30 31 /** 32 * A common base class for all views in the notification stack scroller which don't have a 33 * background. 34 */ 35 public abstract class StackScrollerDecorView extends ExpandableView { 36 37 protected View mContent; 38 protected View mSecondaryView; 39 private boolean mIsVisible = true; 40 private boolean mContentVisible = true; 41 private boolean mIsSecondaryVisible = true; 42 private int mDuration = 260; 43 private boolean mContentAnimating; 44 private final Runnable mContentVisibilityEndRunnable = () -> { 45 mContentAnimating = false; 46 if (getVisibility() != View.GONE && !mIsVisible) { 47 setVisibility(GONE); 48 setWillBeGone(false); 49 notifyHeightChanged(false /* needsAnimation */); 50 } 51 }; 52 53 private boolean mSecondaryAnimating = false; 54 private final Consumer<Boolean> mSecondaryVisibilityEndRunnable = (cancelled) -> { 55 mSecondaryAnimating = false; 56 // If we were on screen, become GONE to avoid touches 57 if (mSecondaryView == null) return; 58 if (getVisibility() != View.GONE 59 && mSecondaryView.getVisibility() != View.GONE 60 && !mIsSecondaryVisible) { 61 mSecondaryView.setVisibility(View.GONE); 62 } 63 }; 64 StackScrollerDecorView(Context context, AttributeSet attrs)65 public StackScrollerDecorView(Context context, AttributeSet attrs) { 66 super(context, attrs); 67 setClipChildren(false); 68 } 69 70 @Override onFinishInflate()71 protected void onFinishInflate() { 72 super.onFinishInflate(); 73 mContent = findContentView(); 74 mSecondaryView = findSecondaryView(); 75 setVisible(false /* nowVisible */, false /* animate */); 76 setSecondaryVisible(false /* nowVisible */, false /* animate */); 77 setOutlineProvider(null); 78 } 79 80 @Override isTransparent()81 public boolean isTransparent() { 82 return true; 83 } 84 85 /** 86 * @param visible True if we should animate contents visible 87 */ setContentVisible(boolean visible)88 public void setContentVisible(boolean visible) { 89 setContentVisible(visible, true /* animate */, null /* runAfter */); 90 } 91 92 /** 93 * @param visible True if the contents should be visible 94 * @param animate True if we should fade to new visibility 95 * @param runAfter Runnable to run after visibility updates 96 */ setContentVisible(boolean visible, boolean animate, Consumer<Boolean> runAfter)97 public void setContentVisible(boolean visible, boolean animate, Consumer<Boolean> runAfter) { 98 if (mContentVisible != visible) { 99 mContentAnimating = animate; 100 mContentVisible = visible; 101 Consumer<Boolean> endRunnable = (cancelled) -> { 102 mContentVisibilityEndRunnable.run(); 103 if (runAfter != null) { 104 runAfter.accept(cancelled); 105 } 106 }; 107 setViewVisible(mContent, visible, animate, endRunnable); 108 } else if (runAfter != null) { 109 // Execute the runAfter runnable immediately if there's no animation to perform. 110 runAfter.accept(true); 111 } 112 113 if (!mContentAnimating) { 114 mContentVisibilityEndRunnable.run(); 115 } 116 } 117 isContentVisible()118 public boolean isContentVisible() { 119 return mContentVisible; 120 } 121 122 /** 123 * Make this view visible. If {@code false} is passed, the view will fade out it's content 124 * and set the view Visibility to GONE. If only the content should be changed 125 * {@link #setContentVisible(boolean)} can be used. 126 * 127 * @param nowVisible should the view be visible 128 * @param animate should the change be animated. 129 */ setVisible(boolean nowVisible, boolean animate)130 public void setVisible(boolean nowVisible, boolean animate) { 131 if (mIsVisible != nowVisible) { 132 mIsVisible = nowVisible; 133 if (animate) { 134 if (nowVisible) { 135 setVisibility(VISIBLE); 136 setWillBeGone(false); 137 notifyHeightChanged(false /* needsAnimation */); 138 } else { 139 setWillBeGone(true); 140 } 141 setContentVisible(nowVisible, true /* animate */, null /* runAfter */); 142 } else { 143 setVisibility(nowVisible ? VISIBLE : GONE); 144 setContentVisible(nowVisible, false /* animate */, null /* runAfter */); 145 setWillBeGone(false); 146 notifyHeightChanged(false /* needsAnimation */); 147 } 148 } 149 } 150 151 /** 152 * Set the secondary view of this layout to visible. 153 * 154 * @param nowVisible should the secondary view be visible 155 * @param animate should the change be animated 156 */ setSecondaryVisible(boolean nowVisible, boolean animate)157 public void setSecondaryVisible(boolean nowVisible, boolean animate) { 158 if (mIsSecondaryVisible != nowVisible) { 159 mSecondaryAnimating = animate; 160 mIsSecondaryVisible = nowVisible; 161 setViewVisible(mSecondaryView, nowVisible, animate, mSecondaryVisibilityEndRunnable); 162 } 163 164 if (!mSecondaryAnimating) { 165 mSecondaryVisibilityEndRunnable.accept(true /* cancelled */); 166 } 167 } 168 169 @VisibleForTesting isSecondaryVisible()170 boolean isSecondaryVisible() { 171 return mIsSecondaryVisible; 172 } 173 174 /** 175 * Is this view visible. If a view is currently animating to gone, it will 176 * return {@code false}. 177 */ isVisible()178 public boolean isVisible() { 179 return mIsVisible; 180 } 181 setDuration(int duration)182 void setDuration(int duration) { 183 mDuration = duration; 184 } 185 186 /** 187 * Animate a view to a new visibility. 188 * @param view Target view, maybe content view or dismiss view. 189 * @param nowVisible Should it now be visible. 190 * @param animate Should this be done in an animated way. 191 * @param endRunnable A runnable that is run when the animation is done. 192 */ setViewVisible(View view, boolean nowVisible, boolean animate, Consumer<Boolean> endRunnable)193 private void setViewVisible(View view, boolean nowVisible, 194 boolean animate, Consumer<Boolean> endRunnable) { 195 if (view == null) { 196 return; 197 } 198 199 // Make sure we're visible so animations work 200 if (view.getVisibility() != View.VISIBLE) { 201 view.setVisibility(View.VISIBLE); 202 } 203 204 // cancel any previous animations 205 view.animate().cancel(); 206 float endValue = nowVisible ? 1.0f : 0.0f; 207 if (!animate) { 208 view.setAlpha(endValue); 209 if (endRunnable != null) { 210 endRunnable.accept(true); 211 } 212 return; 213 } 214 215 // Animate the view alpha 216 Interpolator interpolator = nowVisible ? Interpolators.ALPHA_IN : Interpolators.ALPHA_OUT; 217 view.animate() 218 .alpha(endValue) 219 .setInterpolator(interpolator) 220 .setDuration(mDuration) 221 .setListener(new AnimatorListenerAdapter() { 222 boolean mCancelled; 223 224 @Override 225 public void onAnimationCancel(Animator animation) { 226 mCancelled = true; 227 } 228 229 @Override 230 public void onAnimationEnd(Animator animation) { 231 endRunnable.accept(mCancelled); 232 } 233 }); 234 } 235 236 @Override performRemoveAnimation(long duration, long delay, float translationDirection, boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener)237 public long performRemoveAnimation(long duration, long delay, 238 float translationDirection, boolean isHeadsUpAnimation, float endLocation, 239 Runnable onFinishedRunnable, 240 AnimatorListenerAdapter animationListener) { 241 // TODO: Use duration 242 setContentVisible(false, true /* animate */, (cancelled) -> onFinishedRunnable.run()); 243 return 0; 244 } 245 246 @Override performAddAnimation(long delay, long duration, boolean isHeadsUpAppear)247 public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear) { 248 // TODO: use delay and duration 249 setContentVisible(true); 250 } 251 252 @Override performAddAnimation(long delay, long duration, boolean isHeadsUpAppear, Runnable endRunnable)253 public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear, 254 Runnable endRunnable) { 255 // TODO: use delay and duration 256 setContentVisible(true); 257 } 258 259 @Override needsClippingToShelf()260 public boolean needsClippingToShelf() { 261 return false; 262 } 263 264 @Override hasOverlappingRendering()265 public boolean hasOverlappingRendering() { 266 return false; 267 } 268 findContentView()269 protected abstract View findContentView(); 270 271 /** 272 * Returns a view that might not always appear while the main content view is still visible. 273 */ findSecondaryView()274 protected abstract View findSecondaryView(); 275 } 276