Android Animator 源码分析
<p>下面分析下Animator在Framework层的实现</p> <h2>从ObjectAnimator.ofFloat()开始</h2> <pre> <code class="language-java">public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) { ObjectAnimator anim = new ObjectAnimator(target, propertyName); anim.setFloatValues(values); return anim; }</code></pre> <p>这个工厂方法会创建一个ObjectAnimator对象,在创建时同时设置属性动画的目标和属性名</p> <pre> <code class="language-java">private ObjectAnimator(Object target, String propertyName) { setTarget(target); setPropertyName(propertyName); }</code></pre> <pre> <code class="language-java">// 设置目标对象 @Override public void setTarget(@Nullable Object target) { final Object oldTarget = getTarget(); if (oldTarget != target) { if (isStarted()) { cancel(); } // target必须是一个弱引用对象 mTarget = target == null ? null : new WeakReference<Object>(target); // New target should cause re-initialization prior to starting mInitialized = false; // 记录尚未初始化,ValueAnimator的标志位,一会要用 } }</code></pre> <pre> <code class="language-java">// 设置属性名称 public void setPropertyName(@NonNull String propertyName) { // mValues could be null if this is being constructed piecemeal. Just record the // propertyName to be used later when setValues() is called if so. if (mValues != null) { // 属性值的更新操作委托给PropertyValuesHolder进行 // Animator只进行数值计算 PropertyValuesHolder valuesHolder = mValues[0]; String oldName = valuesHolder.getPropertyName(); valuesHolder.setPropertyName(propertyName); mValuesMap.remove(oldName); mValuesMap.put(propertyName, valuesHolder); } mPropertyName = propertyName; // New property/values/target should cause re-initialization prior to starting mInitialized = false; }</code></pre> <p>ofFloat还有一步就是调用这个方法</p> <pre> <code class="language-java">@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 // 这是mProperty是为null的 if (mProperty != null) { setValues(PropertyValuesHolder.ofFloat(mProperty, values)); } else { setValues(PropertyValuesHolder.ofFloat(mPropertyName, values)); } } else { super.setFloatValues(values); } }</code></pre> <p>setValues这个过程有点长,按照顺序先写下吧</p> <pre> <code class="language-java">// PropertyValueHolder中 // 获取PropertyValuesHolder public static PropertyValuesHolder ofFloat(String propertyName, float... values) { // 创建子类 return new FloatPropertyValuesHolder(propertyName, values); } public FloatPropertyValuesHolder(String propertyName, float... values) { super(propertyName); setFloatValues(values); } @Override public void setFloatValues(float... values) { // 这个过程会取得value的类型 super.setFloatValues(values); mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes; } // super.setFloatValues() // 取得Value的类型 public void setFloatValues(float... values) { mValueType = float.class; mKeyframes = KeyframeSet.ofFloat(values); }</code></pre> <p>然后设置KeyFrame了,KeyFrame时属性动画中的关键帧,通过设置关键帧来保证动画执行时序性</p> <pre> <code class="language-java">// ~KeyFrameSet中 public static KeyframeSet ofFloat(float... values) { boolean badValue = false; int numKeyframes = values.length; FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)]; // 如果获取只有一个数值,那么就只有开始和结束两个关键帧 if (numKeyframes == 1) { // 实际创建关键帧 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"); } // 创建一个关键帧集合 return new FloatKeyframeSet(keyframes); }</code></pre> <p>接下来看关键帧怎么创建的</p> <pre> <code class="language-java">//keyFrame中 public static Keyframe ofFloat(float fraction) { return new FloatKeyframe(fraction); } // FloatKeyFrame中 FloatKeyframe(float fraction) { mFraction = fraction; mValueType = float.class; }</code></pre> <p>仔细看了看,发现KeyFrame其实只是对当前的value和fraction进行一个记录,不同类型的KeyFrame会设置不同的mValueType</p> <p>KeyFrame中的属性值,一会回来再看</p> <pre> <code class="language-java">/** * Flag to indicate whether this keyframe has a valid value. This flag is used when an * animation first starts, to populate placeholder keyframes with real values derived * from the target object. */ boolean mHasValue; /** * Flag to indicate whether the value in the keyframe was read from the target object or not. * If so, its value will be recalculated if target changes. */ boolean mValueWasSetOnStart; /** * The time at which mValue will hold true. */ float mFraction; /** * The type of the value in this Keyframe. This type is determined at construction time, * based on the type of the <code>value</code> object passed into the constructor. */ Class mValueType; /** * The optional time interpolator for the interval preceding this keyframe. A null interpolator * (the default) results in linear interpolation over the interval. */ private TimeInterpolator mInterpolator = null;</code></pre> <p>回来看哪个keyframe的ofFloat方法,最终会创建一个关键帧集合</p> <pre> <code class="language-java">public KeyframeSet(Keyframe... keyframes) { mNumKeyframes = keyframes.length; // immutable list mKeyframes = Arrays.asList(keyframes); mFirstKeyframe = keyframes[0]; mLastKeyframe = keyframes[mNumKeyframes - 1]; mInterpolator = mLastKeyframe.getInterpolator(); }</code></pre> <p>ObjectAnimator的ofFloat过程就结束了,下面看下其他方法</p> <h2>setDuration()</h2> <p>设置动画的执行时间</p> <p>这个就是将执行时间写入属性中,一会会用到</p> <pre> <code class="language-java">@Override public ValueAnimator setDuration(long duration) { if (duration < 0) { throw new IllegalArgumentException("Animators cannot have negative duration: " + duration); } mDuration = duration; return this; }</code></pre> <h2>setInterpolator()</h2> <p>设置插值器,默认的插值器是带有加速度的</p> <pre> <code class="language-java">// The time interpolator to be used if none is set on the animation private static final TimeInterpolator sDefaultInterpolator = new AccelerateDecelerateInterpolator();</code></pre> <pre> <code class="language-java">@Override public void setInterpolator(TimeInterpolator value) { if (value != null) { mInterpolator = value; } else { mInterpolator = new LinearInterpolator(); } }</code></pre> <h2>setEvaluator()</h2> <p>设置估值器</p> <p>估值器实际上是在KeyFrame中使用的</p> <pre> <code class="language-java">public void setEvaluator(TypeEvaluator value) { if (value != null && mValues != null && mValues.length > 0) { mValues[0].setEvaluator(value); } }</code></pre> <pre> <code class="language-java">public void setEvaluator(TypeEvaluator evaluator) { mEvaluator = evaluator; mKeyframes.setEvaluator(evaluator); }</code></pre> <pre> <code class="language-java">//KeyFrameSet中 public void setEvaluator(TypeEvaluator evaluator) { // 使用属性值进行保存 mEvaluator = evaluator; }</code></pre> <h2>start()</h2> <p>开始分析开始动画的方法</p> <p>从ObjectAnimator开始,会调用到ValueAnimator的start(boolean playBackwards)方法</p> <pre> <code class="language-java">// playBackwards 是否倒序播放,我们此时传入的是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. // mSeekFraction这个标志位,第一次启动动画时是-1,暂时不会进入 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; // 这里从线程中取出AnimatonHandler,一会分析 AnimationHandler animationHandler = AnimationHandler.getInstance(); animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale)); // mStartedDelay指示这个动画是否已经从startDelay中开始执行。 // 这里mStartDelay=0可以顺利启动 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. // 第一次启动,设置当前启动时间为0 setCurrentPlayTime(0); } else { setCurrentFraction(mSeekFraction); } } }</code></pre> <h3>AnimationHandler分析</h3> <p>AnimationHandler 是一个实现了Runnable接口的ValueAnimator内部类</p> <p>从当前线程中取得AnimationHandler对象</p> <pre> <code class="language-java">public static AnimationHandler getInstance() { if (sAnimatorHandler.get() == null) { sAnimatorHandler.set(new AnimationHandler()); } return sAnimatorHandler.get(); }</code></pre> <p>然后注册了两个回调,看下具体方法</p> <pre> <code class="language-java">/** * Register to get a callback on the next frame after the delay. */ public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) { if (mAnimationCallbacks.size() == 0) { getProvider().postFrameCallback(mFrameCallback); } if (!mAnimationCallbacks.contains(callback)) { mAnimationCallbacks.add(callback); } if (delay > 0) { mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay)); } }</code></pre> <pre> <code class="language-java">interface AnimationFrameCallback { /** * Run animation based on the frame time. * @param frameTime The frame start time, in the {@link SystemClock#uptimeMillis()} time * base. */ // 每一帧动画开始时回调 void doAnimationFrame(long frameTime); /** * This notifies the callback of frame commit time. Frame commit time is the time after * traversals happen, as opposed to the normal animation frame time that is before * traversals. This is used to compensate expensive traversals that happen as the * animation starts. When traversals take a long time to complete, the rendering of the * initial frame will be delayed (by a long time). But since the startTime of the * animation is set before the traversal, by the time of next frame, a lot of time would * have passed since startTime was set, the animation will consequently skip a few frames * to respect the new frameTime. By having the commit time, we can adjust the start time to * when the first frame was drawn (after any expensive traversals) so that no frames * will be skipped. * * @param frameTime The frame time after traversals happen, if any, in the * {@link SystemClock#uptimeMillis()} time base. */ // 每一帧开始遍历时回调 void commitAnimationFrame(long frameTime); }</code></pre> <p>从ValueAnimator中看下具体实现</p> <pre> <code class="language-java">// 这里对每一帧进行处理,如果时从暂停恢复,将调整开始时间 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); } // The frame time might be before the start time during the first frame of // an animation. The "current time" must always be on or after the start // time to avoid animating frames at negative time intervals. In practice, this // is very rare and only happens when seeking backwards. final long currentTime = Math.max(frameTime, mStartTime); boolean finished = animateBasedOnTime(currentTime); if (finished) { endAnimation(); } }</code></pre> <p>这个回调也是遍历时调整启动时间的</p> <pre> <code class="language-java">public void commitAnimationFrame(long frameTime) { if (!mStartTimeCommitted) { mStartTimeCommitted = true; long adjustment = frameTime - mLastFrameTime; if (adjustment > 0) { mStartTime += adjustment; if (DEBUG) { Log.d(TAG, "Adjusted start time by " + adjustment + " ms: " + toString()); } } } }</code></pre> <h3>startAnimation()</h3> <p>这段代码时start()中真正启动动画的代码, <strong>必须在UI线程</strong> ,仔细研究下</p> <pre> <code class="language-java">private void startAnimation() { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(), System.identityHashCode(this)); } mAnimationEndRequested = false; // 初始化动画 initAnimation(); mRunning = true; if (mSeekFraction >= 0) { mOverallFraction = mSeekFraction; } else { mOverallFraction = 0f; } if (mListeners != null) { // 通知所有回调 notifyStartListeners(); } }</code></pre> <p>先会调用ObjectAnimator的initAnimation,只要是初始化反射的方法,对Target的属性值进行修改</p> <pre> <code class="language-java">@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(); } }</code></pre> <h2>PropertyValuesHolder的setupSetterAndGetter()</h2> <p>初始化反射修改器,这里代码有点多</p> <pre> <code class="language-java">void setupSetterAndGetter(Object target) { mKeyframes.invalidateCache(); if (mProperty != null) { // check to make sure that mProperty is on the class of target // try-catch判断是否这个属性在这个类里 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; } } // 如果还没有找到属性的话,判断get和set方法是否存在 // 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) { // 初始化setter 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) { // 初始化getter 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()); } } } } }</code></pre> <p>继续寻找set和get方法</p> <pre> <code class="language-java">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); } // 放入map中 propertyMap.put(mPropertyName, setterOrGetter); } } //返回给mSetter或mGetter return setterOrGetter; }</code></pre> <h3>然后调用ValueAnimator的initAnimation</h3> <p>这里会初始化每一个PropertyValuesHolder</p> <pre> <code class="language-java">void initAnimation() { if (!mInitialized) { int numValues = mValues.length; for (int i = 0; i < numValues; ++i) { // 这里会初始化每一个PropertyValuesHolder mValues[i].init(); } mInitialized = true; } }</code></pre> <p>PropertyValuesHolder中的init()</p> <pre> <code class="language-java">void init() { if (mEvaluator == null) { // We already handle int and float automatically, but not their Object // equivalents mEvaluator = (mValueType == Integer.class) ? sIntEvaluator : (mValueType == Float.class) ? sFloatEvaluator : null; } if (mEvaluator != null) { // KeyframeSet knows how to evaluate the common types - only give it a custom // evaluator if one has been set on this class // 给每一个KeyFrame设置估值器,前面讲过 mKeyframes.setEvaluator(mEvaluator); } }</code></pre> <h3>start()中的setCurrentPlayTime()</h3> <p>start()中调用的启动动画方法</p> <pre> <code class="language-java">public void setCurrentPlayTime(long playTime) { // 计算fraction float fraction = mDuration > 0 ? (float) playTime / mDuration : 1; setCurrentFraction(fraction); }</code></pre> <pre> <code class="language-java">// 将计算出来的Fraction设置给动画 public void setCurrentFraction(float fraction) { initAnimation(); fraction = clampFraction(fraction); long seekTime = (long) (getScaledDuration() * fraction); // 当前执行动画的时间 long currentTime = AnimationUtils.currentAnimationTimeMillis(); mStartTime = currentTime - seekTime; mStartTimeCommitted = true; // do not allow start time to be compensated for jank if (!isPulsingInternal()) { // If the animation loop hasn't started, the startTime will be adjusted in the first // frame based on seek fraction. mSeekFraction = fraction; } mOverallFraction = fraction; final float currentIterationFraction = getCurrentIterationFraction(fraction); // 拿到Fraction以后,开始变化数值 animateValue(currentIterationFraction); }</code></pre> <h3>对动画数值进行运算</h3> <p>先会调用ObjectAnimator中的animateValue</p> <pre> <code class="language-java">@Override void animateValue(float fraction) { final Object target = getTarget(); if (mTarget != null && target == null) { // We lost the target reference, cancel and clean up. cancel(); return; } // 这里调用ValueAnimator中的animateValue计算数值 // ValueAnimator与属性值无关的,一会再看 super.animateValue(fraction); int numValues = mValues.length; //反射修改每个方法值 //这里修改完这一轮动画就结束了 for (int i = 0; i < numValues; ++i) { mValues[i].setAnimatedValue(target); } }</code></pre> <p>ValueAnimator中的animateValue进行插值运算</p> <pre> <code class="language-java">void animateValue(float fraction) { // 插值运算在这里 fraction = mInterpolator.getInterpolation(fraction); // 获取当前的Fraction mCurrentFraction = fraction; int numValues = mValues.length; for (int i = 0; i < numValues; ++i) { // 对每个PropertyValuesHolder计算数值 mValues[i].calculateValue(fraction); } // 回调update监听器 if (mUpdateListeners != null) { int numListeners = mUpdateListeners.size(); for (int i = 0; i < numListeners; ++i) { mUpdateListeners.get(i).onAnimationUpdate(this); } } }</code></pre> <h3>PropertyValuesHolder中的calculateValue()</h3> <p>通过获取的fraction,对每个属性值进行变化,这个过程通过反射进行</p> <pre> <code class="language-java">void calculateValue(float fraction) { // 从KeyFrame中获取计算完成的属性值,我们来看下这个方法 Object value = mKeyframes.getValue(fraction); // 这里取到属性值 mAnimatedValue = mConverter == null ? value : mConverter.convert(value); }</code></pre> <p>KeyFrameSet中的getValue()</p> <pre> <code class="language-java">//KeyFrameSet中 public Object getValue(float fraction) { // Special-case optimization for the common case of only two keyframes // 只有两个关键帧的情况 if (mNumKeyframes == 2) { if (mInterpolator != null) { fraction = mInterpolator.getInterpolation(fraction); } // 通过估值器进行取值 return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(), mLastKeyframe.getValue()); } // 此处处理多个关键帧的情况,取出俩个关键帧之前的Fraction // 进行计算 if (fraction <= 0f) { final Keyframe nextKeyframe = mKeyframes.get(1); final TimeInterpolator interpolator = nextKeyframe.getInterpolator(); if (interpolator != null) { fraction = interpolator.getInterpolation(fraction); } final float prevFraction = mFirstKeyframe.getFraction(); float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction); return mEvaluator.evaluate(intervalFraction, mFirstKeyframe.getValue(), nextKeyframe.getValue()); } else if (fraction >= 1f) { final Keyframe prevKeyframe = mKeyframes.get(mNumKeyframes - 2); final TimeInterpolator interpolator = mLastKeyframe.getInterpolator(); if (interpolator != null) { fraction = interpolator.getInterpolation(fraction); } final float prevFraction = prevKeyframe.getFraction(); float intervalFraction = (fraction - prevFraction) / (mLastKeyframe.getFraction() - prevFraction); return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), mLastKeyframe.getValue()); } Keyframe prevKeyframe = mFirstKeyframe; // 对两个关键帧之前的fraction使用估值器进行计算 for (int i = 1; i < mNumKeyframes; ++i) { Keyframe nextKeyframe = mKeyframes.get(i); if (fraction < nextKeyframe.getFraction()) { final TimeInterpolator interpolator = nextKeyframe.getInterpolator(); final float prevFraction = prevKeyframe.getFraction(); float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction); // Apply interpolator on the proportional duration. if (interpolator != null) { intervalFraction = interpolator.getInterpolation(intervalFraction); } return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), nextKeyframe.getValue()); } prevKeyframe = nextKeyframe; } // shouldn't reach here return mLastKeyframe.getValue(); }</code></pre> <h2>PropertyValueHolder中的setAnimatedValue()反射修改属性值</h2> <p>如果是ofFloat创建的FloatPropertyValueHolder,那么该方法为</p> <pre> <code class="language-java">@Override void setAnimatedValue(Object target) { if (mFloatProperty != null) { mFloatProperty.setValue(target, mFloatAnimatedValue); return; } if (mProperty != null) { mProperty.set(target, mFloatAnimatedValue); return; } // 针对jni属性 if (mJniSetter != 0) { nCallFloatMethod(target, mJniSetter, mFloatAnimatedValue); return; } // 反射修改数值 if (mSetter != null) { try { mTmpValueArray[0] = mFloatAnimatedValue; mSetter.invoke(target, mTmpValueArray); } catch (InvocationTargetException e) { Log.e("PropertyValuesHolder", e.toString()); } catch (IllegalAccessException e) { Log.e("PropertyValuesHolder", e.toString()); } } }</code></pre> <p>动画流程就是这样</p> <p> </p> <p>来自:http://blog.csdn.net/y874961524/article/details/53984282</p> <p> </p>
本文由用户 fange4828 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!