转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/42056859,本文出自:【张鸿洋的博客】

我参加了博客之星评选,如果你喜欢我的博客,求投票~~http://vote.blog.csdn.net/blogstar2014/selection?username=lmj623565791#content

1、概述

Android中想做很炫酷的动画效果,相信在很多时候你都可以选择使用属性动画,关于属性动画如何使用,我们已经很详细的写过两篇博客讲解。如果你还不了解,请参考:

Android 属性动画(Property Animation) 完全解析 (上)

Android 属性动画(Property Animation) 完全解析 (下)

本篇博客将分析属性动画的实现源码,带你深入的了解Android属性动画的内部实现机制。如果你经常用属性动画,但又一直没有去查看其源码实现,没关系,请往下看。

2、分析前的猜想

在源码分析之前,我们需要有一个明确的思路,例如:源码的入口的选择、甚至对其实现进行简单的猜测,源码分析相当于一个验证的过程,带着一个目标去看源码,这样的话,分析和理解起来更为方便。

对于实现属性动画,最常用的类就是ObjectAnimator了,只需要简单的设置目标view,属性,以及目标值等必要属性,调用一下start();我们的动画就完成了。

类似如下代码:

ObjectAnimator  .ofInt(target,propName,values[])  .setInterpolator(LinearInterpolator)  .setEvaluator(IntEvaluator)  .setDuration(500)  .start();

上述代码很好理解吧,设置动画作用的view,作用的属性,动画开始、结束、以及中间的任意个属性值;

然后是设置插值器,当然了插值器这个词比较难理解,我要是说例如:AccelerateInterpolator、LinearInterpolator

然后设置估值算法,这个看名字挺高端,其实内部实现尤其简单: return (int)(startInt + fraction * (endValue - startInt)); 开始值,加上当前的属性改变的百分比*(结束-开始)

当然了,这个百分比是fraction ,其实就是上面的插值器算出来的。比如线性插值器:fraction值就是currentTime - mStartTime) / mDuration,动画的运行时间/总设置时间。

然后是设置动画事件,

最后start()。

好了,现在我想问个问题,根据上面这些参数,如果我要你设计个属性动画框架,你怎么做?

这个嘛,好整,拿到上述参数之后,start()中,开启一个定时器,去执行一个任务;在任务内部,根据Interpolator计算出来的fraction,交给Evaluator,得到属性当前应该设置的值,然后反射设置tagert的指定属性,ok,奏事这么简单。嗯,大体上应该就是这样,当然了,源码的实现肯定复杂很多,但是万变不离其宗,所以接下来的源码阅读,就是去验证我们的这个答案。

3、源码分析

好了,猜想完了,我们就得进入验证阶段了~~

那么,我们源码的入口就是上述代码了,不过貌似上述代码调用了好几个方法,but,我觉得start之前的代码,无法是初始化实例,设置一些成员变量。

首先我们看ofInt,这里为了简单,我们的ofInt中的values参数,默认就一个,类似.ofInt(view, "translationX", 300) ;

1、ofInt

public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {        ObjectAnimator anim = new ObjectAnimator(target, propertyName);        anim.setIntValues(values);        return anim;    }


首先调用ObjectAnimator的构造方法传入了一个target和propName,估计就是创建对象,然后旧路下target和propName,简单看下

 private ObjectAnimator(Object target, String propertyName) {        mTarget = target;        setPropertyName(propertyName);    } public void setPropertyName(String propertyName) {       //...        mPropertyName = propertyName;       mInitialized = false;    }

记录完成target,propName以后,调用setIntValues

@Override    public void setIntValues(int... values) {            setValues(PropertyValuesHolder.ofInt(mPropertyName, values));      }

可以看到,把我们的propName,和values传入到了一个PropertyValuesHolder的ofInt方法中,去构造一个PropertyValuesHolder对象,这个对象是干什么的呢?

从字面上看,是保存view在动画期间的属性和值,记住是动画期间的。继续往下看:

public static PropertyValuesHolder ofInt(String propertyName, int... values) {        return new IntPropertyValuesHolder(propertyName, values);    }  public IntPropertyValuesHolder(String propertyName, int... values) {            mPropertyName = propertyName;            setIntValues(values);        }@Override        public void setIntValues(int... values) {            mValueType = int.class;            mKeyframeSet = KeyframeSet.ofInt(values);            mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet;        }

可以看到在IntPropertyValuesHolder内部存储了我们的propertyName;,然后又调用了setIntValues,存储了我们的mValueType ,此外还存了一个mIntKeyframeSet。

这里又出现一个新名词,叫做mKeyframeSet,这个是由KeyframeSet.ofInt(values);得到的。

那么这个KeyframeSet是什么呢?单纯的理解是,Keyframe的集合,而Keyframe叫做关键帧,为一个动画保存time/value(时间与值)对。

那么我们去看看它是如何通过KeyframeSet.ofInt(values);去构造与保存的:

public static KeyframeSet ofInt(int... values) {        int numKeyframes = values.length;        IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2)];        if (numKeyframes == 1) {            keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f);            keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]);        } else {            //...        }        return new IntKeyframeSet(keyframes);    } public IntKeyframeSet(IntKeyframe... keyframes) {        mNumKeyframes = keyframes.length;        mKeyframes = new ArrayList<Keyframe>();        mKeyframes.addAll(Arrays.asList(keyframes));        mFirstKeyframe = mKeyframes.get(0);        mLastKeyframe = mKeyframes.get(mNumKeyframes - 1);        mInterpolator = mLastKeyframe.getInterpolator();    }

这里代码跳跃比较大,部分代码我来解释:

根据我们的values的长度,构造了keyframes数组,然后分别通过Keyframe的ofInt方法,去构造keyframe对象,其实在内部:

IntKeyframe(float fraction, int value) {            mFraction = fraction;            mValue = value;            mValueType = int.class;            mHasValue = true;        }        IntKeyframe(float fraction) {            mFraction = fraction;            mValueType = int.class;        }

就简单存了一下fraction,和value;当然了,我们这里values只有一个值,所以构造了两个Keyframe。

拿到初始化完成的keyframes数组以后,将其传入了KeyframeSet的构造方法,初始化了KeyframeSet内部的一些成员变量。

public IntKeyframeSet(IntKeyframe... keyframes) {        mNumKeyframes = keyframes.length;        mKeyframes = new ArrayList<Keyframe>();        mKeyframes.addAll(Arrays.asList(keyframes));        mFirstKeyframe = mKeyframes.get(0);        mLastKeyframe = mKeyframes.get(mNumKeyframes - 1);        mInterpolator = mLastKeyframe.getInterpolator();    }

存了有多少关键帧,开始帧,结束帧,以及插值器。

到此,我们的(PropertyValuesHolder.ofInt在彻底返回,可以看到这个过程中,我们成功的为PropertyValuesHolder对象赋值了propName,valueType,keyframeSet .

keyframeset中存了Keyframe集合,keyframe中存储了(fraction , valuetype , value , hasValue)。

最后,叫PropertyValuesHolder 交给我们的 ObjectAnimator的setValues方法。

 public void setValues(PropertyValuesHolder... values) {        int numValues = values.length;        mValues = values;        mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues);        for (int i = 0; i < numValues; ++i) {            PropertyValuesHolder valuesHolder = values[i];            mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);        }        // New property/values/target should cause re-initialization prior to starting        mInitialized = false;    }
首先记录了mValues,注意这里的values是PropertyValuesHolder类型的,然后通过一个mValueMap记录:key为属性的名称,值为PropertyValuesHolder 。

好了,到此我们的ofInt结束了,晕否,其实还好。如果你晕了,我帮你总结下:ofInt就是记录了target,propName,values(是将我们传入的int型values,辗转转化成了PropertyValuesHolder),以及一个mValueMap,这个map的key是propName,value是PropertyValuesHolder,在PropertyValuesHolder内部又存储了proprName, valueType , keyframeSet等等。

好了,接下来会轻松点,按照顺序到setInterpolator了:

2、setInterpolator

 @Override    public void setInterpolator(TimeInterpolator value) {        if (value != null) {            mInterpolator = value;        } else {            mInterpolator = new LinearInterpolator();        }    }

没撒说的,记录下插值器,我们这里也线性插值器,默认也是~~

然后是setEvaluator。

3、setEvaluator

 public void setEvaluator(TypeEvaluator value) {        if (value != null && mValues != null && mValues.length > 0) {            mValues[0].setEvaluator(value);        }    }

记得我们这里的mValue吧,在ofInt里面初始化的,类型是PropertyValuesHolder。然后调用了PropertyValuesHolder.setEvalutor

  public void setEvaluator(TypeEvaluator evaluator) {        mEvaluator = evaluator;        mKeyframeSet.setEvaluator(evaluator);    }

记录了一下估值算法,然后再将其传给KeyframeSet对象:

public void setEvaluator(TypeEvaluator evaluator) {        mEvaluator = evaluator;    }

可以看到,我们把估值算法,交给了PropertyValuesHolder以及KeyframeSet。

接下来,最后一个属性,duration

4、setDuration

 // How long the animation should last in ms    private long mDuration = (long)(300 * sDurationScale);    private long mUnscaledDuration = 300;    private static float sDurationScale = 1.0f;    public ObjectAnimator setDuration(long duration) {        if (duration < 0) {            throw new IllegalArgumentException("Animators cannot have negative duration: " +                    duration);        }        mUnscaledDuration = duration;        mDuration = (long)(duration * sDurationScale);        return this;    }

就是简单在mDuration中记录了一下动画的持续时间,这个sDurationScale默认为1,貌似是用于调整,观察动画的,比如你可以调整为10,动画就会慢10倍的播放。

好了,到此该设置的设置完成了,小小总结一下:

ofInt中实例化了一个ObjectAnimator对象,然后设置了target,propName,values(PropertyValuesHolder) ;然后分别在setInterpolator,setDuration设置了Interpolator和duration。其中setEvaluator是给values[0],以及keyframeSet设置估值算法。

PropertyValueHolder实际上是IntPropertyValueHolder类型对象,包含propName,valueType,keyframeSet .

keyframeset中存了Keyframe集合,keyframe中存储了(fraction , valuetype , value , hasValue)。

以上都比较简单,关键就是看start()方法中,如何将这些属性进行合理的处理调用神马的。

5、start

喝杯水,小憩一下,准备征战start()方法。

@Override    public void start() {        super.start();    }ValueAnimator@Override    public void start() {        start(false);    }ValueAnimatorprivate void start(boolean playBackwards) {        if (Looper.myLooper() == null) {            throw new AndroidRuntimeException("Animators may only be run on Looper threads");        }        mPlayingBackwards = playBackwards;        mCurrentIteration = 0;        mPlayingState = STOPPED;        mStarted = true;        mStartedDelay = false;        mPaused = false;        AnimationHandler animationHandler = getOrCreateAnimationHandler();        animationHandler.mPendingAnimations.add(this);        if (mStartDelay == 0) {            // This sets the initial value of the animation, prior to actually starting it running            setCurrentPlayTime(0);            mPlayingState = STOPPED;            mRunning = true;            notifyStartListeners();        }        animationHandler.start();    }   
最终调用了ValueAnimator的statr(playBackwards)方法;

15-20行:设置了关于动画的一些标志位,mPlayingBackwards 表示动画是否reverse;mCurrentIteration 记录当前的动画的执行次数(与setRepeatCount有关);mPlayingState 动画的状态为STOPPED;还有些其他的标志位;

21行:生成一个AnimationHandler对象,getOrCreateAnimationHandler就是在当前线程变量ThreadLocal中取出来,没有的话,则创建一个,然后set进去。

AnimationHandler中包含一些List集合用于存储各种状态的ValueAnimator。

22行:将当前ValueAnimator对象,加入 animationHandler.mPendingAnimations 集合。

23行:未设置mStartDelay,默认为0,则进入循环;

24行: setCurrentPlayTime(0);一会需要细说

25-26行:设置些状态。

27行:回调监听动画的接口AnimatorListener的onAnimationStart方法,如果你设置了回调监听,此时就会进行回调;

最后30行:调用animationHandler.start();需要细说;


好了,有两个方法需要细说,首先看setCurrentPlayTime(0)

 public void setCurrentPlayTime(long playTime) {        initAnimation();        long currentTime = AnimationUtils.currentAnimationTimeMillis();        if (mPlayingState != RUNNING) {            mSeekTime = playTime;            mPlayingState = SEEKED;        }        mStartTime = currentTime - playTime;        doAnimationFrame(currentTime);    }

首先初始化动画,然后得到当前的系统开始到现在的时间currentTime;设置mSeekTime,设置当前状态为SEEKED;然后使用mSeekTime-playTime得到动画现在需要执行的时间;最后调用doAnimationFrame(currentTime),稍后看其代码;

关于initAnimation(),实际就是去设置我们ValueAnimator中存储的mValues,也就是IntPropertyValueHolder的mEvaluator;

 void initAnimation() {        if (!mInitialized) {            int numValues = mValues.length;            for (int i = 0; i < numValues; ++i) {                mValues[i].init();            }            mInitialized = true;        }

PropertyValuesHolder的init方法:

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            mKeyframeSet.setEvaluator(mEvaluator);        }    }
其实就是遍历设置PropertyValuesHolder中的mEvaluator属性,默认根据valueType进行判断,IntEvaluator或者FloatEvaluator。

接下来应该看doAnimationFrame(currentTime);了

 final boolean doAnimationFrame(long frameTime) {                final long currentTime = Math.max(frameTime, mStartTime);        return animationFrame(currentTime);    }

内部调用了:animationFrame(currentTime);

boolean animationFrame(long currentTime) {        boolean done = false;        switch (mPlayingState) {        case RUNNING:        case SEEKED:            float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;            if (fraction >= 1f) {               //...            }            if (mPlayingBackwards) {                fraction = 1f - fraction;            }            animateValue(fraction);            break;        }        return done;    }

这里通过判断当前动画的状态,给出fraction,默认传入的就是(float)(currentTime - mStartTime) / mDuration,动画执行的时间除以总的时间比值;

接下来调用了animateValue(fraction)

在animateValue的内部,会将传入的fraction,交给mInterpolator.getInterpolation(fraction);方法,获得插值器处理后的fraction;然后在将fraction交给估值算法mEvaluator.evaluate(fraction, firstValue, lastValue)).intValue();进行计算得到当前时间点,属性应该的值;最后会反射对我们设置的属性进行设置。

终于看到,对我们的属性的值进行设置了,偶也~~当然了,动画如果没结束,应该每隔一定的帧数,再次调用,嗯,的确是这样的,你看到animationFrame最后是不是有个返回值,这个值会在fraction>=1的时候返回true;

我们还是先看看animateValue方法:

  void animateValue(float fraction) {        fraction = mInterpolator.getInterpolation(fraction);        mCurrentFraction = fraction;        int numValues = mValues.length;        for (int i = 0; i < numValues; ++i) {            mValues[i].calculateValue(fraction);        }        if (mUpdateListeners != null) {            int numListeners = mUpdateListeners.size();            for (int i = 0; i < numListeners; ++i) {                mUpdateListeners.get(i).onAnimationUpdate(this);            }        }       int numValues = mValues.length;        for (int i = 0; i < numValues; ++i) {            mValues[i].setAnimatedValue(mTarget);        }    }

首先将fraction交给给mInterpolator.getInterpolation(fraction);得到计算后的fraction;

然后for循环遍历调用IntPropertyValueHolder的calculateValue方法:

  void calculateValue(float fraction) {        mAnimatedValue = mKeyframeSet.getValue(fraction);    }

在其内部,调用了mKeyframeSet的getValue,这里注意我们的IntKeyFrameSet,千万不要看错方法了。

 @Override    public Object getValue(float fraction) {        return getIntValue(fraction);    }public int getIntValue(float fraction) {        if (mNumKeyframes == 2) {            if (firstTime) {                firstTime = false;                firstValue = ((IntKeyframe) mKeyframes.get(0)).getIntValue();                lastValue = ((IntKeyframe) mKeyframes.get(1)).getIntValue();                deltaValue = lastValue - firstValue;            }            if (mInterpolator != null) {                fraction = mInterpolator.getInterpolation(fraction);            }            if (mEvaluator == null) {                return firstValue + (int)(fraction * deltaValue);            } else {                return ((Number)mEvaluator.evaluate(fraction, firstValue, lastValue)).intValue();            }        }        //...省略了很多代码    }

在其内部,因为我们只设置了一个目标属性值,所以只有两个关键帧;

然后16-20行,调用估值算法的mEvaluator.evaluate方法,可以看到如果mEvaluator == null直接调用了firstValue + (int)(fraction * deltaValue);其实这个就是IntEvaluator的默认实现。

好了,for循环结束了,经过我们插值器和估值算法得出的值,最终给了IntPropertyValueHolder的mIntAnimatedValue属性;

回到animateValue方法:在animateValue的8-12行,继续回调动画监听onAnimationUpdate(this);方法;

animateValue的15-18行:循环拿到(其实我们就只有一个属性)我们的IntPropertyValueHolder调用setAnimatedValue,进行反射为我们的属性设置值,反射需要一些东西,比如target,propname,以及该属性应该设置的值;这三个参数在哪呢?target作为参数传入了,propName初始化的时候就设置了,至于该属性应该设置的值,上面有一句:“ 好了,for循环结束了,经过我们插值器和估值算法得出的值,最终给了IntPropertyValueHolder的mIntAnimatedValue属性” 。是不是全了~~反射的代码就不贴了。

好了,到此,我们属性动画,设置的各种值,经过重重的计算作用到了我们的属性上,反射修改了我们的属性。到此我们已经完成了一大半,但是貌似还少了个,每隔多少帧调用一次~~

嗯,的确是的,跨度好大,现在回到我们的start方法,最后一行:调用animationHandler.start();这个还没细说呢~~

animationHandler我们上面已经介绍了,存储在当前线程的ThreadLocal里面,里面放了一些集合用于存储各种状态的ObjectAnimator,我们当前的ObjectAnimator对象也存储在其mPendingAnimations的集合中(上面提到过~~)。

/**         * Start animating on the next frame.         */        public void start() {            scheduleAnimation();        }private void scheduleAnimation() {            if (!mAnimationScheduled) {                mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);                mAnimationScheduled = true;            }        }

start内部最终调用了mChoreographer.postCallback,其中有一个参数是this;至于什么是Choreographer,暂时不用管;但是你需要知道一件事,其实我们的animationHandler是Runnable的子类,而mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);类似与handler发送消息,最终执行这个Runnable的run方法。

说这么多,其实就是一句话,这里调用了animationHandler的 run方法。

 public void run() {            mAnimationScheduled = false;            doAnimationFrame(mChoreographer.getFrameTime());        }private void doAnimationFrame(long frameTime) {            while (mPendingAnimations.size() > 0) {                ArrayList<ValueAnimator> pendingCopy =                        (ArrayList<ValueAnimator>) mPendingAnimations.clone();                mPendingAnimations.clear();                int count = pendingCopy.size();                for (int i = 0; i < count; ++i) {                    ValueAnimator anim = pendingCopy.get(i);                    // If the animation has a startDelay, place it on the delayed list                    if (anim.mStartDelay == 0) {                        anim.startAnimation(this);                    } else {                        mDelayedAnims.add(anim);                    }                }            }            //...省略了一些代码                        // Now process all active animations. The return value from animationFrame()            // tells the handler whether it should now be ended            int numAnims = mAnimations.size();            for (int i = 0; i < numAnims; ++i) {                mTmpAnimations.add(mAnimations.get(i));            }            for (int i = 0; i < numAnims; ++i) {                ValueAnimator anim = mTmpAnimations.get(i);                if (mAnimations.contains(anim) && anim.doAnimationFrame(frameTime)) {                    mEndingAnims.add(anim);                }            }            mTmpAnimations.clear();            if (mEndingAnims.size() > 0) {                for (int i = 0; i < mEndingAnims.size(); ++i) {                    mEndingAnims.get(i).endAnimation(this);                }                mEndingAnims.clear();            }            // If there are still active or delayed animations, schedule a future call to            // onAnimate to process the next frame of the animations.            if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) {                scheduleAnimation();            }        }

6-20行:while循环,遍历所有在mPendingAnimations中的ObjectAnimator,依次调用anim.startAnimation(this);

在anim.startAnimation(this);内部其实主要就一行代码:handler.mAnimations.add(this); 将当前动画加入animationHandler的mAnimations集合;

26-29行:将animationHandler的mAnimations集合中的每个anim,加入到mTmpAnimations中;

30-35行:依次调用mTmpAnimations中的anim,anim.doAnimationFrame(frameTime)

doAnimationFrame(frameTime)上面已经分析过了,如果返回true,即doAnimationFrame的done为true,则将该动画加入到结束动画集合。

37-43行:循环调用mEndingAnims,mEndingAnims.get(i).endAnimation(this);内部,会将动画移除mAnimations,回调动画监听接口onAnimationEnd;以及重置各种标志变量。 46-48行:如果mAnimations不为null,则再次调用scheduleAnimation(); 哈哈,终于终于发现了,每隔多少帧调用一次动画的地方了~~尼玛这个scheduleAnimation,不就是animationHandler的 run方法调用的么~~
前面已经描述过animationHandler的 run方法中通过计算属性应该的值,反射设置;加上我们这里的动画没结束,就会再次调用该run方法内部一致的方法~~~


搜噶,到此~~我们的属性动画的流程已经完美跑通了~~~


对了,看完以后,和我们文章开始的预期符合么,其实我觉得差不多~~


4、总结

其实看源码的目的,最终就是为了总结,尼玛这么长的代码谁也记不住。。。所以看完记得总结:

ofInt中实例化了一个ObjectAnimator对象,然后设置了target,propName,values(PropertyValuesHolder) ;然后分别在setInterpolator,setDuration设置了Interpolator

和duration。其中setEvaluator是给PropertyValuesHolder,以及keyframeSet设置估值算法。

PropertyValueHolder实际上是IntPropertyValueHolder类型对象,包含propName,valueType,keyframeSet .

keyframeset中存了Keyframe集合,keyframe中存储了(fraction , valuetype , value , hasValue)。

上述其实都是设置各种值什么的。真正核心要看start~

start()中:

首先,步骤1:更新动画各种状态,然后初步计算fraction为(currentTime - mStartTime) / mDuration;然后将这个fraction交给我们的插值器计算后得到新的fraction,再将新的fraction交给我们的估值算法,估值算法根据开始、结束、fraction得到当前属性(动画作用的属性)应该的值,最大调用反射进行设置;

当然了:start中还会根据动画的状态,如果没有结束,不断的调用scheduleAnimation();该方法内部利用mChoreographer不断的去重复我们的上述步骤1。


好了,顺便说一句,在看源码的时候,一定要注意,你点进去的有可能不是真正运行时调用的,记得查看该方法子类,比如我们查看ObjectAnimator的方法,可能我们某个方法会跟到其父类ValueAnimator的方法,但是记得查看ObjectAnimator是否复写了该方法~~如果复写了,你该看的应该是ObjectAnimator的方法~~~


源码,嗯?木有源码点击下载了~~~



我新建了一个QQ群,方便大家交流。群号:423372824


----------------------------------------------------------------------------------------------------------

博主部分视频已经上线,如果你不喜欢枯燥的文本,请猛戳(初录,期待您的支持):

1、Android 自定义控件实战 电商活动中的刮刮卡

2、Android自定义控件实战 打造Android流式布局和热门标签

3、Android智能机器人“小慕”的实现

4、高仿QQ5.0侧滑

5、高仿微信5.2.1主界面及消息提醒









更多相关文章

  1. Android(安卓)开发笔记 动画效果 --Animation
  2. Android动态设置edittext的hint属性显示的提示文字大小
  3. android scroller用法及属性
  4. listview自定义背景以及item自定义背景
  5. 《Android开发从零开始》――10. LinearLayout学习
  6. Android(安卓)UI之ImageView旋转的几种方式
  7. android开发之给LinearLayout增加点击效果
  8. Android夜间模式实现(系统自带)
  9. 监控android binder size

随机推荐

  1. python操作Excel文件报lrd.biffh.XLRDErr
  2. 新手如何通过VMware安装部署CentOS 7(详细
  3. 2021年值得关注的5大基础设施和运营趋势
  4. 学习了TP6之后,心理慌得一批!
  5. Linux用户管理误操作处理——未完全删除
  6. Linux切换图形界面与命令界面【CentOS】
  7. 云计算入门从零到一成为云计算/运维工程
  8. Linux详解进程管理——ps进程、top、后台
  9. 【过关斩将】高胜寒带你理清 “为什么从
  10. Linux 下CentOS文件管理、用户管理及权限