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