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