Android开发学习笔记——View动画和属性动画
Android中的动画可分为两类,分别为View动画和属性动画,其中View动画可以分为帧动画和补间动画。
View动画
帧动画
我们知道,实际上我们日常中观看的电影电视其实都是由一帧帧的画面组合而成的,其中一帧就代表一幅画,当每秒切换的帧数(即帧率)足够多那么就能够形成一个连贯的视频,而帧率越高那么视频就会显得越流畅。而帧动画就是利用这个原理,其实际上就是顺序播放的一组预定义好的图片,类似于gif图的实现,同样帧率越高,动画就会更加流畅。帧动画的实现方式相对较为简单,主要是通过AnimationDrawable来实现的。
基本实现
xml实现
首先,我们需要通过xml创建一个动画Drawable文件,如下所示:
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<!--oneshot为是否重复播放-->
<!--每个item代表一帧,duration为该帧持续时间-->
<item android:drawable="@drawable/btn_3he1_bofang_1" android:duration="200"/>
<item android:drawable="@drawable/btn_3he1_bofang_2" android:duration="200"/>
<item android:drawable="@drawable/btn_3he1_bofang_3" android:duration="200"/>
</animation-list>
我们可以看到,帧动画的xml布局是由< animation-list >为根元素的,其oneshot属性代表是否执行一次,animation-list中的每个item就代表了一帧,我们需要为每一帧都指定其drawable和持续时间duration,然后就创建了一个帧动画的drawable了,我们只需要将其作为一个drawable设为view的背景或是src,然后使用start方法播放即可。如下:
val drawable = resources.getDrawable(R.drawable.voice) as AnimationDrawable
ivVoice.setImageDrawable(drawable)
// ivVoice.background = drawable
drawable.start()
// ivVoice.setImageResource(R.drawable.voice)
// val drawable = ivVoice.drawable as AnimationDrawable
// drawable.start()
可以看到,实际上,我们只需要将其作为一般的drawable来设置即可,然后调用start方法播放动画,这样就实现了一个简单的帧动画。
AnimationDrawable实现
除了使用xml创建帧动画之外,我们也可以直接通过AnimationDrawable来动态创建一个帧动画,我们需要调用addFrame方法为帧动画添加每一帧的drawable和时长,然后同样将其设置为view的背景,调用start方法播放即可。如下所示:
val drawable = AnimationDrawable()
drawable.addFrame(resources.getDrawable(R.drawable.btn_3he1_bofang_1), 200)
drawable.addFrame(resources.getDrawable(R.drawable.btn_3he1_bofang_2), 200)
drawable.addFrame(resources.getDrawable(R.drawable.btn_3he1_bofang_3), 200)
drawable.isOneShot = false
ivVoice.setImageDrawable(drawable)
drawable.start()
我们可以看到,实际上其实现和xml相同,同样设置了每一帧的drawable和时长,设置了是否执行一次,但是相对而言,xml显得更加直观,而且如果帧数多起来,我们还是在代码中实现的话就会显得很乱,因此我们通常是直接使用xml来实现帧动画的。
注意事项
帧动画实现比较简单,但是由于其每一帧都是一张图片,所以我们在使用的时候必须注意尽量避免使用许多尺寸较大的图片,否则很容易造成OOM。
同时,对 AnimationDrawable 调用的 start() 方法应该尽量不要在 Activity 的 onCreate() 方法期间调用,因为 AnimationDrawable 此时可能尚未完全附加到窗口。如果想立即播放动画而无需互动,那么可能需要从 Activity 中的 onStart() 方法进行调用,该方法会在 Android 在屏幕上呈现视图时调用。
补间动画
补间动画是通过对场景中的View对象做图像变换,即alpha(淡入淡出),translate(位移),scale(缩放大小),rotate(旋转)从而实现动画效果的,通过设置动画开始与结束时的位置或者大小等属性,自动将其中间部分补齐,进而实现大小和位置等属性的变化进而实现动画效果,即只需要确定起始帧和结束帧即可,其余帧均自动补齐,不需要像帧动画一样将所有帧都说明。
基本种类和属性
补间动画主要包含了四类动画,平移动画、缩放动画、旋转动画和透明度动画,四类动画又分别对应了TranslateAnimation、ScaleAnimation、RotateAnimation和AlphaAnimation四个类,同时也对应了xml中的<translate>、<scale>、<rotate>和<alpha>四个标签,同时,我们也可以让多个动画通过AnimatSet一同播放。如下表所示:
名称 | 子类 | 标签 | 效果 |
---|---|---|---|
平移动画 | TranslateAnimation | <translate> | 平移view |
缩放动画 | ScaleAnimation | <scale> | 放大或缩小view |
旋转动画 | RotateAnimation | <rotate> | 旋转view |
透明度动画 | AlphaAnimation | <alpha> | 改变view透明度 |
动画集合 | AnimationSet | <set> | 同时播放多个动画 |
在Android开发过程中,我们通常都会采用xml的形式来创建动画,因为xml的可读性往往更加好。其xml的基本语法如下:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="integer"
android:repeatMode="reverse|restart"
android:shareInterpolator="true|false">
<translate
android:fromXDelta="float"
android:fromYDelta="float"
android:toXDelta="float"
android:toYDelta="float" />
<alpha
android:fromAlpha="float"
android:toAlpha="float" />
<scale
android:fromXScale="float"
android:fromYScale="float"
android:pivotX="float"
android:pivotY="float"
android:toXScale="float"
android:toYScale="float" />
<rotate
android:fromDegrees="float"
android:pivotX="float"
android:pivotY="float"
android:toDegrees="float" />
</set>
接下来,我们来大概了解学习下各类动画的实现。
平移动画TranslateAnimation
平移动画的基本属性如下:
- android:fromXDelt:表示动画的X轴起始坐标相对于原点(view初始位置)的位置
- android:fromYDelt:表示动画的Y轴起始坐标相对于原点的位置
- android:toXDelta:表示动画的X轴结束坐标相对于原点的位置
- android:toYDelta:表示动画的Y轴结束坐标相对于原点的位置
创建xml动画文件,如下所示:
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="0"
android:toXDelta="200"
android:fromYDelta="0"
android:toYDelta="200"/>
然后,在代码中使用AnimationUtil.loadAnimation方法引入动画,获取Animation实例,然后使用view的startAnimation方法即可。如下所示:
bt1.setOnClickListener {
val animation = AnimationUtils.loadAnimation(this@AnimationActivity, R.anim.translate)
animation.duration = 2000
view1.startAnimation(animation)
}
也可以直接使用TranslateAnimation来实现,如下所示:
val animation = TranslateAnimation(0f, 200f, 0f, 200f)
animation.duration = 2000
view1.startAnimation(animation)
缩放动画ScaleAnimation
属性如下:
- android:fromXScale:水平方向缩放的起始值,以1为原始大小
- android:fromYScale:竖直方向缩放的起始值,以1为原始大小
- android:pivotX:缩放的轴点x坐标(相对初始坐标),会影响缩放的效果
- android:pivotY:缩放的轴点y坐标(相对初始坐标),会影响缩放的效果
- android:toXScale:水平方向的结束值,以1为原始大小
- android:toYScale:竖直方向的结束值,以1为原始大小
基本使用如下:
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:pivotX="25"
android:pivotY="25"
android:toXScale="0.5"
android:toYScale="1.5"/>
//使用xml文件描述动画
val animation = AnimationUtils.loadAnimation(this@AnimationActivity, R.anim.scale)
//使用代码实现
//val animation = ScaleAnimation(1.0f,0.5f,1.0f,1.5f,25f,25f)
animation.duration = 2000
view1.startAnimation(animation)
旋转动画RotateAnimation
基本属性如下:
android:fromDegrees:旋转开始的角度,0-360
android:pivotX:旋转的轴点x坐标
android:pivotY:旋转的轴点y坐标
android:toDegrees:旋转结束的角度
基本使用如下:
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:pivotX="0"
android:pivotY="0"
android:toDegrees="45"/>
//使用xml描述动画
val animation = AnimationUtils.loadAnimation(this@AnimationActivity, R.anim.rotate)
//RotateAnimation实现
//val animation = RotateAnimation(0f,45f,0f,0f)
animation.duration = 2000
view1.startAnimation(animation)
透明度动画AlphaAnimation
基本属性如下:
- android:fromAlpha:表示透明度的起始值,1为完全不透明
- android:toAlpha:表示透明度的结束值
基本使用如下:
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="1"
android:toAlpha="0.1"/>
//使用xml描述动画
val animation = AnimationUtils.loadAnimation(this@AnimationActivity, R.anim.alpha)
//AlphaAnimation实现
//val animation = AlphaAnimation(1f, 0.1f)
animation.duration = 2000
view1.startAnimation(animation)
动画集合
通过上述小节的介绍,我们了解了view动画四类动画的基本使用,但之前我们都是单独的动画,实际上,我们还可以创建一个动画集合使多个动画一起播放。这时我们就需要使用到AnimationSet了,我们可以将之前的动画组合在一起,如在缩放的同时,改变透明度,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000"
android:repeatMode="reverse"
android:shareInterpolator="true">
<alpha
android:fromAlpha="1"
android:toAlpha="0.1"/>
<scale
android:fromXScale="1.0"
android:fromYScale="1.0"
android:pivotX="25"
android:pivotY="25"
android:toXScale="0.5"
android:toYScale="1.5"/>
</set>
//使用xml描述动画
//val animation = AnimationUtils.loadAnimation(aaa@qq.com, R.anim.test)
//AnimationSet实现
val alpha = AlphaAnimation(1f, 0.1f)
val scale = ScaleAnimation(1.0f,0.5f,1.0f,1.5f,25f,25f)
val animationSet = AnimationSet(true)
animationSet.addAnimation(scale)
animationSet.addAnimation(alpha)
animationSet.duration = 2000
view1.startAnimation(animationSet)
常用属性
- duration:动画时长
- repeatMode:重复模式,包含顺序播放和倒放
- repeatCount:重复播放次数
- fillAfter:播放完view是否保持动画最后状态
- interpolator:动画插值器,动画播放速度插值控制,默认为加速减速插值器
- shareInterpolator:set中的动画是否公用一个插值器
注意事项
- view动画中各个坐标都是以view初始位置为原点的相对值;而透明度是以1为完全不透明,到0为完全透明;缩放则是以1为初始大小,按比例缩放,大于1则为放大,小于1即为缩小。
- View动画是对View的影像做动画,并没有真正改变View的属性,因此在View位置发生改变时,我们点击改变位置后的View并不能触发点击事件,而原位置可以。
动画监听
有时,我们可能会需要在动画完成或是动画开始时执行某些操作,这时,我们就需要对动画设置监听,使用setAnimationListener方法,我们能够监听动画的开始、结束和重复,然后执行某些操作,如下所示:
animation.setAnimationListener(object : Animation.AnimationListener{
override fun onAnimationRepeat(p0: Animation?) {
Log.e("test_bug", "动画重复")
}
override fun onAnimationEnd(p0: Animation?) {
Log.e("test_bug", "动画结束")
}
override fun onAnimationStart(p0: Animation?) {
Log.e("test_bug", "动画开始")
}
})
属性动画
属性动画时API11新加入的特性,View动画只能实现平移、旋转、缩放和透明度变化四类动画,而且只能作用于View对象,而属性动画可以定义任意属性如颜色等的变化,从而实现出各种动画效果,而且对作用对象进行了扩展,可以对任意对象做动画。其主要使用ValueAnimator、ObjectAnimator以及AnimatorSet来实现相关动画效果。
基本使用
ValueAnimator
属性动画的本质是利用根据不同的插值器不断对值进行变化,进而改变对象属性来实现动画效果的。而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。事实上,这就类似于我们开启一个子线程,然后不断计算一个值并根据这个值来改变对象属性。
ValueAnimator的使用步骤大致可以分为以下几步:
- 调用ValueAnimator的ofArgb、ofFloat、ofInt和ofObject等方法获取ValueAnimator实例
- 调用setDuration方法设置动画时长,然后根据需要设置动画插值器、估值器、重复模式和重复次数等
- 调用实例的addUpdateListener添加AnimatorUpdateListener监听器,在该监听器中可以getAnimatedValue获得ValueAnimator计算出来的值,将值应用到指定对象上。比如:我将值设置为某个view对象的透明度,那么就可以出现透明度变化动画。
- 根据需要可以addListener监听动画的开启、结束、重复等阶段
- 调用实例的start方法启动动画。
下面我们来写一个简单的例子,我们通过ValueAnimator来实现一个简单的缩放动画,如下:
val animator = ValueAnimator.ofFloat(1f,0f,1f)
animator.duration = 1000
animator.addUpdateListener {
view1.scaleX = it.animatedValue as Float
Log.e("test_bug", "${it.animatedValue}")
}
animator.start()
输出日志如下图:
从上述代码和log日志来看,我们就可以发现,ValueAnimator实际上就是根据初始值和结束值,生成两者之间的中间值,然后根据中间值来改变view的属性进而实现动画效果,如上例,ValueAnimator为我们生成了1f-0f-1f之间的值,然后我们在AnimatorUpdateListener监听器中获取到中间值,然后改变view的scaleX属性即水平缩放比,这样就实现了一个水平缩放的动画效果。如下图:
ObjectAnimator
ObjectAnimator是ValueAnimator的一个子类,与ValueAnimator相比,ObjectAnimator更加简单易用,它能够直接操作任意对象的任意属性(具有set方法的属性),其使用方法和ValueAnimator基本相同,不过ObjectAnimator的ofFloat等方法ofXX方法有所不同,如下:
//第一个参数为动画目标对象,第二个参数为对象属性名(动画效果改变的属性),
val animator = ObjectAnimator.ofFloat(view1, "scaleX", 1f, 0f, 1f)
ObjectAnimator在获取实例时便指定了动画目标对象和属性名,其会自动改变目标对象的目标属性值,而不需要好像ValueAnimator一样需要通过监听器来手动控制,因此其使用更加简单,如上例利用ObjectAnimator来创建一个缩放动画,如下:
val animator = ObjectAnimator.ofFloat(view1, "scaleX", 1f, 0f, 1f)
animator.duration = 1000
animator.start()
实现效果与ValueAnimator的实现效果完全相同。
值得注意的是,ObjectAnimator的第二个参数理论上是可以传递任意属性名的,但是,ObjectAnimator内容实际上是通过set方法和get方法来实现的,因此其第二个参数需要目标对象中能够找到对应的set和get方法,否则无效。而只要存在对应的set和get方法,我们就能够设置任意值。如下:
val animator = ObjectAnimator.ofFloat(view1, "scaleX", 1f, 0f, 1f)
//垂直方向缩放
ObjectAnimator.ofFloat(view1, "scaleY", 1f, 0f, 1f)
//旋转
ObjectAnimator.ofFloat(view1, "rotation", 0f, 45f, 0f)
//x方向平移
ObjectAnimator.ofFloat(view1, "translationX", 0f, 100f)
//y轴方向平移
ObjectAnimator.ofFloat(view1, "translationY", 0f, 100f)
//透明度
ObjectAnimator.ofFloat(view1, "alpha", 1f, 0f, 1f)
AnimatorSet
与view方法相同,属性动画也提供了AnimatorSet来创建动画集合,但是AnimatorSet更加强大,它提供的Builder类还能够控制不同动画按不同顺序来播放,其主要方法如下:
- after:将现有动画插入到传入的动画之后执行
- before:将现有动画插入到传入的动画之前执行
- with: 将现有动画和传入的动画同时执行
然后,再设置动画时长,重复模式,监听器等,如下所示:
//x缩放
val animator1 = ObjectAnimator.ofFloat(view1, "scaleX", 1f, 0f, 1f)
//y缩放
val animator2 = ObjectAnimator.ofFloat(view1, "scaleY", 1f, 0f, 1f)
//旋转
val animator3 = ObjectAnimator.ofFloat(view1, "rotation", 0f, 45f, 0f)
//x方向平移
val animator4 = ObjectAnimator.ofFloat(view1, "translationX", 0f, 100f)
//透明度
val animator5 = ObjectAnimator.ofFloat(view1, "alpha", 1f, 0f, 1f)
val animatorSet = AnimatorSet()
animatorSet.play(animator1)
.with(animator2)
.after(animator3)
.before(animator4)
.with(animator5)
animatorSet.duration = 3000
animatorSet.start()
实际动画效果如下:
监听器
对于属性动画的状态变化监听,我们可以使用addListener来添加一个Animator.AnimatorListener监听器,来监听动画开始、结束、退出和重复状态,如下:
animatorSet.addListener(object : Animator.AnimatorListener{
override fun onAnimationRepeat(p0: Animator?) {
TODO("Not yet implemented")
}
override fun onAnimationEnd(p0: Animator?) {
TODO("Not yet implemented")
}
override fun onAnimationCancel(p0: Animator?) {
TODO("Not yet implemented")
}
override fun onAnimationStart(p0: Animator?) {
TODO("Not yet implemented")
}
})
属性动画的XML
和view动画相同,属性动画也提供了对应的XML实现方式,属性动画中的AnimatorSet、ValueAnimator和ObjectAnimator分别对应了标签<animator><objectAnimator><set> ,使用XML实现属性动画相对麻烦,但是其复用性很强。相关属性如下:
- android:ordering:指定动画的播放顺序:sequentially(顺序执行),together(同时执行)
- android:duration:动画的持续时间
- android:propertyName=“x”:指定目标属性加载动画的那个对象里需要 定义getx和setx的方法,objectAnimator就是通过这里来修改对象里的值的!
- android:valueFrom=“1” :动画起始的初始值
- android:valueTo=“0” :动画结束的最终值
- android:valueType=“floatType”:变化值的数据类型
如下:
<animator xmlns:android="http://schemas.android.com/apk/res/android"
android:valueFrom="0"
android:valueTo="100"
android:valueType="intType"/>
然后,在代码中获取animator对象,如下:
val animator = AnimatorInflater.loadAnimator(this, R.animator.anim_file)
animator.setTarget(view1)//设置目标对象
animator.start()
插值器和估值器
插值器Interpolator
简介
我们知道在属性动画中,ValueAnimator会自动生成初始值和结束值中间的过渡值,那么这个中间值是怎么变化的呢?假设我们使用ofInt生成一个从0到10的ValueAnimator对象,动画时长为2秒,那么其中间值是怎么变化的呢?是随时间成正比匀速变化吗?还是如何呢?也就是说从0-100之间,中间值v随时间t变化的函数v = f(t)是怎么确定的呢?其实这就是由插值器和估值器来控制的,默认情况下先加速后减速的一个插值器AccelerateDecelerateInterpolator。
如下代码:
val a = ValueAnimator.ofInt(0,10)
a.addUpdateListener {
Log.e("test_bug", "${it.animatedValue}")
}
a.duration = 400
a.start()
从上图中,我们可以大致看到一个先加速后减速的过程,当然,由于这是整数,所以并不精确,我们从之前的动画效果其实也能够看出来,属性值的变化有一个先加速后减速的效果,而这就是插值器的效果了。在我们制作动画效果的时候,插值器往往可能对动画的变化效果产生完全不懂的效果,比如我们需要制作一个*落体的动画,这时我们需要改变view的y坐标即可,但是这样做出来的效果很不自然,因为此时view会先加速后减速,而*落体的物体,我们知道是进行匀加速运动的,因此我们需要将其插值器改为AccelerateInterpolator即可,如下:
val animator = ObjectAnimator.ofFloat(view1, "translationX", 0f, 100f)
animator.interpolator = AccelerateInterpolator(2f)
animator.start()
系统提供的插值器
在Android中已经默认为我们提供了一系列的插值器供我们使用,具体如下:
自定义插值器
尽管在日常开发中,系统提供的插值器一般都足够满足需求了,但是我们还是应该简单了解下自定义插值器的方法。
自定义插值器需要继承TimeInterpolator,实现getInterpolation方法,getInterpolation只有一个input的float型参数,需要返回一个float,input是一个在duration中从0f-1f匀速变化的float参数,自定义插值器所需要做的就是根据input计算中间值,其最简单的就是直接返回input,即匀速运动插值器LinearInterpolator。我们可以对照默认的AccelerateDecelerateInterpolator写出一个先减速后加速的插值器,如下:
class TestInterpolator : TimeInterpolator{
override fun getInterpolation(input: Float): Float {
return if (input <= 0.5) {
sin(Math.PI * input).toFloat() / 2
} else {
(2 - sin(Math.PI * input)).toFloat() / 2
}
}
}
具体效果如下:
我们可以发现,实际上自定义插值器的实现方式很简单,主要需要运用到数学知识,来对插值进行计算。
估值器Evaluator
简介
估值器实际上就是根据插值计算出中间值的函数,我们知道插值器只是提供了一个t的函数,但是怎么由t的函数值f(t)转换为过渡值这就是由估值器来控制的了。其实Android中也同样内置了IntEvaluator、FloatEvaluator和ArgbEvaluator等估值器,它们均继承自TypeEvaluator,实际上在开发中,我们一般很少用到估值器,但是我们知道属性动画是可以正对任意对象的而且提供了,ofObject方法,那么object是怎么过渡的呢?这就需要我们自定义Evaluator了。
自定义Evaluator
自定义Evaluator只需要继承TypeEvaluator并实现evaluate方法,evaluate方法存在3个参数,具体为:
- fraction:动画的完成度,我们根据他来计算动画的值应该是多少(插值)
- startValue:动画的起始值
- endValue:动画的结束值
至此,自定义Evaluator的实现方法就很明了了,我们需要实现evaluate,根据fraction以及startValue和endValue来计算并返回当前的值。假如我们存在一个坐标对象Point,包含x和y两个属性,那么其估值器可以如下:
data class Point(
val x : Float,
val y : Float
)
class PointEvaluator : TypeEvaluator<Point>{
override fun evaluate(fraction: Float, startValue: Point?, endValue: Point?): Point {
if (startValue==null || endValue==null) return Point(0f,0f)
val x = startValue.x + fraction*(endValue.x - startValue.x)
val y = startValue.y + fraction*(endValue.y - startValue.y)
return Point(x, y)
}
}
使用插值器,如下:
val animator = ValueAnimator.ofObject(PointEvaluator(), Point(0f,0f), Point(10f,10f))
animator.addUpdateListener {
Log.e("test_bug", "${it.animatedValue}")
}
animator.duration = 1000
animator.start()
日志输出如下:
总结
动画在我们实际开发中也是一个常见的操作,为了提高视觉效果,提升用户体验,在实际开发中,尤其是一些比较优秀的APP中,往往存在很多动画效果,因此掌握动画的基本使用是非常重要的,一般而言,通过属性动画,我们能够完成相当一部分的动画效果,但是对于一些更加复杂的动画而言,我们可能就需要做一些自定义操作,比如自定义一些插值器等,而且在自定义动画的过程中往往涉及到一些矩阵变化的效果,这就需要我们对线性代数等一些数学知识也有所掌握了。同时,在使用动画的时候,我们应该时刻注意释放内存,在退出和销毁页面时释放动画,否则很容易出现一些内存泄漏等问题。
上一篇: [未测试]Chrome安装插件 ARC Welder 运行Android
下一篇: Android Emulator Process finished with exit code -1073741515 (0xC0000135)
推荐阅读
-
pygame学习笔记(2):画点的三种方法和动画实例
-
Css3 笔记 动画 和定位属性
-
Android动画开发常见属性解析
-
黑马Android76期学习笔记01基础--day07--广播,有、无序广播、特殊广播接受者、样式和主题,this与context的区别、普通对话框,进度条对话框、帧动画
-
Android开发触摸touch事件(补间动画和自定义view使用方法)
-
用Android属性动画实现和演示迪士尼动画基本原则
-
Android开发中属性动画(ObjectAnimator)中 插值器(Time Interpolator )详解
-
Android动画学习笔记之补间动画
-
Android开发学习笔记 Gallery和GridView浅析
-
从一个新的角度开始学习Android属性动画