Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 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 | |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 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; |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 27 | import android.widget.LinearLayout; |
Brian Attwell | d462d3a | 2014-12-17 12:10:21 -0800 | [diff] [blame] | 28 | import android.widget.ScrollView; |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 29 | |
Chiao Cheng | e0b2f1e | 2012-06-12 13:07:56 -0700 | [diff] [blame] | 30 | import com.android.contacts.util.SchedulingUtils; |
Gary Mai | 0a49afa | 2016-12-05 15:53:58 -0800 | [diff] [blame] | 31 | |
Chiao Cheng | e0b2f1e | 2012-06-12 13:07:56 -0700 | [diff] [blame] | 32 | import com.google.common.collect.Lists; |
| 33 | |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 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 | } |
Walter Jang | f7dfa54 | 2015-10-26 14:27:27 -0700 | [diff] [blame] | 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); |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 80 | } |
| 81 | } |
| 82 | }); |
| 83 | } |
| 84 | |
Yorke Lee | ba48d21 | 2012-12-26 13:25:48 -0800 | [diff] [blame] | 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); |
Yorke Lee | ba48d21 | 2012-12-26 13:25:48 -0800 | [diff] [blame] | 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 | |
Brian Attwell | 043fba6 | 2014-10-30 11:11:56 -0700 | [diff] [blame] | 119 | public void showFieldFooter(final View view) { |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 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); |
Josh Gargus | 6f5557e | 2012-03-21 10:45:08 -0700 | [diff] [blame] | 125 | SchedulingUtils.doAfterLayout(view, new Runnable() { |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 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 | |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 149 | /** |
Brian Attwell | d462d3a | 2014-12-17 12:10:21 -0800 | [diff] [blame] | 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 | /** |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 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 | /** |
Yorke Lee | ba48d21 | 2012-12-26 13:25:48 -0800 | [diff] [blame] | 216 | * Traverses up the view hierarchy and returns all views physically below this item. |
Chiao Cheng | 2e9ee9f | 2012-05-24 19:57:59 -0700 | [diff] [blame] | 217 | * |
| 218 | * @return List of views that are below the given view. Empty list if parent of view is null. |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 219 | */ |
| 220 | private static List<View> getViewsBelowOf(View view) { |
| 221 | final ViewGroup victimParent = (ViewGroup) view.getParent(); |
| 222 | final List<View> result = Lists.newArrayList(); |
Chiao Cheng | 2e9ee9f | 2012-05-24 19:57:59 -0700 | [diff] [blame] | 223 | if (victimParent != null) { |
| 224 | final int index = victimParent.indexOfChild(view); |
Yorke Lee | ba48d21 | 2012-12-26 13:25:48 -0800 | [diff] [blame] | 225 | getViewsBelowOfRecursive(result, victimParent, index + 1, view); |
Chiao Cheng | 2e9ee9f | 2012-05-24 19:57:59 -0700 | [diff] [blame] | 226 | } |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 227 | return result; |
| 228 | } |
| 229 | |
| 230 | private static void getViewsBelowOfRecursive(List<View> result, ViewGroup container, |
Yorke Lee | ba48d21 | 2012-12-26 13:25:48 -0800 | [diff] [blame] | 231 | int index, View target) { |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 232 | for (int i = index; i < container.getChildCount(); i++) { |
Yorke Lee | ba48d21 | 2012-12-26 13:25:48 -0800 | [diff] [blame] | 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 | } |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 240 | } |
| 241 | |
| 242 | final ViewParent parent = container.getParent(); |
| 243 | if (parent instanceof LinearLayout) { |
| 244 | final LinearLayout parentLayout = (LinearLayout) parent; |
Yorke Lee | ba48d21 | 2012-12-26 13:25:48 -0800 | [diff] [blame] | 245 | int containerIndex = parentLayout.indexOfChild(container); |
| 246 | getViewsBelowOfRecursive(result, parentLayout, containerIndex + 1, target); |
Daniel Lehmann | ca87e9c | 2012-03-06 14:02:31 -0800 | [diff] [blame] | 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 | } |