1 /* 2 * Copyright (C) 2012 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.contacts.editor; 18 19 import android.animation.Animator; 20 import android.animation.Animator.AnimatorListener; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.AnimatorSet; 23 import android.animation.ObjectAnimator; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.view.ViewParent; 27 import android.widget.LinearLayout; 28 29 import com.android.contacts.util.SchedulingUtils; 30 import com.google.common.collect.Lists; 31 32 import java.util.List; 33 34 /** 35 * Configures animations for typical use-cases 36 */ 37 public class EditorAnimator { 38 private static EditorAnimator sInstance = new EditorAnimator(); 39 40 public static EditorAnimator getInstance() { 41 return sInstance; 42 } 43 44 /** Private constructor for singleton */ 45 private EditorAnimator() { } 46 47 private AnimatorRunner mRunner = new AnimatorRunner(); 48 49 public void removeEditorView(final View victim) { 50 mRunner.endOldAnimation(); 51 final int offset = victim.getHeight(); 52 53 final List<View> viewsToMove = getViewsBelowOf(victim); 54 final List<Animator> animators = Lists.newArrayList(); 55 56 // Fade out 57 final ObjectAnimator fadeOutAnimator = 58 ObjectAnimator.ofFloat(victim, View.ALPHA, 1.0f, 0.0f); 59 fadeOutAnimator.setDuration(200); 60 animators.add(fadeOutAnimator); 61 62 // Translations 63 translateViews(animators, viewsToMove, 0.0f, -offset, 100, 200); 64 65 mRunner.run(animators, new AnimatorListenerAdapter() { 66 @Override 67 public void onAnimationEnd(Animator animation) { 68 // Clean up: Remove all the translations 69 for (int i = 0; i < viewsToMove.size(); i++) { 70 final View view = viewsToMove.get(i); 71 view.setTranslationY(0.0f); 72 } 73 // Remove our target view (if parent is null, we were run several times by quick 74 // fingers. Just ignore) 75 final ViewGroup victimParent = (ViewGroup) victim.getParent(); 76 if (victimParent != null) { 77 victimParent.removeView(victim); 78 } 79 } 80 }); 81 } 82 83 /** 84 * Slides the view into its new height, while simultaneously fading it into view. 85 * 86 * @param target The target view to perform the animation on. 87 * @param previousHeight The previous height of the view before its height was changed. 88 * Needed because the view does not store any state information about its previous height. 89 */ 90 public void slideAndFadeIn(final ViewGroup target, final int previousHeight) { 91 mRunner.endOldAnimation(); 92 target.setVisibility(View.VISIBLE); 93 target.setAlpha(0.0f); 94 target.requestFocus(); 95 SchedulingUtils.doAfterLayout(target, new Runnable() { 96 @Override 97 public void run() { 98 final int offset = target.getHeight() - previousHeight; 99 final List<Animator> animators = Lists.newArrayList(); 100 101 // Translations 102 final List<View> viewsToMove = getViewsBelowOf(target); 103 104 translateViews(animators, viewsToMove, -offset, 0.0f, 0, 200); 105 106 // Fade in 107 final ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat( 108 target, View.ALPHA, 0.0f, 1.0f); 109 fadeInAnimator.setDuration(200); 110 fadeInAnimator.setStartDelay(200); 111 animators.add(fadeInAnimator); 112 113 mRunner.run(animators); 114 } 115 }); 116 } 117 118 public void expandOrganization(final View addOrganizationButton, 119 final ViewGroup organizationSectionViewContainer) { 120 mRunner.endOldAnimation(); 121 // Make the new controls visible and do one layout pass (so that we can measure) 122 organizationSectionViewContainer.setVisibility(View.VISIBLE); 123 organizationSectionViewContainer.setAlpha(0.0f); 124 organizationSectionViewContainer.requestFocus(); 125 SchedulingUtils.doAfterLayout(addOrganizationButton, new Runnable() { 126 @Override 127 public void run() { 128 // How many pixels extra do we need? 129 final int offset = organizationSectionViewContainer.getHeight() - 130 addOrganizationButton.getHeight(); 131 final List<Animator> animators = Lists.newArrayList(); 132 133 // Fade out 134 final ObjectAnimator fadeOutAnimator = ObjectAnimator.ofFloat( 135 addOrganizationButton, View.ALPHA, 1.0f, 0.0f); 136 fadeOutAnimator.setDuration(200); 137 animators.add(fadeOutAnimator); 138 139 // Translations 140 final List<View> viewsToMove = getViewsBelowOf(organizationSectionViewContainer); 141 translateViews(animators, viewsToMove, -offset, 0.0f, 0, 200); 142 // Fade in 143 final ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat( 144 organizationSectionViewContainer, View.ALPHA, 0.0f, 1.0f); 145 fadeInAnimator.setDuration(200); 146 fadeInAnimator.setStartDelay(200); 147 animators.add(fadeInAnimator); 148 149 mRunner.run(animators); 150 } 151 }); 152 } 153 154 public void showAddFieldFooter(final View view) { 155 mRunner.endOldAnimation(); 156 if (view.getVisibility() == View.VISIBLE) return; 157 // Make the new controls visible and do one layout pass (so that we can measure) 158 view.setVisibility(View.VISIBLE); 159 view.setAlpha(0.0f); 160 SchedulingUtils.doAfterLayout(view, new Runnable() { 161 @Override 162 public void run() { 163 // How many pixels extra do we need? 164 final int offset = view.getHeight(); 165 166 final List<Animator> animators = Lists.newArrayList(); 167 168 // Translations 169 final List<View> viewsToMove = getViewsBelowOf(view); 170 translateViews(animators, viewsToMove, -offset, 0.0f, 0, 200); 171 172 // Fade in 173 final ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat( 174 view, View.ALPHA, 0.0f, 1.0f); 175 fadeInAnimator.setDuration(200); 176 fadeInAnimator.setStartDelay(200); 177 animators.add(fadeInAnimator); 178 179 mRunner.run(animators); 180 } 181 }); 182 } 183 184 public void hideAddFieldFooter(final View victim) { 185 mRunner.endOldAnimation(); 186 if (victim.getVisibility() == View.GONE) return; 187 final int offset = victim.getHeight(); 188 189 final List<View> viewsToMove = getViewsBelowOf(victim); 190 final List<Animator> animators = Lists.newArrayList(); 191 192 // Fade out 193 final ObjectAnimator fadeOutAnimator = 194 ObjectAnimator.ofFloat(victim, View.ALPHA, 1.0f, 0.0f); 195 fadeOutAnimator.setDuration(200); 196 animators.add(fadeOutAnimator); 197 198 // Translations 199 translateViews(animators, viewsToMove, 0.0f, -offset, 100, 200); 200 201 // Combine 202 mRunner.run(animators, new AnimatorListenerAdapter() { 203 @Override 204 public void onAnimationEnd(Animator animation) { 205 // Clean up: Remove all the translations 206 for (int i = 0; i < viewsToMove.size(); i++) { 207 final View view = viewsToMove.get(i); 208 view.setTranslationY(0.0f); 209 } 210 211 // Restore alpha (for next time), but hide the view for good now 212 victim.setAlpha(1.0f); 213 victim.setVisibility(View.GONE); 214 } 215 }); 216 } 217 218 /** 219 * Creates a translation-animation for the given views 220 */ 221 private static void translateViews(List<Animator> animators, List<View> views, float fromY, 222 float toY, int startDelay, int duration) { 223 for (int i = 0; i < views.size(); i++) { 224 final View child = views.get(i); 225 final ObjectAnimator translateAnimator = 226 ObjectAnimator.ofFloat(child, View.TRANSLATION_Y, fromY, toY); 227 translateAnimator.setStartDelay(startDelay); 228 translateAnimator.setDuration(duration); 229 animators.add(translateAnimator); 230 } 231 } 232 233 /** 234 * Traverses up the view hierarchy and returns all views physically below this item. 235 * 236 * @return List of views that are below the given view. Empty list if parent of view is null. 237 */ 238 private static List<View> getViewsBelowOf(View view) { 239 final ViewGroup victimParent = (ViewGroup) view.getParent(); 240 final List<View> result = Lists.newArrayList(); 241 if (victimParent != null) { 242 final int index = victimParent.indexOfChild(view); 243 getViewsBelowOfRecursive(result, victimParent, index + 1, view); 244 } 245 return result; 246 } 247 248 private static void getViewsBelowOfRecursive(List<View> result, ViewGroup container, 249 int index, View target) { 250 for (int i = index; i < container.getChildCount(); i++) { 251 View view = container.getChildAt(i); 252 // consider the child view below the target view only if it is physically 253 // below the view on-screen, using half the height of the target view as the 254 // baseline 255 if (view.getY() > (target.getY() + target.getHeight() / 2)) { 256 result.add(view); 257 } 258 } 259 260 final ViewParent parent = container.getParent(); 261 if (parent instanceof LinearLayout) { 262 final LinearLayout parentLayout = (LinearLayout) parent; 263 int containerIndex = parentLayout.indexOfChild(container); 264 getViewsBelowOfRecursive(result, parentLayout, containerIndex + 1, target); 265 } 266 } 267 268 /** 269 * Keeps a reference to the last animator, so that we can end that early if the user 270 * quickly pushes buttons. Removes the reference once the animation has finished 271 */ 272 /* package */ static class AnimatorRunner extends AnimatorListenerAdapter { 273 private Animator mLastAnimator; 274 275 @Override 276 public void onAnimationEnd(Animator animation) { 277 mLastAnimator = null; 278 } 279 280 public void run(List<Animator> animators) { 281 run(animators, null); 282 } 283 284 public void run(List<Animator> animators, AnimatorListener listener) { 285 final AnimatorSet set = new AnimatorSet(); 286 set.playTogether(animators); 287 if (listener != null) set.addListener(listener); 288 set.addListener(this); 289 mLastAnimator = set; 290 set.start(); 291 } 292 293 public void endOldAnimation() { 294 if (mLastAnimator != null) { 295 mLastAnimator.end(); 296 } 297 } 298 } 299 } 300