Android动画的使用

       在APP开发的过程中,在合适的时机引入合适的动画。会让我们的APP动起来,更加的吸引眼球。这里我们就来总结下Android里面的动画。按照我们的理解Android里面动画分为三类:属性动画(Property Animation)、视图动画(View Animation)、过渡动画(Transition Animation)。

       在说Android的这三种动画之前,我们先的来提一下TimeInterpolator插值器。我们可以把所有的动画简单的认为是:在指定的时间内,从第一种状态动态的过渡到第二种状态的一个过程。那在这个持续时间内动画的执行过程中是怎么个变化的呢,线性变换,加速变换还是其他的什么变换。所以这里就会引入一个插值器的概念。插值器改变的就是不同时间进度上的值,虽然时间的流逝是线性的(速度是不变),但是插值器可以通过改变不同时间上对应的动画的值,来达到各种各样的变换效果。Android系统也给默认提供了九种插值器的效果。当然也可以去自定义一个插值器。

系统提供的九种TimeInterpolator插值器

Interpolator Class xml Resource ID Description 数学建模
LinearInterpolator @android:anim/linear_interpolator 线性插值器 Android动画的使用_第1张图片
AccelerateInterpolator @android:anim/accelerate_interpolator 加速度插值器 Android动画的使用_第2张图片
DecelerateInterpolator @android:anim/decelerate_interpolator 减速插值器 Android动画的使用_第3张图片
AccelerateDecelerateInterpolator @android:anim/accelerate_decelerate_interpolator 先加速后减速插值器 Android动画的使用_第4张图片
AnticipateInterpolator @android:anim/anticipate_interpolator 在开始的时候向后然后向前甩 Android动画的使用_第5张图片
OvershootInterpolator @android:anim/overshoot_interpolator 向前甩一定值后再回到原来位置 Android动画的使用_第6张图片
AnticipateOvershootInterpolator @android:anim/anticipate_overshoot_interpolator 开始的时候向后然后向前甩一定值后返回最后的值 Android动画的使用_第7张图片
BounceInterpolator @android:anim/bounce_interpolator 动画结束的时候弹起 Android动画的使用_第8张图片
CycleInterpolator @android:anim/cycle_interpolator 动画循环播放特定的次数,速率改变沿着正弦曲线 Android动画的使用_第9张图片

一、属性动画(Property Animation)

       属性动画就是在动画的过程中通过改变对象的属性来达到动态效果。这里的对象不局限于视图View,可以是任何你想要的对象。属性动画也是我们目前使用的最多的一种动画。

关于对象的属性可以把属性简单的理解为对象的参数。

1.1、属性动画源码过程分析

       我们站在源代码的角度上来看属性动画。和属性动画相关的类有:AnimatorSet、ObjectAnimator、ValueAnimator、TimeInterpolator插值器、TypeEvaluator估值器。

  • ObjectAnimator、ValueAnimator:都是用于指定单个动画,唯一区别就是ObjectAnimator可以直接指定动画对象的属性,当然这个动画对象必须实现相应的get,set方法。ValueAnimator的使用呢,咱们就需要自己去处理ValueAnimator.AnimatorUpdateListener监听每个时刻动画数据的变化,然后把变化的值设置给指定对象的属性。更加详细的内容请参考:Android属性动画ValueAnimator源码简单分析、Android属性动画ObjectAnimator源码简单分析

  • AnimatorSet:可以定义多个(AnimatorSet、ObjectAnimator、ValueAnimator)动画,是动画的集合。更加详细的内容请参考:Android属性动画AnimatorSet源码简单分析。

  • TimeInterpolator插值器:根据时间流速计算数值变化比例,重点是数值变化的比例。这个咱们在文章的一开始已经提到了。更加详细的内容请参考:Android动画TimeInterpolator(插值器)和TypeEvaluator(估值器)分析。

  • TypeEvaluator估值器:根据插值器计算出的数值变化比例,计算最终的具体数值。当要变化的值自定义对象的时候可能就需要自定义TypeEvaluator估值器了。更加详细的内容请参考:Android动画TimeInterpolator(插值器)和TypeEvaluator(估值器)分析。

       因为前段时间通过源码走读的方式已经对属性动画的过程做了一个简单的分析,所以上面我很多地方我就直接贴了链接地址。如果大家在看的过程中有什么疑问可以留言,在我的能力范围会为大家解答的。

1.2、属性动画的使用

       属性动画的使用,一般可以分为以下几个步骤:

  1. 确定属性动画作用的对象。对象可以是View也可以是其他任何对象。

  2. 确定属性动画作用对象属性,和属性值的变化范围。

  3. 定义属性动画:AnimatorSet、ObjectAnimator、ValueAnimator。

  4. 选择合适的插值器。(从系统给提供的九种插值器里面选择或者直接自定义一个插值器)。

  5. 选择合适的估值器。(如果我们属性的参数是int,float以外的时候,可能就需要我们自定义估值器了)。

       关于插值器和估值器的使用大家可以参考下Android动画TimeInterpolator(插值器)和TypeEvaluator(估值器)分析。里面有一个非常简单的自定义插值器的实例。这里我们重点瞧一瞧第三点定义属性动画。和所有的动画一样属性动画的定义也有两种方式:XML文件的方式、JAVA代码的方式。

1.2.1、XML文件定义属性动画

       XML属性动画资源文件建议放在 res/animation 文件夹下(也可以放置在res/anim下)。XML属性动画资源文件相关属性如下:


<set xmlns:android="http://schemas.android.com/apk/res/android"
    -- 动画的执行方式;sequentially:顺序执行、together:同时执行 -->
    android:ordering="sequentially | together">

    
    <objectAnimator
        -- 属性,一般在View里面有对应的 setXX() getXX()的方法 -->
        android:propertyName="string"
        
        android:duration="int"
        
        android:valueFrom="float | int | color"
        
        android:valueTo="float | int | color"
        
        android:startOffset="int"
        
        android:repeatCount="int"
        
        android:repeatMode=["repeat" | "reverse"]
        
        android:valueType=["intType" | "floatType"]
        
        android:interpolator=[res anim]/>

    
    <animator
        android:duration="int"
        android:valueFrom="float | int | color"
        android:valueTo="float | int | color"
        android:startOffset="int"
        android:repeatCount="int"
        android:repeatMode=["repeat" | "reverse"]
        android:valueType=["intType" | "floatType"]/>

    <set>
    ...
    set>
set>

       XML定义属性动画,具体实例:

第一步,创建一个XML属性动画资源文件(res/animation目录下)

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="together">
    <set android:ordering="together">
        <objectAnimator
            android:propertyName="rotation"
            android:duration="10000"
            android:valueFrom="0f"
            android:valueTo="360f"
            android:valueType="floatType"
            android:interpolator="@android:anim/accelerate_interpolator"/>
    set>
    <objectAnimator
        android:propertyName="alpha"
        android:duration="10000"
        android:valueFrom="0"
        android:valueTo="1f"
        android:valueType="floatType" />
set>

第二步,启动资源文件动画

    AnimatorSet animationSet = (AnimatorSet) AnimatorInflater.loadAnimator(mContext, R.animator.property_set);
    animationSet.setTarget(mImageView);
    animationSet.start();

       到这里XML定义的属性动画就播放出来,最关键的地方还是XML属性动画文件的配置。

1.2.2、JAVA方式定义属性动画

       JAVA方式定义属性动画可能关键的部分还是的知道AnimatorSet、ObjectAnimator、ValueAnimator这几个类里面动画参数设置的一些API了。和上面提到的XML属性动画资源文件相关属性是一一对应的,肯定有相关的setXX() getXX()方法的。接下来我们通过一个简单的实例来瞧一瞧JAVA方式定义和播放属性动画。

    AnimatorSet animatorSet = new AnimatorSet();
    ObjectAnimator objectXAnimator = ObjectAnimator.ofFloat(mImageView, "rotation", 0f, 360f);
    objectXAnimator.setDuration(10000);
    animatorSet.playTogether(objectXAnimator);
    ObjectAnimator objectAlphaAnimator = ObjectAnimator.ofFloat(mImageView, "alpha", 0f, 1f);
    objectAlphaAnimator.setDuration(10000);
    AnimatorSet animatorSetResult = new AnimatorSet();
    animatorSetResult.playTogether(animatorSet, objectAlphaAnimator);
    animatorSetResult.start();

二、视图动画(View Animation)

       视图动画(View Animation),通过确定视图开始时候的样式和视图结束时候的样式、中间所有动画变化过程则由系统补全的动画效果。视图动画分为两类:补间动画(Tween animation)、帧动画(Frame animation)两种。

这里明确的指定了视图动画的作用是视图(View,View的子类)。所以视图动画只对View或者View的子类有作用。

2.1、补间动画(Tween animation)

       所有补间动画的基类都是Animation,而且所有补间动画的变换过程都是围绕Matrix做操作,Matrix是一个3*3的变形矩阵。通过高等数学里面矩阵的运算公式做平移,缩放,斜切,旋转等变换。简单来说就是在动画过程中随着时间的推移Matrix通过某种变换(平移,缩放,斜切,旋转等)转换到目标Matrix。然后把Matrix的变化作用在视图上。

2.1.1、补间动画源码简单分析

       虽然补间动画的过程是通过View自动补全的,但是咱们还是想知道系统里面处理补间动画的大概流程。虽然不可能全部知道,但是知道个大概的流程总归是好的。所有补间动画开始的动作都是从View里面的startAnimation()函数开始的。

    public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        setAnimation(animation);
        invalidateParentCaches();
        invalidate(true);
    }

很显然startAnimation()函数的调用会让View重绘,之后就会调用到draw()函数了,我们直接看带有三个参数的的draw()函数

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ......
        final Animation a = getAnimation();//如果有设置补间动画
        if (a != null) {
            //applyLegacyAnimation函数会把每次animation的变化都存到parent.getChildTransformation()里面
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            //获取进过applyLegacyAnimation变换之后的Matrix
            transformToApply = parent.getChildTransformation();
        } else {
            ......
        }
        ......
        if (!drawingWithRenderNode || transformToApply != null) {
            restoreTo = canvas.save();
        }
        ......
        float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
        if (transformToApply != null
            || alpha < 1
            || !hasIdentityMatrix()
            || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
            if (transformToApply != null || !childHasIdentityMatrix) {
                ......
                if (transformToApply != null) {
                    if (concatMatrix) {
                        if (drawingWithRenderNode) {
                            renderNode.setAnimationMatrix(transformToApply.getMatrix());
                        } else {
                            // Undo the scroll translation, apply the transformation matrix,
                            // then redo the scroll translate to get the correct result.
                            canvas.translate(-transX, -transY);
                            //把变换的结果应用到canvas上,这样绘制出来的视图就是动画变换之后的结果了
                            canvas.concat(transformToApply.getMatrix());
                            canvas.translate(transX, transY);
                        }
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }

                    float transformAlpha = transformToApply.getAlpha();
                    if (transformAlpha < 1) {
                        alpha *= transformAlpha;
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }
                }
                ......
            }
        } else if ((mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
            ......
        }
        if (restoreTo >= 0) {
            canvas.restoreToCount(restoreTo);
        }
        ......
    }
    private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
                                         Animation a, boolean scalingRequired) {
        ......
        //动画变化过程中,变换的结果要保存的变量
        final Transformation t = parent.getChildTransformation();
        //动画变化更新Matrix结果,这样parent.getChildTransformation()里面保存的就是变换之后的结果
        boolean more = a.getTransformation(drawingTime, t, 1f);
        ......
    }

关键在more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);一句的调用,调用这句后动画变换之后的结果会保存在parent.getChildTransformation()里面,之后在通过canvas.concat(transformToApply.getMatrix());把变换的结果运用到视图Cavans上来,产生相应的动画效果。

上面对补间动画的实现做了一个非常非常简单的分析,只是摘出了关键的部分。如果想了解更加详细的细节就得去好好的啃源码了。

2.1.2、补间动画的使用

       补间动画的使用,三个步骤:

  1. 创建补间动画。

  2. 执行补间动画。

  3. 监听补间动画的执行过程(AnimationListener)。

同样和其它的动画一样补间动画也可以通过XML(res/anim目录下)、JAVA两种方式来创建。

       所有补间动画都有一个共同的基类,所以他们有一些公共的属性:

    android:duration="2000"
    android:repeatMode="restart"
    android:repeatCount="3"
    android:fillAfter="true"
    android:interpolator="@android:anim/decelerate_interpolator"

(这里只是列出XML里面的设置方式,JAVA里面也是一样的,都有相应的setXX() getXX()方法)

       Android系统已经默认给咱提供了五种补间动画。大部分情况下这五种补间动画都能满足我们的需求。

名称 原理 对应Animation子类
平移动画(Transition) 移动视图的位置 TranslateAnimation
缩放动画(Scale) 放大/缩小 视图的大小 ScaleAnimation
旋转动画(Rotate) 旋转视图的角度 RotateAnimation
透明度动画(Alpha) 改变视图的透明度 AlphaAnimation
组合动画(Set) 把多种动画组合在一起执行 AnimationSet
2.1.2.1、平移动画(Transition)TranslateAnimation

       改变View的位置。


<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXDelta=""
    android:toXDelta=""
    android:toYDelta=""
    />

补间动画里面所有距离的设置都三种形式:具体的值、值%、值%p。其中值%表示相对自身宽度或者高度的多少,值%p则是相对父控件宽度或者高度的多少。

2.1.2.2、缩放动画(Scale) ScaleAnimation

       控制View的缩放。


<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:pivotY=""
    android:fromXScale=""
    android:fromYScale=""
    android:toXScale=""
    android:toYScale=""
    />
2.1.2.3、旋转动画(Rotate) RotateAnimation

       控制View的旋转。


<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees=""
    android:pivotY=""
    android:toDegrees=""
    />
2.1.2.4、透明度动画(Alpha) AlphaAnimation

       控制View的透明度。


<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha=""
    />
2.1.2.5、组合动画(Set) AnimationSet

       多个动画组合起来对View起作用。


<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator=""
    >

set>

关于属性动画的具体实例,这里我们就没有列出来了,在最下面DEMO里面有对应的实例。

2.2、帧动画(Frame animation)

       帧动画,就是由一帧帧的图片组合出来。通过指定图片展示的顺序,达到动画的展示的动态效果。换句话说就是在动画的过程中替换视图的背景。

2.2.1、帧动画源码简单分析

       帧动画对应的视图背景类是AnimationDrawable。我们先看下帧动画是怎么播放的,然后在看下通过布局文件里面设置android:src或者android:background是怎么解析为AnimationDrawable对象的。

2.2.1.1、AnimationDrawable播放动画简单分析

       帧动画的播放是通过调用AnimationDrawable类的start()函数开始的。

AnimationDrawable类

    public void start() {
        ......
        if (!isRunning()) {
            // Start from 0th frame.
            //从第一帧开始播放动画
            setFrame(0, false, mAnimationState.getChildCount() > 1
                               || !mAnimationState.mOneShot);
        }
    }

    private void setFrame(int frame, boolean unschedule, boolean animate) {
        ......
        //这里就把当前帧对应的图片给了对应的视图
        selectDrawable(frame);
        ......
        if (animate) {
            ......
            //很显然,定时任务,指定时间之后在调用this里面的run函数。
            scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
        }
    }

start()函数里面会调用setFrame()函数,setFrame()从字面意思也能看出来应该是去设置对应的帧

AnimationDrawable类

    private void setFrame(int frame, boolean unschedule, boolean animate) {
        ......
        //这里就把当前帧对应的图片给了对应的视图
        selectDrawable(frame);
        ......
        if (animate) {
            ......
            //很显然,定时任务,指定时间之后在调用this里面的run函数。
            scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
        }
    }

selectDrawable(frame);的调用就把当前视图对应的背景图片给换掉了,selectDrawable()函数的调用会让DrawableContainer去重绘,调用DrawableContainer里面的draw()函数,在draw()函数里面替换帧对应的图片。然后scheduleSelf()函数相当于启动了一个定时任务。我们找到对应run()函数。

AnimationDrawable类

    @Override
    public void run() {
        nextFrame(false);
    }

    private void nextFrame(boolean unschedule) {
        //下一帧
        int nextFrame = mCurFrame + 1;
        ......
        //又回到了setFrame函数
        setFrame(nextFrame, unschedule, !isLastFrame);
    }

我们往最简单的说AnimationDrawable播放动画就是设置一个帧之后启动一个定时任务然后去设置下一个帧。

2.2.1.2、android:src或者android:background怎么解析为AnimationDrawable对象

       为了分析帧动画XML资源文件是怎么解析成AnimationDrawable对象的。咱先找一个入口,咱们就从View类的setBackgroundResource(id)开始。

View

    @RemotableViewMethod
    public void setBackgroundResource(@DrawableRes int resid) {
        ......
        Drawable d = null;
        if (resid != 0) {
            //通过资源id,解析到对应的Drawable
            d = mContext.getDrawable(resid);
        }
        ......
    }

这样就直接到Context里面的getDrawable()函数了,-> Resources类的getDrawable()函数了,->Resources类 getDrawableForDensity()函数,->ResourcesImpl类的loadDrawable()

ResourcesImpl

    @Nullable
    Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
                          int density, @Nullable Resources.Theme theme)
        throws Resources.NotFoundException {
            ......
            boolean needsNewDrawableAfterCache = false;
            if (cs != null) {
                dr = cs.newDrawable(wrapper);
            } else if (isColorDrawable) {
                dr = new ColorDrawable(value.data);
            } else {
                //解析xml,咱们从这里跟进去
                dr = loadDrawableForCookie(wrapper, value, id, density, null);
            }
            ......
    }

关键调用在loadDrawableForCookie()函数

ResourcesImpl

    private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
                                           int id, int density, @Nullable Resources.Theme theme) {
        ......
        final String file = value.string.toString();
        ......
        final Drawable dr;

        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
        try {
            if (file.endsWith(".xml")) {
                //从xml文件解析资源文件
                final XmlResourceParser rp = loadXmlResourceParser(
                    file, id, value.assetCookie, "drawable");
                dr = Drawable.createFromXmlForDensity(wrapper, rp, density, theme);
                rp.close();
            } else {
                ......
            }
        } catch (Exception e) {
            ......
        }
        ......
    }

这里关键在Drawable.createFromXmlForDensity()调用

Drawable

    public static Drawable createFromXmlForDensity(@NonNull Resources r,
                                                   @NonNull XmlPullParser parser, int density, @Nullable Resources.Theme theme)
        throws XmlPullParserException, IOException {
        ......
        Drawable drawable = createFromXmlInnerForDensity(r, parser, attrs, density, theme);
        ......
    }

    static Drawable createFromXmlInnerForDensity(@NonNull Resources r,
                                                 @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density,
                                                 @Nullable Resources.Theme theme) throws XmlPullParserException, IOException {
        //r.getDrawableInflater()就是DrawableInflater类
        return r.getDrawableInflater().inflateFromXmlForDensity(parser.getName(), parser, attrs,
                                                                density, theme);
    }

DrawableInflater

    @NonNull
    Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
                                      @NonNull AttributeSet attrs, int density, @Nullable Resources.Theme theme)
        throws XmlPullParserException, IOException {
        ......
        //通过tag name得到各种Drawable对应的对象,比如我们这里是"animation-list" 得到AnimationDrawable
        Drawable drawable = inflateFromTag(name);
        if (drawable == null) {
            drawable = inflateFromClass(name);
        }
        drawable.setSrcDensityOverride(density);
        //进入到各种Drawable对应的对象的inflate解析里面去,比如我们这里是进入AnimationDrawable的inflate解析里面去
        drawable.inflate(mRes, parser, attrs, theme);
        ......
    }

这里通过XML文件的tag名字找到对应的Drawable对象,我们这里分析的是帧动画,对应的tag是animation-list所以对应的Drawable对象是AnimationDrawable,这样所有的解析工作就都过渡到AnimationDrawable类里面去了,最终在AnimationDrawable类的inflate()函数里面解析出每一帧具体的数据。到此帧动画xml的解析就结束了,剩下的就是自己调用start()函数启动动画。

上面的流程有很多地方都没有深究进去,看源码。在没有特殊要求的情况下,我都是看一个大概的流程。做到心里有数。

2.2.2、帧动画的简单使用

       所有的动画都一样,可以通过XML、JAVA两种方式来定义动画对象。

2.2.2.1、XML定义帧动画

       XML设置帧动画的资源文件 res/drawable文件夹目录下.


<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    -- 是否连续播放 -->
    android:oneshot=["true" | "false"] >
    <item
        -- 每一帧的图片 -->
        android:drawable="@[package:]drawable/drawable_resource_name"
        
        android:duration="integer" />
animation-list>

一个简单的XML定义帧动画的实例


<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item
        android:drawable="@mipmap/img_miao1"
        android:duration="80" />
    <item
        android:drawable="@mipmap/img_miao2"
        android:duration="80" />
    <item
        android:drawable="@mipmap/img_miao3"
        android:duration="80" />
animation-list>

       播放动画

AnimationDrawable animationDrawable = (AnimationDrawable) mImageAnimation.getDrawable();
                animationDrawable.start();
2.2.2.2、JAVA定义帧动画

       通过JAVA的方式来定义帧动画这个就简单的多了。我们直接用一个简单的实例来说明。

    AnimationDrawable animationDrawable = new AnimationDrawable();
    animationDrawable.addFrame(getResources().getDrawable(R.mipmap.img_miao1), 80);
    animationDrawable.addFrame(getResources().getDrawable(R.mipmap.img_miao2), 80);
    animationDrawable.addFrame(getResources().getDrawable(R.mipmap.img_miao3), 80);
    animationDrawable.setOneShot(false);
    mImageAnimation.setBackgroundDrawable(animationDrawable);
    animationDrawable.start();

       按照上面的方式实现帧动画,使用不当容易发生OOM的情况,而且效率比较低。给大家推荐YY公司出的一个实现帧动画的开源库SVGA Animation SVGA Animation。提供了高性能动画播放体验。同时SVGA是一种同时兼容 iOS / Android / Web 多个平台的动画格式。

三、过渡动画(Transition Animation)

       在Android 4.4 Transition 就已经引入了,但在Android 5.0(API 21)之后,Transition 被更多的应用起来。相对于View Animation或Property Animator,Transition动画更加具有特殊性,Transition可以看作对Property Animator的高度封装。不同于Animator,Transition动画具有视觉连续性的场景切换。

       关于过渡动画(Transition Animation)更加详细的内容,我就偷个懒,请看之前写的一篇Android Transition(Android过渡动画)的介绍。

       到此三种动画,我们都做了一个非常简单的介绍。最后给出文章里面对应的一些DEMO实例的下载地址。DEMO

你可能感兴趣的:(Android,Android成长之路)