屬性動畫(ObjectAnimator)

  1. 屬性動畫的集成關系


    Paste_Image.png
  2. 看一段屬性動畫的使用代碼
ObjectAnimator animator=ObjectAnimator.ofFloat(image,"rotationX",0f,360f);
        animator.setDuration(2000);//執行時間
        animator.setInterpolator(new LinearInterpolator());//插值器
        animator.setRepeatCount(-1);//-1 代表無限循環執行
        animator.start();
  1. 跟進ObjectAnimator.ofFloat(image,"rotationX",0f,360f)
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
        ObjectAnimator anim = new ObjectAnimator(target, propertyName);
        anim.setFloatValues(values);
        return anim;
    }
  1. 繼續進入anim.setFloatValues(values);
 @Override
    public void setFloatValues(float... values) {
        if (mValues == null || mValues.length == 0) {
            // No values yet - this animator is being constructed piecemeal. Init the values with
            // whatever the current propertyName is
            if (mProperty != null) {
                setValues(PropertyValuesHolder.ofFloat(mProperty, values));
            } else {
                setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
            }
        } else {
            super.setFloatValues(values);
        }
    }
  1. 進入PropertyValuesHolder.ofFloat(mPropertyName, values)
public static PropertyValuesHolder ofFloat(String propertyName, float... values) {
        return new FloatPropertyValuesHolder(propertyName, values);
    }

這里出現了 PropertyValuesHolder 那么這個類是干嘛的呢?

PropertyValuesHolder:顧名思義,就是屬性值持有者,它保存了動畫過程中所需要操作的屬性和對應的值,我們通過ofFloat(Object target, String propertyName, float… values)構造的動畫,ofFloat()的內部實現其實就是將傳進來的參數封裝成PropertyValuesHolder實例來保存動畫狀態。在封裝成PropertyValuesHolder實例以后,后面的操作也是以PropertyValuesHolder為主的

  1. 我們看到返回一個new FloatPropertyValuesHolder(propertyName, values) 進入構造方法
        public FloatPropertyValuesHolder(String propertyName, float... values) {
            super(propertyName);
            setFloatValues(values);
        }
//進入  super(propertyName) 看到只是跟PropertyValuesHolder的成員變量賦值
private PropertyValuesHolder(String propertyName) {
        mPropertyName = propertyName;
    }
  1. 進入 setFloatValues(values)
@Override
        public void setFloatValues(float... values) {
            super.setFloatValues(values);
            mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
        }
//進入super.setFloatValues(values);
  public void setFloatValues(float... values) {
        mValueType = float.class;
        mKeyframes = KeyframeSet.ofFloat(values);
    }

KeyframeSet.ofFloat(values);
Keyframe:意為關鍵幀,設置了關鍵幀后,動畫就可以在各個關鍵幀之間平滑過渡的,一個關鍵幀必須包含兩個原素,第一時間點,第二位置,即這個關鍵幀是表示的是某個物體在哪個時間點應該在哪個位置上。fraction表示當前進度,value表示當前位置。

  1. 我們進入KeyframeSet.ofFloat(values)
public static KeyframeSet ofFloat(float... values) {
//這里的values 參數就是我們所傳遞動畫執行參數比如:
//ObjectAnimator.ofFloat(image,"rotationX",0f,360f);中的0f ,360f
        boolean badValue = false;
        int numKeyframes = values.length;
//生成最小2個長度大小的數組
        FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)];
//單我們傳遞的參數只有一個的時候
        if (numKeyframes == 1) {
          //設置默認數組的元素的一個為0f
            keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
            keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
            if (Float.isNaN(values[0])) {
                badValue = true;
            }
        } else {
        //如果不是一個參數
            keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
        //就遍歷數組
            for (int i = 1; i < numKeyframes; ++i) {
                keyframes[i] =
                        (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
                if (Float.isNaN(values[i])) {
                    badValue = true;
                }
            }
        }
        if (badValue) {
            Log.w("Animator", "Bad value (NaN) in float animator");
        }
        //將生成的數組添加到List集合
        return new FloatKeyframeSet(keyframes);
    }
  1. 接上一步進入new FloatKeyframeSet(keyframes);構造函數
public FloatKeyframeSet(FloatKeyframe... keyframes) {
        super(keyframes);
    }
//在進入super(keyframes) 父類 KeyframeSet
class KeyframeSet implements Keyframes {
    int mNumKeyframes;
    Keyframe mFirstKeyframe;
    Keyframe mLastKeyframe;
    TimeInterpolator mInterpolator; // only used in the 2-keyframe case
    List<Keyframe> mKeyframes; // only used when there are not 2 keyframes
    TypeEvaluator mEvaluator;
    public KeyframeSet(Keyframe... keyframes) {
        mNumKeyframes = keyframes.length;
        // 將數組添加mKeyframes集合中 里面保存的是動畫每的一幀
        mKeyframes = Arrays.asList(keyframes);
        mFirstKeyframe = keyframes[0];
        mLastKeyframe = keyframes[mNumKeyframes - 1];
        mInterpolator = mLastKeyframe.getInterpolator();
    }

由上可以看出:
PropertyValuesHolder 它保存了動畫過程中所需要操作的屬性和對應的值 同時持有KeyframeSet.ofFloat(values)得到動畫每一幀的集合

  1. 我去再去屬性動畫的其他屬性設置看看
//設置執行時間
@Override
    @NonNull
    public ObjectAnimator setDuration(long duration) {
        super.setDuration(duration);
        return this;
    }
//進入super.setDuration(duration);
 @Override
    public ValueAnimator setDuration(long duration) {
        if (duration < 0) {
            throw new IllegalArgumentException("Animators cannot have negative duration: " +
                    duration);
        }
        mUnscaledDuration = duration;
        updateScaledDuration();
        return this;
    }
    //設置執行時間 函數 sDurationScale=1.0f
    private void updateScaledDuration() {
        mDuration = (long)(mUnscaledDuration * sDurationScale);
    }
//設置插值器
@Override
    public void setInterpolator(TimeInterpolator value) {
        if (value != null) {
            mInterpolator = value;
        } else {
          //如果為空 就使用默認的插值器 線性
            mInterpolator = new LinearInterpolator();
        }
    }
  1. 最后我們來關注重點 start()方法
 @Override
    public void start() {
        // See if any of the current active/pending animators need to be canceled
        AnimationHandler handler = sAnimationHandler.get();
        if (handler != null) {
            int numAnims = handler.mAnimations.size();
            for (int i = numAnims - 1; i >= 0; i--) {
                if (handler.mAnimations.get(i) instanceof ObjectAnimator) {
                    ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i);
                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                        anim.cancel();
                    }
                }
            }
            numAnims = handler.mPendingAnimations.size();
            for (int i = numAnims - 1; i >= 0; i--) {
                if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
                    ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                        anim.cancel();
                    }
                }
            }
            numAnims = handler.mDelayedAnims.size();
            for (int i = numAnims - 1; i >= 0; i--) {
                if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
                    ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                        anim.cancel();
                    }
                }
            }
        }
        if (DBG) {
            Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
            for (int i = 0; i < mValues.length; ++i) {
                PropertyValuesHolder pvh = mValues[i];
                Log.d(LOG_TAG, "   Values[" + i + "]: " +
                    pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
                    pvh.mKeyframes.getValue(1));
            }
        }
        super.start();
    }
**super.start();**調用父類的start()方法
 @Override
    public void start() {
        start(false);
    }
//在接著跟進
private void start(boolean playBackwards) {
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        mReversing = playBackwards;
        // Special case: reversing from seek-to-0 should act as if not seeked at all.
        if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
            if (mRepeatCount == INFINITE) {
                // Calculate the fraction of the current iteration.
                float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
                mSeekFraction = 1 - fraction;
            } else {
                mSeekFraction = 1 + mRepeatCount - mSeekFraction;
            }
        }
        mStarted = true;
        mPaused = false;
        mRunning = false;
        // Resets mLastFrameTime when start() is called, so that if the animation was running,
        // calling start() would put the animation in the
        // started-but-not-yet-reached-the-first-frame phase.
        mLastFrameTime = 0;
        AnimationHandler animationHandler = AnimationHandler.getInstance();
        //這里每隔16毫秒會回調一次該回調方法 這里有一個this 
        animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale));

        if (mStartDelay == 0 || mSeekFraction >= 0) {
            // If there's no start delay, init the animation and notify start listeners right away
            // to be consistent with the previous behavior. Otherwise, postpone this until the first
            // frame after the start delay.
            startAnimation();
            if (mSeekFraction == -1) {
                // No seek, start at play time 0. Note that the reason we are not using fraction 0
                // is because for animations with 0 duration, we want to be consistent with pre-N
                // behavior: skip to the final value immediately.
                setCurrentPlayTime(0);
            } else {
                setCurrentFraction(mSeekFraction);
            }
        }
    }
Paste_Image.png
  1. 上面我們看到 addAnimationFrameCallback()中傳遞了一個this 說明ValueAnimator也實現了該接口 且是循環的不斷調用該抽象回調方法


    Paste_Image.png

    果然實現了該接口 我們去看看該接口有什么抽象方法

//該抽象方法有2個 
 interface AnimationFrameCallback {
        void doAnimationFrame(long frameTime);
        void commitAnimationFrame(long frameTime);
    }
//抽象方法的實現
 * Processes a frame of the animation, adjusting the start time if needed.
     *
     * @param frameTime The frame time.
     * @return true if the animation has ended.
     * @hide
     */
//參數為繪制一幀的時間(每隔16毫秒調用一次) 監聽手機發出的Vsync信號 
    public final void doAnimationFrame(long frameTime) {
        AnimationHandler handler = AnimationHandler.getInstance();
        if (mLastFrameTime == 0) {
            // First frame
            handler.addOneShotCommitCallback(this);
            if (mStartDelay > 0) {
                startAnimation();
            }
            if (mSeekFraction < 0) {
                mStartTime = frameTime;
            } else {
                long seekTime = (long) (getScaledDuration() * mSeekFraction);
                mStartTime = frameTime - seekTime;
                mSeekFraction = -1;
            }
            mStartTimeCommitted = false; // allow start time to be compensated for jank
        }
        mLastFrameTime = frameTime;
        if (mPaused) {
            mPauseTime = frameTime;
            handler.removeCallback(this);
            return;
        } else if (mResumed) {
            mResumed = false;
            if (mPauseTime > 0) {
                // Offset by the duration that the animation was paused
                mStartTime += (frameTime - mPauseTime);
                mStartTimeCommitted = false; // allow start time to be compensated for jank
            }
            handler.addOneShotCommitCallback(this);
        }
        final long currentTime = Math.max(frameTime, mStartTime);
        boolean finished = animateBasedOnTime(currentTime);
        if (finished) {
            endAnimation();
        }
    }
  1. boolean finished = animateBasedOnTime(currentTime);將相對時間轉換為絕對時間的百分比
boolean animateBasedOnTime(long currentTime) {
        boolean done = false;
        if (mRunning) {
            final long scaledDuration = getScaledDuration();
            //(總時間-開始時間)/運行的時間 得出運行百分比
            final float fraction = scaledDuration > 0 ?
                    (float)(currentTime - mStartTime) / scaledDuration : 1f;
            final float lastFraction = mOverallFraction;
            final boolean newIteration = (int) fraction > (int) lastFraction;
            final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
                    (mRepeatCount != INFINITE);
            if (scaledDuration == 0) {
                // 0 duration animator, ignore the repeat count and skip to the end
                done = true;
            } else if (newIteration && !lastIterationFinished) {
                // Time to repeat
                if (mListeners != null) {
                    //mListeners監聽集合
                    int numListeners = mListeners.size();
                    for (int i = 0; i < numListeners; ++i) {
                      //觀察者模式 監聽回調 動畫重復執行
                        mListeners.get(i).onAnimationRepeat(this);
                    }
                }
            } else if (lastIterationFinished) {
                done = true;
            }
            mOverallFraction = clampFraction(fraction);
            float currentIterationFraction = getCurrentIterationFraction(mOverallFraction);
            //這里調用ObjectAnimator子類的animateValue()函數
            animateValue(currentIterationFraction);
        }
        return done;
    }
  1. 進入ObjectAnimator子類的animateValue()
@CallSuper
    @Override
    //參數執行百分比(0-1)
    void animateValue(float fraction) {
        final Object target = getTarget();
        if (mTarget != null && target == null) {
            // We lost the target reference, cancel and clean up.
            cancel();
            return;
        }
        //調用父類animateValue()方法
        super.animateValue(fraction);
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
              //這里設置View的寬高 target代表動畫控件
            mValues[i].setAnimatedValue(target);
        }
    }
//進入父類的animateValue();
@CallSuper
    void animateValue(float fraction) {
      //調用插值器的方法 (策略模式) 得到插值器運行百分比
        fraction = mInterpolator.getInterpolation(fraction);
      //賦值成員變量
        mCurrentFraction = fraction;
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
            //調用PropertyValuesHolder 的calculateValue();
            mValues[i].calculateValue(fraction);
        }
        if (mUpdateListeners != null) {
            int numListeners = mUpdateListeners.size();
            for (int i = 0; i < numListeners; ++i) {
              //動畫更新監聽回調回調
                mUpdateListeners.get(i).onAnimationUpdate(this);
            }
        }
    }
//抽象插值器
public interface TimeInterpolator {
    float getInterpolation(float input);
}
//PropertyValuesHolder 的calculateValue
 void calculateValue(float fraction) {
        Object value = mKeyframes.getValue(fraction);
        //返回執行動畫的百分比
        mAnimatedValue = mConverter == null ? value : mConverter.convert(value);
    }
//接上面mAnimatedValue返回后回到 ObjectAnimator的 animateValue()繼續執行來到
 int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
              //這里設置View的寬高 target代表動畫控件
            mValues[i].setAnimatedValue(target);
        }
//進入 mValues[i].setAnimatedValue(target);
void setAnimatedValue(Object target) {
        if (mProperty != null) {
            mProperty.set(target, getAnimatedValue());
        }
        if (mSetter != null) {
            try {
                mTmpValueArray[0] = getAnimatedValue();
                //mSetter是在startAnimator()初始化的時候通過反射賦值
                //invoke 重繪 動畫view
                mSetter.invoke(target, mTmpValueArray);
            } catch (InvocationTargetException e) {
                Log.e("PropertyValuesHolder", e.toString());
            } catch (IllegalAccessException e) {
                Log.e("PropertyValuesHolder", e.toString());
            }
        }
    }
//其中mSetter 是在動畫初始化的時候賦值ObjectAnimator
 @CallSuper
    @Override
    void initAnimation() {
        if (!mInitialized) {
            // mValueType may change due to setter/getter setup; do this before calling super.init(),
            // which uses mValueType to set up the default type evaluator.
            final Object target = getTarget();
            if (target != null) {
                final int numValues = mValues.length;
                for (int i = 0; i < numValues; ++i) {
                    mValues[i].setupSetterAndGetter(target);
                }
            }
            super.initAnimation();
        }
    }
//其中mValues[i].setupSetterAndGetter(target); PropertyValuesHolder
void setupSetterAndGetter(Object target) {
        mKeyframes.invalidateCache();
        if (mProperty != null) {
            // check to make sure that mProperty is on the class of target
            try {
                Object testValue = null;
                List<Keyframe> keyframes = mKeyframes.getKeyframes();
                int keyframeCount = keyframes == null ? 0 : keyframes.size();
                for (int i = 0; i < keyframeCount; i++) {
                    Keyframe kf = keyframes.get(i);
                    if (!kf.hasValue() || kf.valueWasSetOnStart()) {
                        if (testValue == null) {
                            testValue = convertBack(mProperty.get(target));
                        }
                        kf.setValue(testValue);
                        kf.setValueWasSetOnStart(true);
                    }
                }
                return;
            } catch (ClassCastException e) {
                Log.w("PropertyValuesHolder","No such property (" + mProperty.getName() +
                        ") on target object " + target + ". Trying reflection instead");
                mProperty = null;
            }
        }
        // We can't just say 'else' here because the catch statement sets mProperty to null.
        if (mProperty == null) {
            Class targetClass = target.getClass();
            if (mSetter == null) {
                setupSetter(targetClass);
            }
            List<Keyframe> keyframes = mKeyframes.getKeyframes();
            int keyframeCount = keyframes == null ? 0 : keyframes.size();
            for (int i = 0; i < keyframeCount; i++) {
                Keyframe kf = keyframes.get(i);
                if (!kf.hasValue() || kf.valueWasSetOnStart()) {
                    if (mGetter == null) {
                        setupGetter(targetClass);
                        if (mGetter == null) {
                            // Already logged the error - just return to avoid NPE
                            return;
                        }
                    }
                    try {
                        Object value = convertBack(mGetter.invoke(target));
                        kf.setValue(value);
                        kf.setValueWasSetOnStart(true);
                    } catch (InvocationTargetException e) {
                        Log.e("PropertyValuesHolder", e.toString());
                    } catch (IllegalAccessException e) {
                        Log.e("PropertyValuesHolder", e.toString());
                    }
                }
            }
        }
    }
其中: if (mSetter == null) {setupSetter(targetClass);}
private void setupGetter(Class targetClass) {
        mGetter = setupSetterOrGetter(targetClass, sGetterPropertyMap, "get", null);
    }
//繼續深入
private Method setupSetterOrGetter(Class targetClass,
            HashMap<Class, HashMap<String, Method>> propertyMapMap,
            String prefix, Class valueType) {
        Method setterOrGetter = null;
        synchronized(propertyMapMap) {
            // Have to lock property map prior to reading it, to guard against
            // another thread putting something in there after we've checked it
            // but before we've added an entry to it
            HashMap<String, Method> propertyMap = propertyMapMap.get(targetClass);
            boolean wasInMap = false;
            if (propertyMap != null) {
                wasInMap = propertyMap.containsKey(mPropertyName);
                if (wasInMap) {
                    setterOrGetter = propertyMap.get(mPropertyName);
                }
            }
            if (!wasInMap) {
                setterOrGetter = getPropertyFunction(targetClass, prefix, valueType);
                if (propertyMap == null) {
                    propertyMap = new HashMap<String, Method>();
                    propertyMapMap.put(targetClass, propertyMap);
                }
                propertyMap.put(mPropertyName, setterOrGetter);
            }
        }
        return setterOrGetter;
    }
//其中: setterOrGetter = getPropertyFunction(targetClass, prefix, valueType);
 private Method getPropertyFunction(Class targetClass, String prefix, Class valueType) {
        // TODO: faster implementation...
        Method returnVal = null;
        String methodName = getMethodName(prefix, mPropertyName);
        Class args[] = null;
        if (valueType == null) {
            try {
                //這里通過反射獲取動畫方法
                returnVal = targetClass.getMethod(methodName, args);
            } catch (NoSuchMethodException e) {
                // Swallow the error, log it later
            }
        } else {
            args = new Class[1];
            Class typeVariants[];
            if (valueType.equals(Float.class)) {
                typeVariants = FLOAT_VARIANTS;
            } else if (valueType.equals(Integer.class)) {
                typeVariants = INTEGER_VARIANTS;
            } else if (valueType.equals(Double.class)) {
                typeVariants = DOUBLE_VARIANTS;
            } else {
                typeVariants = new Class[1];
                typeVariants[0] = valueType;
            }
            for (Class typeVariant : typeVariants) {
                args[0] = typeVariant;
                try {
                    returnVal = targetClass.getMethod(methodName, args);
                    if (mConverter == null) {
                        // change the value type to suit
                        mValueType = typeVariant;
                    }
                    return returnVal;
                } catch (NoSuchMethodException e) {
                    // Swallow the error and keep trying other variants
                }
            }
            // If we got here, then no appropriate function was found
        }

        if (returnVal == null) {
            Log.w("PropertyValuesHolder", "Method " +
                    getMethodName(prefix, mPropertyName) + "() with type " + valueType +
                    " not found on target class " + targetClass);
        }

        return returnVal;
    }
屬性動畫的原理就是不斷調用view.setScrollX() view.setScrollY()
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容

  • 這段時間正好要做些動畫,于是把屬性動畫重新學習了一遍,做些總結 1. 前言 Android動畫分為Frame An...
    夢sora閱讀 3,980評論 0 9
  • 對于android手機上的動畫實現主要有三種,一種是幀動畫,一種是View動畫,以及3.0以上提供的屬性動畫,所有...
    查理吃西瓜閱讀 6,527評論 1 39
  • Animation Animation類是所有動畫(scale、alpha、translate、rotate)的基...
    四月一號閱讀 1,930評論 0 10
  • 媽媽我想當勞動委員,勞動委員是干什么的?我告訴她,是和同學們一起把班級衛生打掃干凈。
    時凱旋閱讀 213評論 0 0
  • 花也愁, 葉也愁, 唯有秋風自風流, 江山點點秋。 歌也休, 舞也休, 無限秋思落心頭, 花殘人空瘦。
    tulipjia閱讀 455評論 7 20