Android深入分析屬性動(dòng)畫源碼
1.先看一段動(dòng)畫的代碼實(shí)現(xiàn)
ObjectAnimator alpha = ObjectAnimator.ofFloat(view, "alpha", 1, 0,1); alpha.setDuration(500); alpha.start();
代碼很簡(jiǎn)單,上面三行代碼就可以開啟一個(gè)透明度變化的動(dòng)畫。 那么android系統(tǒng)到底是如何實(shí)現(xiàn)的呢?進(jìn)入源碼分析。
1)看第一行代碼:
ObjectAnimator alpha = ObjectAnimator.ofFloat(view, "alpha", 1, 0,1);
創(chuàng)建了一個(gè)ObjectAnimator對(duì)象,并把values數(shù)組設(shè)置給了anim對(duì)象。
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) { ObjectAnimator anim = new ObjectAnimator(target, propertyName); anim.setFloatValues(values); return anim; }
ObjectAnimator 構(gòu)造函數(shù)中。將傳過(guò)來(lái)的View對(duì)象和propertyName賦值給成員變量。
private ObjectAnimator(Object target, String propertyName) { //將傳過(guò)來(lái)的View對(duì)象賦值給成員變量mTarget setTarget(target); //將propertyName賦值給成員變量mPropertyName setPropertyName(propertyName); }
注意這個(gè)mTarget為什么要用一個(gè)軟引用?
那是為了防止Activity發(fā)生內(nèi)存泄漏。因?yàn)闀?huì)有Activity已經(jīng)退出,但是動(dòng)畫可能還未執(zhí)行完,這個(gè)時(shí)候View得不到釋放的話,會(huì)引發(fā)Activity內(nèi)存泄漏。
private WeakReference<Object> mTarget; public void setTarget(@Nullable Object target) { final Object oldTarget = getTarget(); if (oldTarget != target) { if (isStarted()) { cancel(); } //將傳進(jìn)來(lái)的View對(duì)象賦值給mTarget mTarget = target == null ? null : new WeakReference<Object>(target); mInitialized = false; } }
再看第二行代碼做了啥?anim.setFloatValues(values);
首次進(jìn)來(lái)mValues==null,mProperty==null,所以會(huì)執(zhí)行這行代碼。 setValues(PropertyValuesHolder.ofFloat(mPropertyName, values))。
public void setFloatValues(float... values) { if (mValues == null || mValues.length == 0) { if (mProperty != null) { setValues(PropertyValuesHolder.ofFloat(mProperty, values)); } else { setValues(PropertyValuesHolder.ofFloat(mPropertyName, values)); } } else { super.setFloatValues(values); } }
setValue將得到的 PropertyValuesHolder數(shù)組賦值給成員變量PropertyValuesHolder[] mValues;
再看PropertyValuesHolder.ofFloat(mPropertyName, values));
先調(diào)用super構(gòu)造函數(shù),將propertyName賦值給父類的mPropertyName,
public FloatPropertyValuesHolder(String propertyName, float... values) { super(propertyName); setFloatValues(values); }
然后再調(diào)用setFloatValues(values);
public void setFloatValues(float... values) { super.setFloatValues(values); //將mKeyframes強(qiáng)轉(zhuǎn)為mFloatKeyframes mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes; } //調(diào)用父類方法創(chuàng)建了KeyframeSet對(duì)象,賦值給了mKeyframes public void setFloatValues(float... values) { mValueType = float.class; mKeyframes = KeyframeSet.ofFloat(values); }
KeyframeSet.ofFloat(values);這行代碼創(chuàng)建了一個(gè)關(guān)鍵幀的集合。
public static KeyframeSet ofFloat(float... values) { boolean badValue = false; int numKeyframes = values.length; //創(chuàng)建一個(gè)value長(zhǎng)度的 FloatKeyFrame的數(shù)組 FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)]; //numKeyframes==1的話,其實(shí)是沒有View是沒有動(dòng)畫的。如果傳過(guò)來(lái)的values的長(zhǎng)度是1的話,會(huì)報(bào)錯(cuò)的。 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 { //下面的代碼才是關(guān)鍵的 Keyframe ofFloat(float fraction, float value)是創(chuàng)建關(guān)鍵幀。 //fraction英文單詞意思是部分,在這作為參數(shù)的意思是:從動(dòng)畫啟示位置,到當(dāng)前位置,所占的整個(gè)動(dòng)畫的百分比。 //value就是某個(gè)部分對(duì)應(yīng)的屬性值。 // 比如傳進(jìn)來(lái)的value值是1.0f 2.0f 3.0f 4.0f,5.0f。整個(gè)動(dòng)畫有5個(gè)值。因?yàn)?.0是初始值,要完成整個(gè)動(dòng)畫需要4步。 //從1-2,2-3,3-4,4-5;4個(gè)部分。 //第0個(gè)位置是起始位置,所以他所在的部分就是0。第一個(gè)位置就是四分之一,第二個(gè)就是四分之二.... //第i個(gè)位置,所在整個(gè)動(dòng)畫的部分就是i/(i-1)。而這個(gè)位置對(duì)應(yīng)的動(dòng)畫的屬性值,就是value[i] //所以這個(gè)keyframes[]數(shù)組的目的就是保存,動(dòng)畫的關(guān)鍵位置所占的百分比和關(guān)鍵位置對(duì)應(yīng)的屬性值。 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; } } } return new FloatKeyframeSet(keyframes); }
到這為止,第一行代碼執(zhí)行完畢。
ObjectAnimator.ofFloat(view, "alpha", 1, 0,1)
將view賦值給ObjectAnimator成員變量。
將propertyName賦值給PropertyValuesHolder,會(huì)通過(guò)屬性name來(lái)反射它的set方法,用來(lái)修改屬性值。
創(chuàng)建KeyframeSet,關(guān)鍵幀集合。將value數(shù)組轉(zhuǎn)換成對(duì)應(yīng)的關(guān)鍵幀集合,通過(guò)動(dòng)畫執(zhí)行的時(shí)間,來(lái)計(jì)算當(dāng)前時(shí)間對(duì)應(yīng)的屬性值,然后再調(diào)用view的set屬性方法,從而達(dá)到形成動(dòng)畫的目的。
這塊的代碼會(huì)再后面看到。
2).看動(dòng)畫的第二行代碼alpha.start();
ObjectAnimator的父類是ValueAnimator。start()里面調(diào)用到的方法會(huì)在子類和父類里跳來(lái)跳去,這也增大了閱讀的難度。
首先看ValueAnimator#start(boolean playBackwards)方法
addAnimationCallback:向Choreographer注冊(cè)回調(diào)函數(shù),我們知道Choreographer可以接受Vsync信號(hào),16.66ms一次,也是屏幕刷新一次的時(shí)間。這樣在屏幕刷新的時(shí)候,就可以通過(guò)向Choreographer注冊(cè)回調(diào)函數(shù)進(jìn)行動(dòng)畫的更新。
private void start(boolean playBackwards) { //Animators 必須運(yùn)行在一個(gè)Looper不能為空的線程中,因?yàn)閯?dòng)畫需要涉及到Choreographer。 if (Looper.myLooper() == null) { throw new AndroidRuntimeException("Animators may only be run on Looper threads"); } mStarted = true; mPaused = false; mRunning = false; mAnimationEndRequested = false; mStartTime = -1; //這個(gè)是一個(gè)回調(diào)函數(shù)。這塊是由Choreographer回調(diào)的,稍后分析。 addAnimationCallback(0); if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) { //開始動(dòng)畫。 startAnimation(); } }
先看startAnimation方法(),會(huì)在這個(gè)方法中調(diào)用initAnimation();
在這會(huì)先調(diào)用子類ObjectAnimator,然后在調(diào)用父類的ValueAnimator的initAnimation方法。
先看子類的initAnimation(),這個(gè)方法根據(jù)propertyName來(lái)反射view的set屬性方法。
void initAnimation() { if (!mInitialized) { //先拿到target,也就是view對(duì)象。 final Object target = getTarget(); if (target != null) { // PropertyValuesHolder[] mValues;這個(gè)values就是PropertyValuesHolder的集合。 final int numValues = mValues.length; for (int i = 0; i < numValues; ++i) { //在PropertyValuesHolder中傳進(jìn)了屬性值,下面這行代碼就是根據(jù)屬性值,來(lái)反射view的set方法, //通過(guò)set方法,就可以動(dòng)態(tài)的改變view的屬性值的變化。 mValues[i].setupSetterAndGetter(target); } } //調(diào)用父類的initAnimation()方法 super.initAnimation(); } }
再看父類ValueAnimator的initAnimation方法。調(diào)用了PropertyValuesHolder的init()方法。
在init方法中,向KeyframeSet關(guān)鍵幀集合設(shè)置了一個(gè)估值器,這個(gè)用來(lái)計(jì)算屬性值的,后面會(huì)看到具體的計(jì)算方法。
void initAnimation() { if (!mInitialized) { int numValues = mValues.length; for (int i = 0; i < numValues; ++i) { //調(diào)用PropertyValuesHolder#init方法 mValues[i].init(); } mInitialized = true; } }
void init() { if (mEvaluator == null) { //得到一個(gè)估值器 mEvaluator = (mValueType == Integer.class) ? sIntEvaluator : (mValueType == Float.class) ? sFloatEvaluator : null; } if (mEvaluator != null) { //向KeyframeSet中設(shè)置一個(gè)估值器,這個(gè)估值器用來(lái)計(jì)算動(dòng)畫在某個(gè)時(shí)刻的屬性值。 mKeyframes.setEvaluator(mEvaluator); } }
private static final TypeEvaluator sIntEvaluator = new IntEvaluator(); private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator(); public class FloatEvaluator implements TypeEvaluator<Number> { //This function returns the result of linearly interpolating the start and end values 這個(gè)方法返回一個(gè)在動(dòng)畫開始和結(jié)束之間的一個(gè)線性的結(jié)果。其實(shí)就是個(gè)一元一次方程,來(lái)計(jì)算動(dòng)畫當(dāng)前的位置。 //result = x0 + t * (v1 - v0) public Float evaluate(float fraction, Number startValue, Number endValue) { float startFloat = startValue.floatValue(); return startFloat + fraction * (endValue.floatValue() - startFloat); } }
至此,initAnimation的代碼已經(jīng)執(zhí)行完畢。主要做的工作可以總結(jié)為兩點(diǎn):
1.調(diào)用PropertyValuesHolder的setupSetterAndGetter方法,通過(guò)反射拿到View的setter方法。
2.向KeyframeSet中設(shè)置一個(gè)估值器,用來(lái)計(jì)算動(dòng)畫某一時(shí)刻的屬性值。
3)接下來(lái)看ValueAnimator#addAnimationCallback
這個(gè)方法是向Choreographer設(shè)置了一個(gè)會(huì)回調(diào)函數(shù),每隔16.66ms回調(diào)一次,用來(lái)刷新動(dòng)畫。
還設(shè)置了一個(gè)回調(diào)集合,在Choreographer的回調(diào)函數(shù)中,回調(diào)集合里面的回調(diào)函數(shù),來(lái)實(shí)現(xiàn)屬性動(dòng)畫的刷新
private void addAnimationCallback(long delay) { if (!mSelfPulse) { return; } //getAnimationHandler 就是上面創(chuàng)建的AnimationHandler。 //將this作為 AnimationFrameCallback的回調(diào),會(huì)回調(diào)doAnimationFrame(long frameTime) getAnimationHandler().addAnimationFrameCallback(this, delay); }
//AnimationHandler#addAnimationFrameCallback getProvider()拿到的是MyFrameCallbackProvider。 public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) { if (mAnimationCallbacks.size() == 0) { //向Choreographer加入一個(gè)回調(diào)函數(shù)mFrameCallback getProvider().postFrameCallback(mFrameCallback); } //將添加的回調(diào)函數(shù)加入一個(gè)回調(diào)的集合。 if (!mAnimationCallbacks.contains(callback)) { mAnimationCallbacks.add(callback); } }
先看這個(gè)getProvider().postFrameCallback(mFrameCallback);這個(gè)就是向Choreographer注冊(cè)一個(gè)回調(diào)。
final Choreographer mChoreographer = Choreographer.getInstance(); //這行代碼是向編舞者Choreographer添加了一個(gè)回調(diào)函數(shù)。 public void postFrameCallback(Choreographer.FrameCallback callback) { mChoreographer.postFrameCallback(callback); } Choreographer中 public void postFrameCallback(FrameCallback callback) { postFrameCallbackDelayed(callback, 0); }
下面這行代碼就是向Choreographer添加CallBackType為CALLBACK_ANIMATION,Token為FRAME_CALLBACK_TOKEN的回調(diào)函數(shù)。 callback 就是傳進(jìn)來(lái)的mFrameCallback。
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) { postCallbackDelayedInternal(CALLBACK_ANIMATION, callback, FRAME_CALLBACK_TOKEN, delayMillis); }
省略中間的調(diào)用過(guò)程。。。這塊的代碼在Choreographer源碼分析過(guò)。
MyFrameCallbackProvider#postFrameCallback就是向Choreographer添加一個(gè)回調(diào)函數(shù)。 我們知道,Choreographer在接收到Vsync信號(hào)后調(diào)用這些回調(diào)函數(shù)。
void doFrame(long frameTimeNanos, int frame) { doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos); doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos); }
最終會(huì)調(diào)到這里,根據(jù)上面?zhèn)鬟^(guò)來(lái)的token,轉(zhuǎn)換成不同的回調(diào)函數(shù),調(diào)用不同的方法。 //在將View繪制時(shí),調(diào)用的是else分支的回調(diào) //在動(dòng)畫這里,傳進(jìn)來(lái)的是mFrameCallback,Choreographer.FrameCallback的實(shí)例,會(huì)調(diào)用到doFrame方法 public void run(long frameTimeNanos) { if (token == FRAME_CALLBACK_TOKEN) { ((FrameCallback)action).doFrame(frameTimeNanos); } else { ((Runnable)action).run(); } }
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() { @Override public void doFrame(long frameTimeNanos) { doAnimationFrame(getProvider().getFrameTime()); if (mAnimationCallbacks.size() > 0) { //再次向Choreographer注冊(cè)回調(diào),等到下一次Vsync信號(hào)來(lái)的時(shí)候調(diào)用, //針對(duì)于60Hz的屏幕,刷新時(shí)間間隔是16.66ms,也就是Vsync回調(diào)的時(shí)間間隔 //也就是說(shuō)屬性動(dòng)畫16.66毫秒會(huì)改變一次 getProvider().postFrameCallback(this); } } };
Choreographer中每個(gè)16.6ms會(huì)回調(diào)doFrame方法(),在doAnimationFrame方法中,就會(huì)回調(diào)注冊(cè)的回調(diào)集合。
private void doAnimationFrame(long frameTime) { long currentTime = SystemClock.uptimeMillis(); final int size = mAnimationCallbacks.size(); for (int i = 0; i < size; i++) { final AnimationFrameCallback callback = mAnimationCallbacks.get(i); if (callback == null) { continue; } //遍歷mAnimationCallbacks,調(diào)用callBack回調(diào)函數(shù), //這個(gè)回調(diào)函數(shù)是ValueAnimator的doAnimationFrame if (isCallbackDue(callback, currentTime)) { callback.doAnimationFrame(frameTime); } } }
doAnimationFrame是AnimationFrameCallback的回調(diào)函數(shù),由ValueAnimator實(shí)現(xiàn)。
public final boolean doAnimationFrame(long frameTime) { //frameTime 這個(gè)時(shí)間是從Choreographer傳過(guò)來(lái)的時(shí)間, //記錄為上一次動(dòng)畫刷新的時(shí)間 mLastFrameTime = frameTime; final long currentTime = Math.max(frameTime, mStartTime); boolean finished = animateBasedOnTime(currentTime); return finished; }
public final boolean doAnimationFrame(long frameTime) { //frameTime 這個(gè)時(shí)間是從Choreographer傳過(guò)來(lái)的時(shí)間, //記錄為上一次動(dòng)畫刷新的時(shí)間 mLastFrameTime = frameTime; final long currentTime = Math.max(frameTime, mStartTime); boolean finished = animateBasedOnTime(currentTime); return finished; }
boolean animateBasedOnTime(long currentTime) { boolean done = false; if (mRunning) { //拿到總時(shí)間 final long scaledDuration = getScaledDuration(); //通過(guò)計(jì)算得到動(dòng)畫當(dāng)前執(zhí)行占比多少。(currentTime - mStartTime)動(dòng)畫執(zhí)行的時(shí)間 //除以scaledDuration總時(shí)間,得到就是已經(jīng)執(zhí)行的部分,如果是一個(gè)重復(fù)的動(dòng)畫,這個(gè)值可能會(huì)大于1. final float fraction = scaledDuration > 0 ? (float)(currentTime - mStartTime) / scaledDuration : 1f; //下面通過(guò)計(jì)算對(duì)fraction進(jìn)行修正,減去重復(fù)執(zhí)行的部分,得到真正的在一次動(dòng)畫中要執(zhí)行到哪一部分 mOverallFraction = clampFraction(fraction); float currentIterationFraction = getCurrentIterationFraction( mOverallFraction, mReversing); animateValue(currentIterationFraction); } return done; }
注意animateValue,這個(gè)方法在父類ValueAnimator和子類ObjectAnimator都有實(shí)現(xiàn)。
所以這里先調(diào)用子類ObjectAnimator的方法。
//這個(gè)方法是調(diào)用的子類的方法 void animateValue(float fraction) { final Object target = getTarget(); if (mTarget != null && target == null) { cancel(); return; } //先調(diào)用父類的方法 super.animateValue(fraction); //再回到子類 int numValues = mValues.length; for (int i = 0; i < numValues; ++i) { //給View設(shè)置改變后的屬性值 mValues[i].setAnimatedValue(target); } }
先看super.animateValue方法,這個(gè)方法就是去計(jì)算動(dòng)畫變動(dòng)后的屬性值。
void animateValue(float fraction) { //通過(guò)插值器,來(lái)修改。如果沒有設(shè)置插值器,那么fraction的變化就是勻速的。 //經(jīng)過(guò)插值器的計(jì)算,fraction的變化就會(huì)呈現(xiàn)出加速、減速變化的效果。 fraction = mInterpolator.getInterpolation(fraction); mCurrentFraction = fraction; int numValues = mValues.length; for (int i = 0; i < numValues; ++i) { //PropertyValuesHolder[] mValues,因?yàn)橐粋€(gè)View可以有多個(gè)屬性動(dòng)畫,所以這用一個(gè)數(shù)組來(lái)存儲(chǔ)。 mValues[i].calculateValue(fraction); } }
AccelerateDecelerateInterpolator 插值器 public float getInterpolation(float input) { return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f; } void calculateValue(float fraction) { //mKeyframes 就是前面創(chuàng)建的關(guān)鍵幀集合KeyframeSet Object value = mKeyframes.getValue(fraction); // 將得到的值,賦值給mAnimatedValue mAnimatedValue = mConverter == null ? value : mConverter.convert(value); }
下面這個(gè)方法是真正去計(jì)算改變后的屬性值。通過(guò)估值器mEvaluator去計(jì)算的。
public Object getValue(float fraction) { //第一關(guān)鍵幀記做前一關(guān)鍵幀 Keyframe prevKeyframe = mFirstKeyframe; for (int i = 1; i < mNumKeyframes; ++i) { //得到下一關(guān)鍵幀 Keyframe nextKeyframe = mKeyframes.get(i); if (fraction < nextKeyframe.getFraction()) { final TimeInterpolator interpolator = nextKeyframe.getInterpolator(); //得到前一關(guān)鍵幀,對(duì)應(yīng)的部分 final float prevFraction = prevKeyframe.getFraction(); //fraction - prevFraction 當(dāng)前要執(zhí)行的部分距離前一關(guān)鍵幀是多少。 //nextKeyframe.getFraction() - prevFraction,這一幀有多少 //兩者相除,得到的就是當(dāng)前部分在這一幀的占比 float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction); if (interpolator != null) { //通過(guò)插值器來(lái)修改,這一部分的大小 intervalFraction = interpolator.getInterpolation(intervalFraction); } //通過(guò)估值器,來(lái)計(jì)算屬性值要變化到多少 //這個(gè)估值器就是上面賦值的FloatEvaluator或IntEvaluator return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), nextKeyframe.getValue()); } prevKeyframe = nextKeyframe; } // shouldn't reach here //不應(yīng)該執(zhí)行到這里,在上面的for循環(huán)就應(yīng)該返回當(dāng)前動(dòng)畫,屬性變化的大小。 return mLastKeyframe.getValue(); }
通過(guò)估值器計(jì)算view的屬性值。
public Float evaluate(float fraction, Number startValue, Number endValue) { float startFloat = startValue.floatValue(); //通過(guò)一個(gè)一元一次方程,來(lái)計(jì)算得到當(dāng)前的屬性值。 return startFloat + fraction * (endValue.floatValue() - startFloat); }
至此,動(dòng)畫要變動(dòng)后的屬性值,已經(jīng)計(jì)算出來(lái)了,
通過(guò) mValues[i].setAnimatedValue(target);
用來(lái)修改View的屬性值大小。
void setAnimatedValue(Object target) { //前面已經(jīng)通過(guò)反射拿到了View的setter方法 if (mSetter != null) { try { //拿到屬性值大小, mTmpValueArray[0] = getAnimatedValue(); //通過(guò)反射,修改view屬性值的大小 mSetter.invoke(target, mTmpValueArray); } catch (InvocationTargetException e) { Log.e("PropertyValuesHolder", e.toString()); } catch (IllegalAccessException e) { Log.e("PropertyValuesHolder", e.toString()); } } } Object getAnimatedValue() { return mAnimatedValue; }
至此,android屬性動(dòng)畫的整個(gè)執(zhí)行流程已經(jīng)分析完畢。
可以總結(jié)以下幾點(diǎn):
1.ValueAnimator是父類,ObjectAnimator是子類,這里面封裝了一個(gè)target,也就是view對(duì)象。
2.PropertyValuesHolder,有屬性名,屬性值,通過(guò)屬名來(lái)反射view的setter方法,來(lái)動(dòng)態(tài)修改屬性值。
3.KeyframeSet,是一個(gè)關(guān)鍵幀集合,封裝了定義動(dòng)畫是value數(shù)組的值,每一個(gè)值都被記錄為一個(gè)關(guān)鍵幀F(xiàn)loatKeyframe。
4.通過(guò)插值器,可以改變屬性變化的快慢,通過(guò)估值器計(jì)算屬性值的大小。
5.給Choreographer注冊(cè)了一個(gè)回調(diào),每隔16.66ms回調(diào)一次,每一次回調(diào)都會(huì)去改變view屬性值的大小。改變是通過(guò)fraction計(jì)算的,進(jìn)而通過(guò)計(jì)算得到改變后的屬性值大小。
這樣動(dòng)態(tài)的改變view屬性值的大小,就連貫的形成一幅動(dòng)畫。
到此這篇關(guān)于Android深入分析屬性動(dòng)畫源碼的文章就介紹到這了,更多相關(guān)Android屬性動(dòng)畫內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Android動(dòng)畫系列之屬性動(dòng)畫的基本使用教程
- Android屬性動(dòng)畫實(shí)現(xiàn)圖片從左到右逐漸消失
- Android動(dòng)畫教程之屬性動(dòng)畫詳解
- Android利用屬性動(dòng)畫實(shí)現(xiàn)優(yōu)酷菜單
- Android屬性動(dòng)畫特點(diǎn)詳解
- Android使用屬性動(dòng)畫如何自定義倒計(jì)時(shí)控件詳解
- Android屬性動(dòng)畫之ValueAnimator代碼詳解
- Android 屬性動(dòng)畫ValueAnimator與插值器詳解
- Android源碼解析之屬性動(dòng)畫詳解
相關(guān)文章
簡(jiǎn)單實(shí)現(xiàn)android輪播圖
這篇文章主要為大家詳細(xì)介紹了android輪播圖的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01Android CountDownTimer實(shí)現(xiàn)倒計(jì)時(shí)器
這篇文章主要為大家詳細(xì)介紹了Android CountDownTimer實(shí)現(xiàn)倒計(jì)時(shí)效果的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02android studio xml文件實(shí)現(xiàn)添加注釋
這篇文章主要介紹了android studio xml文件實(shí)現(xiàn)添加注釋,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03android Jsoup獲取網(wǎng)站內(nèi)容 android獲取新聞標(biāo)題實(shí)例
這篇文章主要為大家詳細(xì)介紹了android Jsoup獲取網(wǎng)站內(nèi)容,android獲取新聞標(biāo)題實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03android中webview控件和javascript交互實(shí)例
這篇文章主要介紹了android中webview控件和javascript交互實(shí)例,例子中包括javascript調(diào)用java的方法,java代碼中調(diào)用javascript的方法,需要的朋友可以參考下2014-07-07初學(xué)Android之網(wǎng)絡(luò)封裝實(shí)例
大家好,本篇文章主要講的是初學(xué)Android之網(wǎng)絡(luò)封裝實(shí)例,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12Android開發(fā)自學(xué)筆記(四):APP布局下
這篇文章主要介紹了Android開發(fā)自學(xué)筆記(四):APP布局下,本文是上一篇的補(bǔ)充,需要的朋友可以參考下2015-04-04