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