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.app.animation.Interpolators; 27 import com.android.internal.annotations.VisibleForTesting; 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, Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener)237 public long performRemoveAnimation(long duration, long delay, 238 float translationDirection, boolean isHeadsUpAnimation, 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