Android O Framework架构分析:属性动画ObjectAnimator机制分析
2018-02-13 15:36
477 查看
一. 概述
紧接上一篇: Android O Framework架构分析:属性动画ValueAnimator机制分析,本文将介绍另一个常用的属性动画类型:ObjectAnimator.
ObjectAnimator是ValueAnimator的子类,其整体动画驱动逻辑,属性值的计算方式和ValueAnimator完全相同。不同的地方在于ObjectAnimator可以将属性的每一帧的取值自动设置到传递的对象中。本文将介绍ObjectAnimator的实现架构。
例如:private void testObjectAnimator() {
final ObjectAnimatorTest test = new ObjectAnimatorTest();
ObjectAnimator animator = ObjectAnimator.ofInt(test, "value", 0, 100);
animator.setDuration(100);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Log.d(TAG, "test value: " + test.mValue);
}
});
animator.start();
}
class ObjectAnimatorTest {
int mValue;
private void setValue(int value) {
mValue = value;
}
} 其基本使用方式如上所示,ObjectAnimator同样提供了ofXXX方法进行初始化,不同的是参数发生了变化。上面例子中的三个参数分别代表:
Object:属性值所作用到的对象(上例中的test)。
String:属性名,在第一个参数的类中必须提供对应的setXXX方法(XXX为传递到这里的属性名),用来在每一帧更新该对象的这个成员变量的值。
int...:关键帧,作用同ValueAnimator
上面的例子输出结果为:
02-12 17:31:49.331 12805 12805 D property_animator_test: test value: 0
02-12 17:31:49.350 12805 12805 D property_animator_test: test value: 0
02-12 17:31:49.364 12805 12805 D property_animator_test: test value: 6
02-12 17:31:49.381 12805 12805 D property_animator_test: test value: 24
02-12 17:31:49.397 12805 12805 D property_animator_test: test value: 50
02-12 17:31:49.414 12805 12805 D property_animator_test: test value: 75
02-12 17:31:49.431 12805 12805 D property_animator_test: test value: 93
可以看到在每一帧的回调中,传入的对象对应的属性值都被自动地更新了。下面将基于Android O的版本详细介绍下ObjectAnimator在系统中的实现方式。
二. 基本流程分析
以上例中的代码实现为例,其基本流程可以分为以下几个步骤:
1.动画初始化过程
其初始化过程和ValueAnimator大体相同,唯一不同的是在创建ObjectAnimator需要将属性作用对象与属性名保存起来(step3 ~ step4)。上面省略了部分与ValueAnimator初始化相同的逻辑。
2.动画启动过程
由于前面例子中没有设置对应的Property,而是通过传入Object和属性名进行初始化的,所以其时序图如下(需要通过jni层反射获取setter/getter):
其主要流程为:
step2:检查之前是否有作用与相同Object相同属性的动画,如果有,则取消之前的。
step5:尝试获取传入Object的setXXX方法和getXXX方法(上图仅画出了setter获取流程,getter类似)。
step6:尝试获取setter方法。
step7 ~ step8:根据传入的属性名拼接对应的setter方法名,其逻辑为将属性名的第一个字母改成大写,然后和前缀"set"拼接起来。如前例所示例,传入的属性值为"value",则拼接出来的方法名为"setValue".
step9 ~ step11:这里通过JNI获取对应方法名的jMethodID,默认通过JNI层反射调用对应的setter/getter方法。
step12:调用基类的initAnimation方法,后续流程与ValueAnimator一致。
这里省略了一段逻辑,如果通过jni层反射无法找到,则会通过Java层的反射找到对应的Method对象。后续也将通过Java层反射调用对应的setter/getter方法。
3.动画驱动流程
同样由于前面例子中没有设置对应的Property,而是通过传入Object和属性名进行初始化的,所以其时序图如下(需要通过JNI层反射调用setter):
ObjectAnimator的动画驱动逻辑与ValueAnimator一致,都是通过向Choreographer中注册动画回调来驱动的。
其主要流程为:
step1:Choreographer中回调属性动画注册的动画回调。
step2 ~ step4:先执行基类(ValueAnimator)的流程,为了先计算出动画进度以及当前帧的属性取值。
step5 ~ step7:这里已经计算出了属性取值,需要通过setter方法设置到传递进来的Object中。上图中依然是通过JNI反射去调用对应的setter方法的。如果JNI反射找不到该方法,则通过Java层反射去访问。
动画驱动过程中仍然是基于ValueAnimator的逻辑,只不过在计算出属性取值后,需要将其主动通过传入的Object的setXXX方法设置进去。
三. 关键流程源码分析
与ValueAnimator相比,ObjectAnimator主要的的功能在于,在动画驱动的每一帧里自动地为指定对象的属性赋值。首先需要找到对应的方法(PropertyValuesHolder.java):void setupSetterAndGetter(Object target) {
// 如果设置了Property则后续通过Property调用setter
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.
// 没有设置Property,则需要先找到对应Object的setter/getter方法
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
// 如果客户端没有提供getter方法,则直接返回,不对关键帧进行初始化
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());
}
}
}
}
} 上面可以看到,在尝试获取setter/getter方法时的逻辑为:
(1) 客户端是否传入了Property对象,如果传入,则setter/getter都被客户端封装在了Property的set/get方法中,后面直接调用即可。
(2) 没有传入Property对象,则需要根据属性名查询。
本文一开始的例子是传入的int型属性,所以在查询setter方法时将会调用PropertyValuesHolder的子类:
IntPropertyValuesHolder#setupSetter方法:
JNIEnv* env, jclass pvhClass, jclass targetClass, jstring methodName)
{
const char *nativeString = env->GetStringUTFChars(methodName, 0);
// 获取对应的方法ID
jmethodID mid = env->GetMethodID(targetClass, nativeString, "(I)V");
env->ReleaseStringUTFChars(methodName, nativeString);
// 将其转化为Java层的long类返回给Java层
return reinterpret_cast<jlong>(mid);
} 基类的setupSetter方法是通过Java层反射进行的(PropertyValuesHolder.java):void setupSetter(Class targetClass) {
Class<?> propertyType = mConverter == null ? mValueType : mConverter.getTargetType();
mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", propertyType);
}
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);
}
}
// 如果不在缓存里面,则通过Java层反射,获取对应的Method对象记录下来
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;
} 当动画驱动时,首先根据ValueAnimator的驱动逻辑,会计算出这一帧的属性取值。在ObjectAnimator中会把这一帧的属性取值通过前面获取的setter方法设置到传入的Object中。前面例子中设置的是int类型的属性,所以这里会调用到
IntPropertyValuesHolder#setAnimatedValue:@Override
void setAnimatedValue(Object target) {
// 客户端是否传入了IntProperty对象,如果传入了,则setter被包装在其setValue方法中
// 直接调用后返回
if (mIntProperty != null) {
mIntProperty.setValue(target, mIntAnimatedValue);
return;
}
// 客户端是否传入了Property对象,如果传入了,则setter被包装在其set方法中
// 直接调用后返回
if (mProperty != null) {
mProperty.set(target, mIntAnimatedValue);
return;
}
// 前面是否通过了JNI层反射找到了setter方法,如果找到了,则再次通过JNI层方法调用对应的setter
if (mJniSetter != 0) {
nCallIntMethod(target, mJniSetter, mIntAnimatedValue);
return;
}
// 如果前面都不满足,则使用Java层反射调用
if (mSetter != null) {
try {
mTmpValueArray[0] = mIntAnimatedValue;
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
} 本例子中没有传入Property,则应该通过JNI层反射进行调用(android_animation_PropertyValuesHolder.cpp):static void android_animation_PropertyValuesHolder_callIntMethod(
JNIEnv* env, jclass pvhObject, jobject target, jlong methodID, jint arg)
{
env->CallVoidMethod(target, reinterpret_cast<jmethodID>(methodID), arg);
} 其余的动画机制和ValueAnimator中一致。
紧接上一篇: Android O Framework架构分析:属性动画ValueAnimator机制分析,本文将介绍另一个常用的属性动画类型:ObjectAnimator.
ObjectAnimator是ValueAnimator的子类,其整体动画驱动逻辑,属性值的计算方式和ValueAnimator完全相同。不同的地方在于ObjectAnimator可以将属性的每一帧的取值自动设置到传递的对象中。本文将介绍ObjectAnimator的实现架构。
例如:private void testObjectAnimator() {
final ObjectAnimatorTest test = new ObjectAnimatorTest();
ObjectAnimator animator = ObjectAnimator.ofInt(test, "value", 0, 100);
animator.setDuration(100);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Log.d(TAG, "test value: " + test.mValue);
}
});
animator.start();
}
class ObjectAnimatorTest {
int mValue;
private void setValue(int value) {
mValue = value;
}
} 其基本使用方式如上所示,ObjectAnimator同样提供了ofXXX方法进行初始化,不同的是参数发生了变化。上面例子中的三个参数分别代表:
Object:属性值所作用到的对象(上例中的test)。
String:属性名,在第一个参数的类中必须提供对应的setXXX方法(XXX为传递到这里的属性名),用来在每一帧更新该对象的这个成员变量的值。
int...:关键帧,作用同ValueAnimator
上面的例子输出结果为:
02-12 17:31:49.331 12805 12805 D property_animator_test: test value: 0
02-12 17:31:49.350 12805 12805 D property_animator_test: test value: 0
02-12 17:31:49.364 12805 12805 D property_animator_test: test value: 6
02-12 17:31:49.381 12805 12805 D property_animator_test: test value: 24
02-12 17:31:49.397 12805 12805 D property_animator_test: test value: 50
02-12 17:31:49.414 12805 12805 D property_animator_test: test value: 75
02-12 17:31:49.431 12805 12805 D property_animator_test: test value: 93
可以看到在每一帧的回调中,传入的对象对应的属性值都被自动地更新了。下面将基于Android O的版本详细介绍下ObjectAnimator在系统中的实现方式。
二. 基本流程分析
以上例中的代码实现为例,其基本流程可以分为以下几个步骤:
1.动画初始化过程
其初始化过程和ValueAnimator大体相同,唯一不同的是在创建ObjectAnimator需要将属性作用对象与属性名保存起来(step3 ~ step4)。上面省略了部分与ValueAnimator初始化相同的逻辑。
2.动画启动过程
由于前面例子中没有设置对应的Property,而是通过传入Object和属性名进行初始化的,所以其时序图如下(需要通过jni层反射获取setter/getter):
其主要流程为:
step2:检查之前是否有作用与相同Object相同属性的动画,如果有,则取消之前的。
step5:尝试获取传入Object的setXXX方法和getXXX方法(上图仅画出了setter获取流程,getter类似)。
step6:尝试获取setter方法。
step7 ~ step8:根据传入的属性名拼接对应的setter方法名,其逻辑为将属性名的第一个字母改成大写,然后和前缀"set"拼接起来。如前例所示例,传入的属性值为"value",则拼接出来的方法名为"setValue".
step9 ~ step11:这里通过JNI获取对应方法名的jMethodID,默认通过JNI层反射调用对应的setter/getter方法。
step12:调用基类的initAnimation方法,后续流程与ValueAnimator一致。
这里省略了一段逻辑,如果通过jni层反射无法找到,则会通过Java层的反射找到对应的Method对象。后续也将通过Java层反射调用对应的setter/getter方法。
3.动画驱动流程
同样由于前面例子中没有设置对应的Property,而是通过传入Object和属性名进行初始化的,所以其时序图如下(需要通过JNI层反射调用setter):
ObjectAnimator的动画驱动逻辑与ValueAnimator一致,都是通过向Choreographer中注册动画回调来驱动的。
其主要流程为:
step1:Choreographer中回调属性动画注册的动画回调。
step2 ~ step4:先执行基类(ValueAnimator)的流程,为了先计算出动画进度以及当前帧的属性取值。
step5 ~ step7:这里已经计算出了属性取值,需要通过setter方法设置到传递进来的Object中。上图中依然是通过JNI反射去调用对应的setter方法的。如果JNI反射找不到该方法,则通过Java层反射去访问。
动画驱动过程中仍然是基于ValueAnimator的逻辑,只不过在计算出属性取值后,需要将其主动通过传入的Object的setXXX方法设置进去。
三. 关键流程源码分析
与ValueAnimator相比,ObjectAnimator主要的的功能在于,在动画驱动的每一帧里自动地为指定对象的属性赋值。首先需要找到对应的方法(PropertyValuesHolder.java):void setupSetterAndGetter(Object target) {
// 如果设置了Property则后续通过Property调用setter
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.
// 没有设置Property,则需要先找到对应Object的setter/getter方法
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
// 如果客户端没有提供getter方法,则直接返回,不对关键帧进行初始化
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());
}
}
}
}
} 上面可以看到,在尝试获取setter/getter方法时的逻辑为:
(1) 客户端是否传入了Property对象,如果传入,则setter/getter都被客户端封装在了Property的set/get方法中,后面直接调用即可。
(2) 没有传入Property对象,则需要根据属性名查询。
本文一开始的例子是传入的int型属性,所以在查询setter方法时将会调用PropertyValuesHolder的子类:
IntPropertyValuesHolder#setupSetter方法:
@Override void setupSetter(Class targetClass) { // 如果客户端自己传递进来了Property,则需要自己实现Property中的set/get方法 // 通过Property中的set/get包装对应Object的setter/getter方法 // 此时在此处不需要获取setter了,直接返回 if (mProperty != null) { return; } // Check new static hashmap<propName, int> for setter method synchronized(sJNISetterPropertyMap) { // sJNISetterPropertyMap是一个property name -> setter的缓存,防止重复查找 HashMap<String, Long> propertyMap = sJNISetterPropertyMap.get(targetClass); boolean wasInMap = false; // 查看缓存里是否已经有setter了,如果有,则不需要重新查找了 if (propertyMap != null) { wasInMap = propertyMap.containsKey(mPropertyName); if (wasInMap) { Long jniSetter = propertyMap.get(mPropertyName); if (jniSetter != null) { mJniSetter = jniSetter; } } } // 缓存中没有,则需要重新查找setter if (!wasInMap) { // 拼接对应的setter方法名,逻辑为将属性名第一个字母变成大写,然后与前缀set进行拼接 String methodName = getMethodName("set", mPropertyName); try { // 通过JNI层的反射查找对应的方法 mJniSetter = nGetIntMethod(targetClass, methodName); } catch (NoSuchMethodError e) { // Couldn't find it via JNI - try reflection next. Probably means the method // doesn't exist, or the type is wrong. An error will be logged later if // reflection fails as well. } // 将查询结果记录在缓存中 if (propertyMap == null) { propertyMap = new HashMap<String, Long>(); sJNISetterPropertyMap.put(targetClass, propertyMap); } propertyMap.put(mPropertyName, mJniSetter); } } // 如果JNI层反射找不到,则使用Java层反射查找 if (mJniSetter == 0) { // Couldn't find method through fast JNI approach - just use reflection super.setupSetter(targetClass); } }上面通过nGetIntMethod方法调用到JNI层(android_animation_PropertyValuesHolder.cpp):static jlong android_animation_PropertyValuesHolder_getIntMethod(
JNIEnv* env, jclass pvhClass, jclass targetClass, jstring methodName)
{
const char *nativeString = env->GetStringUTFChars(methodName, 0);
// 获取对应的方法ID
jmethodID mid = env->GetMethodID(targetClass, nativeString, "(I)V");
env->ReleaseStringUTFChars(methodName, nativeString);
// 将其转化为Java层的long类返回给Java层
return reinterpret_cast<jlong>(mid);
} 基类的setupSetter方法是通过Java层反射进行的(PropertyValuesHolder.java):void setupSetter(Class targetClass) {
Class<?> propertyType = mConverter == null ? mValueType : mConverter.getTargetType();
mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", propertyType);
}
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);
}
}
// 如果不在缓存里面,则通过Java层反射,获取对应的Method对象记录下来
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;
} 当动画驱动时,首先根据ValueAnimator的驱动逻辑,会计算出这一帧的属性取值。在ObjectAnimator中会把这一帧的属性取值通过前面获取的setter方法设置到传入的Object中。前面例子中设置的是int类型的属性,所以这里会调用到
IntPropertyValuesHolder#setAnimatedValue:@Override
void setAnimatedValue(Object target) {
// 客户端是否传入了IntProperty对象,如果传入了,则setter被包装在其setValue方法中
// 直接调用后返回
if (mIntProperty != null) {
mIntProperty.setValue(target, mIntAnimatedValue);
return;
}
// 客户端是否传入了Property对象,如果传入了,则setter被包装在其set方法中
// 直接调用后返回
if (mProperty != null) {
mProperty.set(target, mIntAnimatedValue);
return;
}
// 前面是否通过了JNI层反射找到了setter方法,如果找到了,则再次通过JNI层方法调用对应的setter
if (mJniSetter != 0) {
nCallIntMethod(target, mJniSetter, mIntAnimatedValue);
return;
}
// 如果前面都不满足,则使用Java层反射调用
if (mSetter != null) {
try {
mTmpValueArray[0] = mIntAnimatedValue;
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
} 本例子中没有传入Property,则应该通过JNI层反射进行调用(android_animation_PropertyValuesHolder.cpp):static void android_animation_PropertyValuesHolder_callIntMethod(
JNIEnv* env, jclass pvhObject, jobject target, jlong methodID, jint arg)
{
env->CallVoidMethod(target, reinterpret_cast<jmethodID>(methodID), arg);
} 其余的动画机制和ValueAnimator中一致。
相关文章推荐
- Android O Framework架构分析:属性动画ValueAnimator机制分析
- Android属性动画ObjectAnimator源码简单分析
- android属性动画效果的实现之ObjectAnimator
- Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
- Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
- Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
- Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
- Android——属性动画(ObjectAnimator)
- Android属性动画,ValueAnimator和ObjectAnimator的高级用法
- android属性动画 —— ValueAnimator和ObjectAnimator的例子
- Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
- Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
- Android-Animator属性动画( ObjectAnimator , AnimatorSet , ValueAnimator )
- Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
- android 属性动画之 ObjectAnimator
- Android属性动画的学习_ObjectAnimator
- [置顶] Android属性动画系列(一)——ObjectAnimator
- [置顶] [动画]属性动画ObjectAnimator及ValueAnimator运用分析
- Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
- Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法