• Android 动画animation 深入分析


    转载请注明出处:http://blog.csdn.net/farmer_cc/article/details/18259117

    Android 动画animation 深入分析

    前言:本文试图通过分析动画流程,来理解android动画系统的设计与实现,学习动画的基本原则,最终希望能够指导动画的设计。

    0 本文中用到的一些类图

    1 view animation 

    调用方法:view.startAnimation(animation);

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. public void startAnimation(Animation animation) {  
    2.     animation.setStartTime(Animation.START_ON_FIRST_FRAME);  
    3.     setAnimation(animation);  
    4.     invalidateParentCaches();  
    5.     invalidate(true);  
    6. }  

    在invalidate(ture);中

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. if (p != null && ai != null) {  
    2.     final Rect r = ai.mTmpInvalRect;  
    3.     r.set(0, 0, mRight - mLeft, mBottom - mTop);  
    4.     // Don't call invalidate -- we don't want to internally scroll  
    5.     // our own bounds  
    6.     p.invalidateChild(this, r);  
    7. }  


    即调用parent的invalidateChild,


    假定父控件即为ViewRootImpl;

    public final class ViewRootImpl implements ViewParent;

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. @Override  
    2. public void invalidateChild(View child, Rect dirty) {  
    3.     invalidateChildInParent(null, dirty);  
    4. }  
    5.   
    6. public ViewParent invalidateChildInParent(int[] location, Rect dirty) {  
    7.     //...省略一堆判断条件,最终调用  
    8.     if (!mWillDrawSoon && (intersected || mIsAnimating)) {  
    9.         scheduleTraversals();  
    10.     }  
    11.   
    12.     return null;  
    13. }  
    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. void scheduleTraversals() {  
    2.     if (!mTraversalScheduled) {  
    3.         mTraversalScheduled = true;  
    4.         mTraversalBarrier = mHandler.getLooper().postSyncBarrier();  
    5.         mChoreographer.postCallback(  
    6.                 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);  
    7.         scheduleConsumeBatchedInput();  
    8.     }  
    9. }  

    其中mTraversalBarrier = mHandler.getLooper().postSyncBarrier();是设置同步障碍(syncBarrier),当looper中的消息队列执行到barrier 后,会暂停执行,只有当barrier 被释放mHandler.getLooper().removeSyncBarrier(mTraversalBarrier); 后消息队列才能继续执行。

        Choreographer mChoreographer; 是动画系统中的核心组织者, 负责统一调度。后面详细说。

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. final TraversalRunnable mTraversalRunnable = new TraversalRunnable();  
    2. final class TraversalRunnable implements Runnable {  
    3.     @Override  
    4.     public void run() {  
    5.         doTraversal();  
    6.     }  
    7. }  
    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1.     void doTraversal() {  
    2.         performTraversals();  
    3.     }  

    perform 待补充

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. final class ConsumeBatchedInputRunnable implements Runnable {  
    2.     @Override  
    3.     public void run() {  
    4.         doConsumeBatchedInput(mChoreographer.getFrameTimeNanos());  
    5.     }  
    6. }  
    7. final ConsumeBatchedInputRunnable mConsumedBatchedInputRunnable =  
    8.         new ConsumeBatchedInputRunnable();  

    doConsume 待补充



    2 属性动画aninmator

    valueAnimator.start();

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1.     private void start(boolean playBackwards) {  
    2.         if (Looper.myLooper() == null) {  
    3.             throw new AndroidRuntimeException("Animators may only be run on Looper threads");  
    4.         }  
    5.         AnimationHandler animationHandler = getOrCreateAnimationHandler();  
    6.         animationHandler.mPendingAnimations.add(this);  
    7.         if (mStartDelay == 0) {  
    8.             // This sets the initial value of the animation, prior to actually starting it running  
    9.             setCurrentPlayTime(0);  
    10.             mPlayingState = STOPPED;  
    11.             mRunning = true;  
    12.             notifyStartListeners();  
    13.         }  
    14.         animationHandler.start();  
    15.     }  

    这里会检查调用线程必须是Looper线程,如果是view相关的属性动画,还必须是UI 线程。

    得到AnimationHandle 并把自己加入到PendingAnimations  的list中.

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. getOrCreateAnimationHandler();  
    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. protected static ThreadLocal<AnimationHandler> sAnimationHandler =  
    2.         new ThreadLocal<AnimationHandler>()  
    3. protected static class AnimationHandler implements Runnable {  
    4.     // The per-thread list of all active animations  
    5.     /** @hide */  
    6.     protected final ArrayList<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>();  
    7.   
    8.     // Used in doAnimationFrame() to avoid concurrent modifications of mAnimations  
    9.     private final ArrayList<ValueAnimator> mTmpAnimations = new ArrayList<ValueAnimator>();  
    10.   
    11.     // The per-thread set of animations to be started on the next animation frame  
    12.     /** @hide */  
    13.     protected final ArrayList<ValueAnimator> mPendingAnimations = new ArrayList<ValueAnimator>();  
    14.   
    15.     /** 
    16.      * Internal per-thread collections used to avoid set collisions as animations start and end 
    17.      * while being processed. 
    18.      * @hide 
    19.      */  
    20.     protected final ArrayList<ValueAnimator> mDelayedAnims = new ArrayList<ValueAnimator>();  
    21.     private final ArrayList<ValueAnimator> mEndingAnims = new ArrayList<ValueAnimator>();  
    22.     private final ArrayList<ValueAnimator> mReadyAnims = new ArrayList<ValueAnimator>();  
    23.   
    24.     private final Choreographer mChoreographer;  
    25.     private boolean mAnimationScheduled;  


    AnimationHandler 就是一个runnable, 注意成员变量中的多个animator 的list 以及重要的mChoreographer = Choreographer.getInstance();

    mChoreographer 也是一个threadlocal的变量。

    在animationHandler.start() 中

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1.         public void start() {  
    2.             scheduleAnimation();  
    3.         }  
    4.         private void scheduleAnimation() {  
    5.             if (!mAnimationScheduled) {  
    6.                 mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);  
    7.                 mAnimationScheduled = true;  
    8.             }  
    9.         }  

    this 是runnable 即把animationHandler自己添加添加到mChoreographer 的队列中。

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1.     public void postCallback(int callbackType, Runnable action, Object token) {  
    2.         postCallbackDelayed(callbackType, action, token, 0);  
    3.     }  
    4.     public void postCallbackDelayed(int callbackType,  
    5.             Runnable action, Object token, long delayMillis) {  
    6.         postCallbackDelayedInternal(callbackType, action, token, delayMillis);  
    7.     }  
    8.     private void postCallbackDelayedInternal(int callbackType,  
    9.             Object action, Object token, long delayMillis) {  
    10.         synchronized (mLock) {  
    11.             final long now = SystemClock.uptimeMillis();  
    12.             final long dueTime = now + delayMillis;  
    13.             mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);  
    14.   
    15.             if (dueTime <= now) {  
    16.                 scheduleFrameLocked(now);  
    17.             } else {  
    18.                 Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);  
    19.                 msg.arg1 = callbackType;  
    20.                 msg.setAsynchronous(true);  
    21.                 mHandler.sendMessageAtTime(msg, dueTime);  
    22.             }  
    23.         }  
    24.     }  

    传入的delay为0, 即调用scheduleFrameLocked(now);

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. private void scheduleFrameLocked(long now) {  
    2.     if (!mFrameScheduled) {  
    3.         mFrameScheduled = true;  
    4.         if (USE_VSYNC) {  
    5.             if (DEBUG) {  
    6.                 Log.d(TAG, "Scheduling next frame on vsync.");  
    7.             }  
    8.   
    9.             // If running on the Looper thread, then schedule the vsync immediately,  
    10.             // otherwise post a message to schedule the vsync from the UI thread  
    11.             // as soon as possible.  
    12.             if (isRunningOnLooperThreadLocked()) {  
    13.                 scheduleVsyncLocked();  
    14.             } else {  
    15.                 Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);  
    16.                 msg.setAsynchronous(true);  
    17.                 mHandler.sendMessageAtFrontOfQueue(msg);  
    18.             }  
    19.         } else {  
    20.             final long nextFrameTime = Math.max(  
    21.                     mLastFrameTimeNanos / NANOS_PER_MS + sFrameDelay, now);  
    22.             if (DEBUG) {  
    23.                 Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");  
    24.             }  
    25.             Message msg = mHandler.obtainMessage(MSG_DO_FRAME);  
    26.             msg.setAsynchronous(true);  
    27.             mHandler.sendMessageAtTime(msg, nextFrameTime);  
    28.         }  
    29.     }  
    30. }  
    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. private static final boolean USE_VSYNC = SystemProperties.getBoolean(  
    2.         "debug.choreographer.vsync", true);  

    USE_VSYNC 默认是true;    

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. private boolean isRunningOnLooperThreadLocked() {  
    2.     return Looper.myLooper() == mLooper;  
    3. }  

    检查当前looper和mChoreographer的looper是否一致。一般情况是一致的。就会调用scheduleVsyncLocked();

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. private void scheduleVsyncLocked() {  
    2.     mDisplayEventReceiver.scheduleVsync();  
    3. }  
    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. public void scheduleVsync() {  
    2.     if (mReceiverPtr == 0) {  
    3.         Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "  
    4.                 + "receiver has already been disposed.");  
    5.     } else {  
    6.         nativeScheduleVsync(mReceiverPtr);  
    7.     }  
    8. }  

    到了native 暂时先不涉及。

    回头来看animationHandler 的run()。 前面提到animationHandler把自己添加到mChoreographer,当被调用时,调用run方法。

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. // Called by the Choreographer.  
    2. @Override  
    3. public void run() {  
    4.     mAnimationScheduled = false;  
    5.     doAnimationFrame(mChoreographer.getFrameTime());  
    6. }  
    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1.     public long getFrameTime() {  
    2.         return getFrameTimeNanos() / NANOS_PER_MS;  
    3.     }  
    4.     public long getFrameTimeNanos() {  
    5.         synchronized (mLock) {  
    6.             if (!mCallbacksRunning) {  
    7.                 throw new IllegalStateException("This method must only be called as "  
    8.                         + "part of a callback while a frame is in progress.");  
    9.             }  
    10.             return USE_FRAME_TIME ? mLastFrameTimeNanos : System.nanoTime();  
    11.         }  
    12.     }  

    doAnimationFrame()总结就是

    1.遍历pending list动画,如果delay为0 则调用start,不为0,加入delay list;

    2.遍历delay list, 根据frametime计算是继续delay还是ready可以播放,若是ready,则加入到ready list中;

    3 遍历ready list,调用start ;

    4,遍历所有animation,根据frametime计算动画是否要结束,如果可以结束,则加入到ending list中;

    5,遍历ending list, 调用end;

    6, 如果有列表中仍然有动画,则继续scheduleAnimation;

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. private void doAnimationFrame(long frameTime) {  
    2.     // mPendingAnimations holds any animations that have requested to be started  
    3.     // We're going to clear mPendingAnimations, but starting animation may  
    4.     // cause more to be added to the pending list (for example, if one animation  
    5.     // starting triggers another starting). So we loop until mPendingAnimations  
    6.     // is empty.  
    7.     while (mPendingAnimations.size() > 0) {  
    8.         ArrayList<ValueAnimator> pendingCopy =  
    9.                 (ArrayList<ValueAnimator>) mPendingAnimations.clone();  
    10.         mPendingAnimations.clear();  
    11.         int count = pendingCopy.size();  
    12.         for (int i = 0; i < count; ++i) {  
    13.             ValueAnimator anim = pendingCopy.get(i);  
    14.             // If the animation has a startDelay, place it on the delayed list  
    15.             if (anim.mStartDelay == 0) {  
    16.                 anim.startAnimation(this);  
    17.             } else {  
    18.                 mDelayedAnims.add(anim);  
    19.             }  
    20.         }  
    21.     }  
    22.     // Next, process animations currently sitting on the delayed queue, adding  
    23.     // them to the active animations if they are ready  
    24.     int numDelayedAnims = mDelayedAnims.size();  
    25.     for (int i = 0; i < numDelayedAnims; ++i) {  
    26.         ValueAnimator anim = mDelayedAnims.get(i);  
    27.         if (anim.delayedAnimationFrame(frameTime)) {  
    28.             mReadyAnims.add(anim);  
    29.         }  
    30.     }  
    31.     int numReadyAnims = mReadyAnims.size();  
    32.     if (numReadyAnims > 0) {  
    33.         for (int i = 0; i < numReadyAnims; ++i) {  
    34.             ValueAnimator anim = mReadyAnims.get(i);  
    35.             anim.startAnimation(this);  
    36.             anim.mRunning = true;  
    37.             mDelayedAnims.remove(anim);  
    38.         }  
    39.         mReadyAnims.clear();  
    40.     }  
    41.   
    42.     // Now process all active animations. The return value from animationFrame()  
    43.     // tells the handler whether it should now be ended  
    44.     int numAnims = mAnimations.size();  
    45.     for (int i = 0; i < numAnims; ++i) {  
    46.         mTmpAnimations.add(mAnimations.get(i));  
    47.     }  
    48.     for (int i = 0; i < numAnims; ++i) {  
    49.         ValueAnimator anim = mTmpAnimations.get(i);  
    50.         if (mAnimations.contains(anim) && anim.doAnimationFrame(frameTime)) {  
    51.             mEndingAnims.add(anim);  
    52.         }  
    53.     }  
    54.     mTmpAnimations.clear();  
    55.     if (mEndingAnims.size() > 0) {  
    56.         for (int i = 0; i < mEndingAnims.size(); ++i) {  
    57.             mEndingAnims.get(i).endAnimation(this);  
    58.         }  
    59.         mEndingAnims.clear();  
    60.     }  
    61.   
    62.     // If there are still active or delayed animations, schedule a future call to  
    63.     // onAnimate to process the next frame of the animations.  
    64.     if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) {  
    65.         scheduleAnimation();  
    66.     }  
    67. }  


    在animationFrame() 中根据当前状态,并且计算fraction,调用animateValue();

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1.     boolean animationFrame(long currentTime) {  
    2.         boolean done = false;  
    3.         switch (mPlayingState) {  
    4.         case RUNNING:  
    5.         case SEEKED:  
    6.             //省略计算fraction的代码  
    7.             animateValue(fraction);  
    8.             break;  
    9.         }  
    10.         return done;  
    11.     }  

    通过mInterpolator.getInterpolation计算fraction;@Interpolator 

    根据fraction计算内部所有value,如果有updateListener,调用之。

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. void animateValue(float fraction) {  
    2.     fraction = mInterpolator.getInterpolation(fraction);  
    3.     mCurrentFraction = fraction;  
    4.     int numValues = mValues.length;  
    5.     for (int i = 0; i < numValues; ++i) {  
    6.         mValues[i].calculateValue(fraction);  
    7.     }  
    8.     if (mUpdateListeners != null) {  
    9.         int numListeners = mUpdateListeners.size();  
    10.         for (int i = 0; i < numListeners; ++i) {  
    11.             mUpdateListeners.get(i).onAnimationUpdate(this);  
    12.         }  
    13.     }  
    14. }  

    3. 插值器

    从上面的介绍可以看到,Interpolator的关键是getInterpolation();

    在ValueAnimator.animationFrame()中可以看到, 传递给Interpolator 的fraction是在[0,1] 值域范围。

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;  
    2. if (fraction >= 1f) {  
    3.     if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) {  
    4.         // Time to repeat  
    5.         if (mListeners != null) {  
    6.             int numListeners = mListeners.size();  
    7.             for (int i = 0; i < numListeners; ++i) {  
    8.                 mListeners.get(i).onAnimationRepeat(this);  
    9.             }  
    10.         }  
    11.         if (mRepeatMode == REVERSE) {  
    12.             mPlayingBackwards = !mPlayingBackwards;  
    13.         }  
    14.         mCurrentIteration += (int)fraction;  
    15.         fraction = fraction % 1f;  
    16.         mStartTime += mDuration;  
    17.     } else {  
    18.         done = true;  
    19.         fraction = Math.min(fraction, 1.0f);  
    20.     }  
    21. }  
    22. if (mPlayingBackwards) {  
    23.     fraction = 1f - fraction;  
    24. }  


    所以设计Interpolator 就是设计一个输入[0,1] 的函数。

    先参观一下系统的几个Interpolator。

    3.1 AccelerateDecelerateInterpolator 

    cos(t+1)Pi /2 +0.5f

    从图可以看到,先加速后减速,病最终到达结束位置。

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. public class AccelerateDecelerateInterpolator implements Interpolator {  
    2.     public float getInterpolation(float input) {  
    3.         return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;  
    4.     }  
    5. }  



    3.2 AccelerateInterpolator 

    如果factor=1 则函数为x^2

    否则函数为x^a (a 是参数)

    默认函数式x^2

    如图示,逐渐加速到结束位置。

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. public class AccelerateInterpolator implements Interpolator {  
    2.     private final float mFactor;  
    3.     private final double mDoubleFactor;  
    4.   
    5.     public AccelerateInterpolator() {  
    6.         mFactor = 1.0f;  
    7.         mDoubleFactor = 2.0;  
    8.     }  
    9.       
    10.     /** 
    11.      * Constructor 
    12.      *  
    13.      * @param factor Degree to which the animation should be eased. Seting 
    14.      *        factor to 1.0f produces a y=x^2 parabola. Increasing factor above 
    15.      *        1.0f  exaggerates the ease-in effect (i.e., it starts even 
    16.      *        slower and ends evens faster) 
    17.      */  
    18.     public AccelerateInterpolator(float factor) {  
    19.         mFactor = factor;  
    20.         mDoubleFactor = 2 * mFactor;  
    21.     }  
    22.   
    23.      public float getInterpolation(float input) {  
    24.         if (mFactor == 1.0f) {  
    25.             return input * input;  
    26.         } else {  
    27.             return (float)Math.pow(input, mDoubleFactor);  
    28.         }  
    29.     }  
    30. }  



    3.3 LinearInterpolator 

    线性的就是Y=X 没啥说的。

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. public class LinearInterpolator implements Interpolator {  
    2.     public float getInterpolation(float input) {  
    3.         return input;  
    4.     }  
    5. }  



    3.4 anticipateInterpolator  

    函数是:x^2((a+1)x-a) 默认参数a=2 默认函数为x^2(3x-1)

    如图示, 会先反方向执行一段,然后正向一直加速至结束位置。

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. public class AnticipateInterpolator implements Interpolator {  
    2.     private final float mTension;  
    3.   
    4.     public AnticipateInterpolator() {  
    5.         mTension = 2.0f;  
    6.     }  
    7.   
    8.     /** 
    9.      * @param tension Amount of anticipation. When tension equals 0.0f, there is 
    10.      *                no anticipation and the interpolator becomes a simple 
    11.      *                acceleration interpolator. 
    12.      */  
    13.     public AnticipateInterpolator(float tension) {  
    14.         mTension = tension;  
    15.     }  
    16.   
    17.     public float getInterpolation(float t) {  
    18.         // a(t) = t * t * ((tension + 1) * t - tension)  
    19.         return t * t * ((mTension + 1) * t - mTension);  
    20.     }  
    21. }  



    3.5 aniticipateOvershoot 

    是一个分段函数,默认参数a=3

    2x*x[(2x*(a+1)-a)]     0<=x<=0.5

    2(x-1)(x-1)[(2x-1)(a+1)+a]    0.5<x<=1

    通过下图可以看到,动画会先反方向执行,然后向正方向逐渐加速,在快结束时逐渐减速,并超过预设的值,最后回到结束位置。


    2x*x[(2x*(a+1)-a)]     0<=x<=0.5 的函数图


    2(x-1)(x-1)[(2x-1)(a+1)+a]    0.5<x<=1的函数图

    [java] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. public class AnticipateOvershootInterpolator implements Interpolator {  
    2.     private final float mTension;  
    3.   
    4.     public AnticipateOvershootInterpolator() {  
    5.         mTension = 2.0f * 1.5f;  
    6.     }  
    7.   
    8.     /** 
    9.      * @param tension Amount of anticipation/overshoot. When tension equals 0.0f, 
    10.      *                there is no anticipation/overshoot and the interpolator becomes 
    11.      *                a simple acceleration/deceleration interpolator. 
    12.      */  
    13.     public AnticipateOvershootInterpolator(float tension) {  
    14.         mTension = tension * 1.5f;  
    15.     }  
    16.   
    17.     /** 
    18.      * @param tension Amount of anticipation/overshoot. When tension equals 0.0f, 
    19.      *                there is no anticipation/overshoot and the interpolator becomes 
    20.      *                a simple acceleration/deceleration interpolator. 
    21.      * @param extraTension Amount by which to multiply the tension. For instance, 
    22.      *                     to get the same overshoot as an OvershootInterpolator with 
    23.      *                     a tension of 2.0f, you would use an extraTension of 1.5f. 
    24.      */  
    25.     public AnticipateOvershootInterpolator(float tension, float extraTension) {  
    26.         mTension = tension * extraTension;  
    27.     }  
    28.   
    29.     private static float a(float t, float s) {  
    30.         return t * t * ((s + 1) * t - s);  
    31.     }  
    32.   
    33.     private static float o(float t, float s) {  
    34.         return t * t * ((s + 1) * t + s);  
    35.     }  
    36.   
    37.     public float getInterpolation(float t) {  
    38.         // a(t, s) = t * t * ((s + 1) * t - s)  
    39.         // o(t, s) = t * t * ((s + 1) * t + s)  
    40.         // f(t) = 0.5 * a(t * 2, tension * extraTension), when t < 0.5  
    41.         // f(t) = 0.5 * (o(t * 2 - 2, tension * extraTension) + 2), when t <= 1.0  
    42.         if (t < 0.5f) return 0.5f * a(t * 2.0f, mTension);  
    43.         else return 0.5f * (o(t * 2.0f - 2.0f, mTension) + 2.0f);  
    44.     }  
    45. }  

    4. 指导设计动画。

    从第3节中可以看到,想要让动画按照我们预期的行为来执行,需要做的就是找到合适的函数。

    画图使用http://www.fooplot.com/在线工具

  • 相关阅读:
    CAFFE安装(3):cuDNN v4
    监测查询性能(1)
    SQL Server 中的三种分页方式
    使用DBCC SHOW_STATISTICS展示索引的统计信息
    查询表的分配单元数据
    Node.js中的事件
    node-mysql中的连接池代码学习
    Excel动态生成JSON
    使用SignalR实现比特币价格实时刷新
    使用Async同步执行异步函数
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/4379968.html
Copyright © 2020-2023  润新知