1 /* 2 * Copyright (C) 2014 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.tv.settings.util; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ValueAnimator; 22 import android.animation.ValueAnimator.AnimatorUpdateListener; 23 import android.graphics.RectF; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.view.animation.DecelerateInterpolator; 27 import android.view.animation.Interpolator; 28 29 import java.util.ArrayList; 30 import java.util.Comparator; 31 import java.util.List; 32 33 /** 34 * Class used by an activity to perform animation of multiple TransitionImageViews 35 * Usage: 36 * - on activity create: 37 * TransitionImageAnimation animation = new TransitionImageAnimation(rootView); 38 * for_each TransitionImage of source 39 * animation.addTransitionSource(source); 40 * animation.startCancelTimer(); 41 * - When the activity loads all target images 42 * for_each TransitionImage of target 43 * animation.addTransitionTarget(target); 44 * animation.startTransition(); 45 */ 46 public class TransitionImageAnimation { 47 48 public static class Listener { 49 onRemovedView(TransitionImage src, TransitionImage dst)50 public void onRemovedView(TransitionImage src, TransitionImage dst) { 51 } 52 onCancelled(TransitionImageAnimation animation)53 public void onCancelled(TransitionImageAnimation animation) { 54 } 55 onFinished(TransitionImageAnimation animation)56 public void onFinished(TransitionImageAnimation animation) { 57 } 58 } 59 60 private static long DEFAULT_TRANSITION_TIMEOUT_MS = 2000; 61 private static long DEFAULT_CANCEL_TRANSITION_MS = 250; 62 private static long DEFAULT_TRANSITION_DURATION_MS = 250; 63 private static long DEFAULT_TRANSITION_START_DELAY_MS = 160; 64 65 private Interpolator mInterpolator = new DecelerateInterpolator(); 66 private ViewGroup mRoot; 67 private long mTransitionTimeoutMs = DEFAULT_TRANSITION_TIMEOUT_MS; 68 private long mCancelTransitionMs = DEFAULT_CANCEL_TRANSITION_MS; 69 private long mTransitionDurationMs = DEFAULT_TRANSITION_DURATION_MS; 70 private long mTransitionStartDelayMs = DEFAULT_TRANSITION_START_DELAY_MS; 71 private List<TransitionImageView> mTransitions = new ArrayList<TransitionImageView>(); 72 private Listener mListener; 73 private Comparator<TransitionImage> mComparator = new TransitionImageMatcher(); 74 75 private static final int STATE_INITIAL = 0; 76 private static final int STATE_WAIT_DST = 1; 77 private static final int STATE_TRANSITION = 2; 78 private static final int STATE_CANCELLED = 3; 79 private static final int STATE_FINISHED = 4; 80 private int mState; 81 82 private boolean mListeningLayout; 83 private static RectF sTmpRect1 = new RectF(); 84 private static RectF sTmpRect2 = new RectF(); 85 TransitionImageAnimation(ViewGroup root)86 public TransitionImageAnimation(ViewGroup root) { 87 mRoot = root; 88 mState = STATE_INITIAL; 89 } 90 91 /** 92 * Set listener for animation events 93 */ listener(Listener listener)94 public TransitionImageAnimation listener(Listener listener) { 95 mListener = listener; 96 return this; 97 } 98 getListener()99 public Listener getListener() { 100 return mListener; 101 } 102 103 /** 104 * set comparator for matching src and dst ImageTransition 105 */ comparator(Comparator<TransitionImage> comparator)106 public TransitionImageAnimation comparator(Comparator<TransitionImage> comparator) { 107 mComparator = comparator; 108 return this; 109 } 110 getComparator()111 public Comparator<TransitionImage> getComparator() { 112 return mComparator; 113 } 114 115 /** 116 * set interpolator used for animating the Transition 117 */ interpolator(Interpolator interpolator)118 public TransitionImageAnimation interpolator(Interpolator interpolator) { 119 mInterpolator = interpolator; 120 return this; 121 } 122 getInterpolator()123 public Interpolator getInterpolator() { 124 return mInterpolator; 125 } 126 127 /** 128 * set timeout in ms for {@link #startCancelTimer} 129 */ timeoutMs(long timeoutMs)130 public TransitionImageAnimation timeoutMs(long timeoutMs) { 131 mTransitionTimeoutMs = timeoutMs; 132 return this; 133 } 134 getTimeoutMs()135 public long getTimeoutMs() { 136 return mTransitionTimeoutMs; 137 } 138 139 /** 140 * set duration of fade out animation when cancel the transition 141 */ cancelDurationMs(long ms)142 public TransitionImageAnimation cancelDurationMs(long ms) { 143 mCancelTransitionMs = ms; 144 return this; 145 } 146 getCancelDurationMs()147 public long getCancelDurationMs() { 148 return mCancelTransitionMs; 149 } 150 151 /** 152 * set start delay of transition animation 153 */ transitionStartDelayMs(long delay)154 public TransitionImageAnimation transitionStartDelayMs(long delay) { 155 mTransitionStartDelayMs = delay; 156 return this; 157 } 158 getTransitionStartDelayMs()159 public long getTransitionStartDelayMs() { 160 return mTransitionStartDelayMs; 161 } 162 163 /** 164 * set duration of transition animation 165 */ transitionDurationMs(long duration)166 public TransitionImageAnimation transitionDurationMs(long duration) { 167 mTransitionDurationMs = duration; 168 return this; 169 } 170 getTransitionDurationMs()171 public long getTransitionDurationMs() { 172 return mTransitionDurationMs; 173 } 174 175 /** 176 * add source transition and create initial view in root 177 */ addTransitionSource(TransitionImage src)178 public void addTransitionSource(TransitionImage src) { 179 if (mState != STATE_INITIAL) { 180 return; 181 } 182 TransitionImageView view = new TransitionImageView(mRoot.getContext()); 183 mRoot.addView(view); 184 view.setSourceTransition(src); 185 mTransitions.add(view); 186 if (!mListeningLayout) { 187 mListeningLayout = true; 188 mRoot.addOnLayoutChangeListener(mInitializeClip); 189 } 190 } 191 192 /** 193 * kick off the timer for cancel transition 194 */ startCancelTimer()195 public void startCancelTimer() { 196 if (mState != STATE_INITIAL) { 197 return; 198 } 199 mRoot.postDelayed(mCancelTransitionRunnable, mTransitionTimeoutMs); 200 mState = STATE_WAIT_DST; 201 } 202 203 private Runnable mCancelTransitionRunnable = new Runnable() { 204 205 @Override 206 public void run() { 207 cancelTransition(); 208 } 209 210 }; 211 setProgress(float progress)212 private void setProgress(float progress) { 213 // draw from last child (top most in z-order) 214 int lastIndex = mTransitions.size() - 1; 215 for (int i = lastIndex; i >= 0; i--) { 216 TransitionImageView view = mTransitions.get(i); 217 view.setProgress(progress); 218 sTmpRect2.left = 0; 219 sTmpRect2.top = 0; 220 sTmpRect2.right = view.getWidth(); 221 sTmpRect2.bottom = view.getHeight(); 222 WindowLocationUtil.getLocationsInWindow(view, sTmpRect2); 223 if (i == lastIndex) { 224 view.clearExcludeClipRect(); 225 sTmpRect1.set(sTmpRect2); 226 } else { 227 view.setExcludeClipRect(sTmpRect1); 228 // FIXME: this assumes 3rd image will be clipped by "1st union 2nd", 229 // applies to certain situation such as images are stacked in one row 230 sTmpRect1.union(sTmpRect2); 231 } 232 view.invalidate(); 233 } 234 } 235 236 private View.OnLayoutChangeListener mInitializeClip = new View.OnLayoutChangeListener() { 237 @Override 238 public void onLayoutChange(View v, int left, int top, int right, int bottom, 239 int oldLeft, int oldTop, int oldRight, int oldBottom) { 240 v.removeOnLayoutChangeListener(this); 241 mListeningLayout = false; 242 // set initial clipping for all views 243 setProgress(0f); 244 } 245 }; 246 247 /** 248 * start transition 249 */ startTransition()250 public void startTransition() { 251 if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) { 252 return; 253 } 254 for (int i = mTransitions.size() - 1; i >= 0; i--) { 255 TransitionImageView view = mTransitions.get(i); 256 if (view.getDestTransition() == null) { 257 cancelTransition(view); 258 mTransitions.remove(i); 259 } 260 } 261 if (mTransitions.size() == 0) { 262 mState = STATE_CANCELLED; 263 if (mListener != null) { 264 mListener.onCancelled(this); 265 } 266 return; 267 } 268 ValueAnimator v = ValueAnimator.ofFloat(0f, 1f); 269 v.setInterpolator(mInterpolator); 270 v.setDuration(mTransitionDurationMs); 271 v.setStartDelay(mTransitionStartDelayMs); 272 v.addUpdateListener(new AnimatorUpdateListener() { 273 @Override 274 public void onAnimationUpdate(ValueAnimator animation) { 275 float progress = animation.getAnimatedFraction(); 276 setProgress(progress); 277 } 278 }); 279 v.addListener(new AnimatorListenerAdapter() { 280 @Override 281 public void onAnimationEnd(Animator animation) { 282 for (int i = 0, count = mTransitions.size(); i < count; i++) { 283 final TransitionImageView view = mTransitions.get(i); 284 if (mListener != null) { 285 mListener.onRemovedView(view.getSourceTransition(), 286 view.getDestTransition()); 287 } 288 mRoot.removeView(view); 289 } 290 mTransitions.clear(); 291 mState = STATE_FINISHED; 292 if (mListener != null) { 293 mListener.onFinished(TransitionImageAnimation.this); 294 } 295 } 296 }); 297 v.start(); 298 mState = STATE_TRANSITION; 299 } 300 cancelTransition(final View iv)301 private void cancelTransition(final View iv) { 302 iv.animate().alpha(0f).setDuration(mCancelTransitionMs). 303 setListener(new AnimatorListenerAdapter() { 304 @Override 305 public void onAnimationEnd(Animator arg0) { 306 mRoot.removeView(iv); 307 } 308 }).start(); 309 } 310 311 /** 312 * Cancel the transition before it starts, no effect if it already starts 313 */ cancelTransition()314 public void cancelTransition() { 315 if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) { 316 return; 317 } 318 int count = mTransitions.size(); 319 if (count > 0) { 320 for (int i = 0; i < count; i++) { 321 cancelTransition(mTransitions.get(i)); 322 } 323 mTransitions.clear(); 324 } 325 mState = STATE_CANCELLED; 326 if (mListener != null) { 327 mListener.onCancelled(this); 328 } 329 } 330 331 /** 332 * find a matching source and relate it with destination 333 */ addTransitionTarget(TransitionImage dst)334 public boolean addTransitionTarget(TransitionImage dst) { 335 if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) { 336 return false; 337 } 338 for (int i = 0, count = mTransitions.size(); i < count; i++) { 339 TransitionImageView view = mTransitions.get(i); 340 if (mComparator.compare(view.getSourceTransition(), dst) == 0) { 341 view.setDestTransition(dst); 342 return true; 343 } 344 } 345 return false; 346 } 347 } 348