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.internal.widget; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.util.IntProperty; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.view.animation.Interpolator; 26 import android.view.animation.PathInterpolator; 27 28 import com.android.internal.R; 29 30 /** 31 * A listener that automatically starts animations when the layout bounds change. 32 */ 33 public class MessagingPropertyAnimator implements View.OnLayoutChangeListener { 34 private static final long APPEAR_ANIMATION_LENGTH = 210; 35 private static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); 36 public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f); 37 private static final int TAG_TOP_ANIMATOR = R.id.tag_top_animator; 38 private static final int TAG_TOP = R.id.tag_top_override; 39 private static final int TAG_LAYOUT_TOP = R.id.tag_layout_top; 40 private static final int TAG_FIRST_LAYOUT = R.id.tag_is_first_layout; 41 private static final int TAG_ALPHA_ANIMATOR = R.id.tag_alpha_animator; 42 private static final ViewClippingUtil.ClippingParameters CLIPPING_PARAMETERS = 43 view -> view.getId() == com.android.internal.R.id.notification_messaging; 44 private static final IntProperty<View> TOP = 45 new IntProperty<View>("top") { 46 @Override 47 public void setValue(View object, int value) { 48 setTop(object, value); 49 } 50 51 @Override 52 public Integer get(View object) { 53 return getTop(object); 54 } 55 }; 56 57 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)58 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 59 int oldTop, int oldRight, int oldBottom) { 60 setLayoutTop(v, top); 61 if (isFirstLayout(v)) { 62 setFirstLayout(v, false /* first */); 63 setTop(v, top); 64 return; 65 } 66 startTopAnimation(v, getTop(v), top, MessagingLayout.FAST_OUT_SLOW_IN); 67 } 68 isFirstLayout(View view)69 private static boolean isFirstLayout(View view) { 70 Boolean tag = (Boolean) view.getTag(TAG_FIRST_LAYOUT); 71 if (tag == null) { 72 return true; 73 } 74 return tag; 75 } 76 recycle(View view)77 public static void recycle(View view) { 78 setFirstLayout(view, true /* first */); 79 } 80 setFirstLayout(View view, boolean first)81 private static void setFirstLayout(View view, boolean first) { 82 view.setTagInternal(TAG_FIRST_LAYOUT, first); 83 } 84 setLayoutTop(View view, int top)85 private static void setLayoutTop(View view, int top) { 86 view.setTagInternal(TAG_LAYOUT_TOP, top); 87 } 88 getLayoutTop(View view)89 public static int getLayoutTop(View view) { 90 Integer tag = (Integer) view.getTag(TAG_LAYOUT_TOP); 91 if (tag == null) { 92 return getTop(view); 93 } 94 return tag; 95 } 96 97 /** 98 * Start a translation animation from a start offset to the laid out location 99 * @param view The view to animate 100 * @param startTranslation The starting translation to start from. 101 * @param interpolator The interpolator to use. 102 */ startLocalTranslationFrom(View view, int startTranslation, Interpolator interpolator)103 public static void startLocalTranslationFrom(View view, int startTranslation, 104 Interpolator interpolator) { 105 startTopAnimation(view, getTop(view) + startTranslation, getLayoutTop(view), interpolator); 106 } 107 108 /** 109 * Start a translation animation from a start offset to the laid out location 110 * @param view The view to animate 111 * @param endTranslation The end translation to go to. 112 * @param interpolator The interpolator to use. 113 */ startLocalTranslationTo(View view, int endTranslation, Interpolator interpolator)114 public static void startLocalTranslationTo(View view, int endTranslation, 115 Interpolator interpolator) { 116 int top = getTop(view); 117 startTopAnimation(view, top, top + endTranslation, interpolator); 118 } 119 getTop(View v)120 public static int getTop(View v) { 121 Integer tag = (Integer) v.getTag(TAG_TOP); 122 if (tag == null) { 123 return v.getTop(); 124 } 125 return tag; 126 } 127 setTop(View v, int value)128 private static void setTop(View v, int value) { 129 v.setTagInternal(TAG_TOP, value); 130 updateTopAndBottom(v); 131 } 132 updateTopAndBottom(View v)133 private static void updateTopAndBottom(View v) { 134 int top = getTop(v); 135 int height = v.getHeight(); 136 v.setTop(top); 137 v.setBottom(height + top); 138 } 139 startTopAnimation(final View v, int start, int end, Interpolator interpolator)140 private static void startTopAnimation(final View v, int start, int end, 141 Interpolator interpolator) { 142 ObjectAnimator existing = (ObjectAnimator) v.getTag(TAG_TOP_ANIMATOR); 143 if (existing != null) { 144 existing.cancel(); 145 } 146 if (!v.isShown() || start == end 147 || (MessagingLinearLayout.isGone(v) && !isHidingAnimated(v))) { 148 setTop(v, end); 149 return; 150 } 151 ObjectAnimator animator = ObjectAnimator.ofInt(v, TOP, start, end); 152 setTop(v, start); 153 animator.setInterpolator(interpolator); 154 animator.setDuration(APPEAR_ANIMATION_LENGTH); 155 animator.addListener(new AnimatorListenerAdapter() { 156 public boolean mCancelled; 157 158 @Override 159 public void onAnimationEnd(Animator animation) { 160 v.setTagInternal(TAG_TOP_ANIMATOR, null); 161 setClippingDeactivated(v, false); 162 } 163 164 @Override 165 public void onAnimationCancel(Animator animation) { 166 mCancelled = true; 167 } 168 }); 169 setClippingDeactivated(v, true); 170 v.setTagInternal(TAG_TOP_ANIMATOR, animator); 171 animator.start(); 172 } 173 isHidingAnimated(View v)174 private static boolean isHidingAnimated(View v) { 175 if (v instanceof MessagingLinearLayout.MessagingChild) { 176 return ((MessagingLinearLayout.MessagingChild) v).isHidingAnimated(); 177 } 178 return false; 179 } 180 fadeIn(final View v)181 public static void fadeIn(final View v) { 182 ObjectAnimator existing = (ObjectAnimator) v.getTag(TAG_ALPHA_ANIMATOR); 183 if (existing != null) { 184 existing.cancel(); 185 } 186 if (v.getVisibility() == View.INVISIBLE) { 187 v.setVisibility(View.VISIBLE); 188 } 189 ObjectAnimator animator = ObjectAnimator.ofFloat(v, View.ALPHA, 190 0.0f, 1.0f); 191 v.setAlpha(0.0f); 192 animator.setInterpolator(ALPHA_IN); 193 animator.setDuration(APPEAR_ANIMATION_LENGTH); 194 animator.addListener(new AnimatorListenerAdapter() { 195 @Override 196 public void onAnimationEnd(Animator animation) { 197 v.setTagInternal(TAG_ALPHA_ANIMATOR, null); 198 updateLayerType(v, false /* animating */); 199 } 200 }); 201 updateLayerType(v, true /* animating */); 202 v.setTagInternal(TAG_ALPHA_ANIMATOR, animator); 203 animator.start(); 204 } 205 updateLayerType(View view, boolean animating)206 private static void updateLayerType(View view, boolean animating) { 207 if (view.hasOverlappingRendering() && animating) { 208 view.setLayerType(View.LAYER_TYPE_HARDWARE, null); 209 } else if (view.getLayerType() == View.LAYER_TYPE_HARDWARE) { 210 view.setLayerType(View.LAYER_TYPE_NONE, null); 211 } 212 } 213 fadeOut(final View view, Runnable endAction)214 public static void fadeOut(final View view, Runnable endAction) { 215 ObjectAnimator existing = (ObjectAnimator) view.getTag(TAG_ALPHA_ANIMATOR); 216 if (existing != null) { 217 existing.cancel(); 218 } 219 if (!view.isShown() || (MessagingLinearLayout.isGone(view) && !isHidingAnimated(view))) { 220 view.setAlpha(0.0f); 221 if (endAction != null) { 222 endAction.run(); 223 } 224 return; 225 } 226 ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, 227 view.getAlpha(), 0.0f); 228 animator.setInterpolator(ALPHA_OUT); 229 animator.setDuration(APPEAR_ANIMATION_LENGTH); 230 animator.addListener(new AnimatorListenerAdapter() { 231 @Override 232 public void onAnimationEnd(Animator animation) { 233 view.setTagInternal(TAG_ALPHA_ANIMATOR, null); 234 updateLayerType(view, false /* animating */); 235 if (endAction != null) { 236 endAction.run(); 237 } 238 } 239 }); 240 updateLayerType(view, true /* animating */); 241 view.setTagInternal(TAG_ALPHA_ANIMATOR, animator); 242 animator.start(); 243 } 244 setClippingDeactivated(final View transformedView, boolean deactivated)245 public static void setClippingDeactivated(final View transformedView, boolean deactivated) { 246 ViewClippingUtil.setClippingDeactivated(transformedView, deactivated, 247 CLIPPING_PARAMETERS); 248 } 249 isAnimatingTranslation(View v)250 public static boolean isAnimatingTranslation(View v) { 251 return v.getTag(TAG_TOP_ANIMATOR) != null; 252 } 253 isAnimatingAlpha(View v)254 public static boolean isAnimatingAlpha(View v) { 255 return v.getTag(TAG_ALPHA_ANIMATOR) != null; 256 } 257 setToLaidOutPosition(View view)258 public static void setToLaidOutPosition(View view) { 259 setTop(view, getLayoutTop(view)); 260 } 261 } 262