Home | History | Annotate | Download | only in util
      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 
     50         public void onRemovedView(TransitionImage src, TransitionImage dst) {
     51         }
     52 
     53         public void onCancelled(TransitionImageAnimation animation) {
     54         }
     55 
     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 
     86     public TransitionImageAnimation(ViewGroup root) {
     87         mRoot = root;
     88         mState = STATE_INITIAL;
     89     }
     90 
     91     /**
     92      * Set listener for animation events
     93      */
     94     public TransitionImageAnimation listener(Listener listener) {
     95         mListener = listener;
     96         return this;
     97     }
     98 
     99     public Listener getListener() {
    100         return mListener;
    101     }
    102 
    103     /**
    104      * set comparator for matching src and dst ImageTransition
    105      */
    106     public TransitionImageAnimation comparator(Comparator<TransitionImage> comparator) {
    107         mComparator = comparator;
    108         return this;
    109     }
    110 
    111     public Comparator<TransitionImage> getComparator() {
    112         return mComparator;
    113     }
    114 
    115     /**
    116      * set interpolator used for animating the Transition
    117      */
    118     public TransitionImageAnimation interpolator(Interpolator interpolator) {
    119         mInterpolator = interpolator;
    120         return this;
    121     }
    122 
    123     public Interpolator getInterpolator() {
    124         return mInterpolator;
    125     }
    126 
    127     /**
    128      * set timeout in ms for {@link #startCancelTimer}
    129      */
    130     public TransitionImageAnimation timeoutMs(long timeoutMs) {
    131         mTransitionTimeoutMs = timeoutMs;
    132         return this;
    133     }
    134 
    135     public long getTimeoutMs() {
    136         return mTransitionTimeoutMs;
    137     }
    138 
    139     /**
    140      * set duration of fade out animation when cancel the transition
    141      */
    142     public TransitionImageAnimation cancelDurationMs(long ms) {
    143         mCancelTransitionMs = ms;
    144         return this;
    145     }
    146 
    147     public long getCancelDurationMs() {
    148         return mCancelTransitionMs;
    149     }
    150 
    151     /**
    152      * set start delay of transition animation
    153      */
    154     public TransitionImageAnimation transitionStartDelayMs(long delay) {
    155         mTransitionStartDelayMs = delay;
    156         return this;
    157     }
    158 
    159     public long getTransitionStartDelayMs() {
    160         return mTransitionStartDelayMs;
    161     }
    162 
    163     /**
    164      * set duration of transition animation
    165      */
    166     public TransitionImageAnimation transitionDurationMs(long duration) {
    167         mTransitionDurationMs = duration;
    168         return this;
    169     }
    170 
    171     public long getTransitionDurationMs() {
    172         return mTransitionDurationMs;
    173     }
    174 
    175     /**
    176      * add source transition and create initial view in root
    177      */
    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      */
    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 
    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      */
    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 
    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      */
    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      */
    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