Android-动画归纳

各种各样的动画快速归纳下

参考:Android 自定义控件三部曲中动画篇章,以及动画总结,两篇上面详细的学习

视图动画

早期android上动画是通过视图动画即Animation这个类以及超类来实现控制,通过控制View的属性,只是控制而已,并没有改变View的属性,所以我们能看到动画的效果,但是view本身的坐标以及各种属性是没有改变的,所以比如点击还是在原来的位置。

详细学习查询跳转启舰的视图动画博客

Animation的xml基础使用

可供控制的属性有很多,通过在xml文件任意的view中就能看到,其实和差不多对应了自定义控件里面的各种操作了。

  • alpha 透明度
  • scale 缩放
  • translate 平移
  • rotate 旋转

定义xml文件

在资源文件夹建立anim的文件夹,在里面定义各种动画的xml文件,一共有这么几个标签

其中scale、alpha、rotate、translate都可以单独做根标签表示一个动画,使用set表示动画集合,当然set里面也是可以嵌套set的

1
2
3
4
5
6
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale/>
<alpha/>
<rotate/>
<translate/>
</set>

每个标签的属性就不解释了,大概了解就行了,记不住的

首先定义一个scale缩放的,缩放就是x,y从哪缩放到哪,根据那个坐标来缩放,以及时间等属性。

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXScale="0"
android:toXScale="1.0"
android:fromYScale="0"
android:toYScale="1.0"
android:pivotX="0"
android:pivotY="0"
android:duration="400">
</scale>

开启动画

直接通过工具方法加载动画xml文件,目标view开启动画就行了

1
2
3
4
val scaleAnimation = AnimationUtils.loadAnimation(this, R.anim.scale)
btn_start.setOnClickListener {
tv.startAnimation(scaleAnimation)//开启
}

是不是特简单,使用是很简单,具体其它的属性就要具体使用了,这个具体看应用场景。其它的几个动画依次类推

组合使用

有时候需要多个属性效果变化结合起来,这时候就需要用到set标签,将动画写在set标签里面,调用还是一样的

使用插值器

动画我们定义了起始值和终值,但是其中是怎么变化并没规定,而插值器就是用来控制从起始值到终值是怎么变化的。比如匀速变化,加速变化等等。通过在动画标签加入android:interpolator属性

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator"
android:fromXScale="0"
android:toXScale="1.0"
android:fromYScale="0"
android:toYScale="1.0"
android:pivotX="0"
android:pivotY="0"
android:duration="1000">
</scale>

一些常用的插值器在@android:anim/目录下都可找到

代码使用Animation

本质上xml文件还是被解析成的Animation的子类,然后再使用,当然也可直接通过实例化不同Animation进行动画操作

  • scale对应ScaleAnimation
  • alpha对应AlphaAnimation
  • rotate对应RotateAnimation
  • translate 对应TranslateAnimation
  • set 对应AnimationSet

用法的那就是通过代码去实例化animation对象即可

1
2
3
4
5
6
7
8
//构建
val translateAnimation = TranslateAnimation(0f,200f, 0f,300f)
translateAnimation.duration = 1000
translateAnimation.fillAfter = true

btn_start_by_code.setOnClickListener {
tv.startAnimation(translateAnimation)//开启
}

其它的也是依次类推,除了基本的控件动画,还可以作用于activity和fragment的过场动画,由于视图动画很遥远了,“只能做表面上的效果”,优先考虑使用属性动画。

帧动画

帧动画好理解,我们看的电影,以及玩的游戏,都会有帧的概念,帧动画通过播放不同drawable实现动画效果

AnimationDrawable它本质上是drawable

定义xml

在drawable下定义

一个item表示一个帧

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:drawable/ic_media_pause" android:duration="1000"/>
<item android:drawable="@android:drawable/ic_media_play" android:duration="1000"/>
</animation-list>

使用

ImageView或其它中引用它,然后获得对象

1
(imageView.drawable as AnimationDrawable).start()//开启

就这么简单

属性动画

属性动画是在视图动画后面出现的,为了弥补视图动画的“表面功夫”,属性动画是通过控制view的属性来完成动画的。

基础用法之ValueAnimator

看名字也直到ValueAnimator传入值进行变化计算,然后我们需要在监听中获得值来设置,从而生成动画效果

一个栗子

1
2
3
4
5
6
7
8
9
10
11
val animator = ValueAnimator.ofArgb(Color.RED, Color.CYAN)//实例
animator.duration = 1000//设置时长
animator.repeatCount = 2//重复次数
animator.repeatMode = REVERSE//重复模式
animator.interpolator = LinearInterpolator()//插值器
animator.addUpdateListener {//添加监听
btn.setBackgroundColor(it.animatedValue as Int)//获取动画值进行更新view
}
btn_start.setOnClickListener {
animator.start()//开启动画
}

获得实例方法

1
2
3
4
5
public static ValueAnimator ofInt(int... values);//从int值构建
public static ValueAnimator ofArgb(int... values);//从arg的int值构建
public static ValueAnimator ofFloat(float... values);//从float构建
public static ValueAnimator ofPropertyValuesHolder(PropertyValuesHolder... values);//PropertyValuesHolder构建
public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values);//自定义取值器构建

有了这些值得变化,就能够实现简单的动画。但是呢我们动画效果的值可能不是int,float,有可能是抽象的数据类型,这个时候就要用下面的自定义求值器了(Evaluator)

插值器和求值器(Evaluator)

插值器讲过了是用来设定动画过程中值是如何变化的

求值器则是根据从插值器中得到的瞬时值,转化为我们所要包装的值。比如ValueAnimator.ofArgb其实就是使用了ArgbEvaluator来计算最终值

自定义插值器

input 输入进度,返回输出进度

1
2
3
4
5
class MyInterpolater: BaseInterpolator(){
override fun getInterpolation(input: Float): Float {
return input
}
}

求值器

求值器根据插值器中的进度,计算出真正的进度值

比如这个是用于求字符的求值器

1
2
3
4
5
6
class MyEvaluator: TypeEvaluator<Char> {
override fun evaluate(fraction: Float, startValue: Char?, endValue: Char?): Char {
val a : Int = ((endValue!!.toInt() - startValue!!.toInt()) * fraction).toInt() + startValue.toInt()
return a.toChar()
}
}

ObjectAnimator运用

对于上面的ValueAnimator中得出动画值,通过监听来设置属性,从而完成动画效果,而ObjectAnimator则就是不需要你手动设置监听改变属性

一个栗子

通过直接传入目标,以及动画作用属性完成animator构建,但是这中创键方式需要满足,能够找到对应的set方法,即参数签名类型,方法名相同。才会起作用

1
2
3
4
5
6
7
8
9
 //ObjectAnimator
val objAnimator = ObjectAnimator.ofFloat(btn, "rotation", 0f, 180f)//构建
animator.duration = 1000//设置时长
animator.repeatCount = 2//重复次数
animator.repeatMode = REVERSE//重复模式
animator.interpolator = LinearInterpolator()//插值器
btn_start_by_object.setOnClickListener {
objAnimator.start()//开启
}

其它属性

总之能够找到对应的set方法就行

1
2
3
4
5
6
7
8
rotation //绕z轴
rotationX //绕x轴
rotationY //绕y轴
scaleX //水平缩放
scaleY //竖直缩放
translateX //水平平移
translateY //竖直平移
alpha //透密度

原理

其实本质上和VauleAnimator差不多,ObjectAnimator会根据传入的属性名去反射查找对应的set方法,从而进行动画。

作用于自定义属性

既然它是通过反射去查找对应的set方法,那么只要我们的自定义view中暴露了set的属性方法,就可以使用ObjectAnimator进行动画操作。当然如果需要get的方法,还是要实现的一下的。

PropertyValuesHolder和KeyFrame

PropertyValuesHolder

PropertyValuesHolder直译也直到属性持有,之前我们通过ObjectAnimator是设置目标属性,以及变化,而一个PropertyValueHolder就是一个属性的变化持有,而ObjectAnimator可以从多个PropertyValueHolder构建,也就是可以有多个属性同时变化来完成动画。

下面的栗子就表现为,同时旋转和颜色改变。

1
2
3
4
5
6
7
val rotationHolder = PropertyValuesHolder.ofFloat("rotation", 0f, 180f)
val argbHolder = PropertyValuesHolder.ofInt("backgroundColor", Color.CYAN, Color.RED, Color.BLUE)
val holderAnimator = ObjectAnimator.ofPropertyValuesHolder(btn_start_by_holder, rotationHolder, argbHolder)
holderAnimator.duration = 2000
btn_start_by_holder.setOnClickListener {
holderAnimator.start()
}

KeyFrame

这个就是关键帧的意思,给定关键帧的属性,让它自己计算动画变化。然后也可以设置不同的插值器,这么一看,其实我们之前的动画就相当于给了起始和末尾两个关键帧。而这里可以更加自由的设置变化。

1
2
3
4
5
6
7
8
9
10
11
val keyframeA = Keyframe.ofFloat(0.1f, 30f)//关键帧A
val keyframeB = Keyframe.ofFloat(0.5f, -30f)//关键帧B
val keyframeC = Keyframe.ofFloat(1f, 90f)//关键帧C
//使用关键帧构建属性holder
val translateHolder = PropertyValuesHolder.ofKeyframe("rotation", keyframeA, keyframeB, keyframeC)
//构建动画实例
val keyAnimator = ObjectAnimator.ofPropertyValuesHolder(btn_start_by_keyframe,translateHolder)
keyAnimator.duration = 2000
btn_start_by_keyframe.setOnClickListener {
keyAnimator.start()//开启
}

这里没有介绍PropertyValuesHolder.ofObject因为类似之前的,也是要去自定义求值器。然后关键帧之间是可以设置插值器的。

组合动画

组合动画?之前我们同时变化多个属性不就相当了嘛。no,no。那只是一个Animator的实例在起作用。而这里的组合动画就是AnimatorSet是可以将多个Animator的动画进行一同播放或者按照某种顺序播放。

顺序播放

调用playSequentially传入animator集合即可,animatorSet只是负责在一个动画完成后顺序开另一个动画,如果一个动画始终没有完成,那么下一个也始终不会执行

1
2
3
4
5
6
7
//AnimatorSet
val animatorSet = AnimatorSet()
animatorSet.playSequentially(animator, holderAnimator, keyAnimator)
animatorSet.duration = 2000
btn_start_animator_set.setOnClickListener {
animatorSet.start()
}

同时播放

调用playTogether传入animator集合即可,animatorSet负责同时运行多个动画

1
2
3
4
5
6
7
//AnimatorSet
val animatorSet = AnimatorSet()
animatorSet.playTogether(animator, holderAnimator, keyAnimator)
animatorSet.duration = 2000
btn_start_animator_set.setOnClickListener {
animatorSet.start()
}

自由播放

通过Animator.Builder来自由的构建动画顺序

1
2
3
4
5
6
val animatorSetByBuild = AnimatorSet()
animatorSetByBuild
.play(keyAnimator)//通过play构建Builder
.with(vlaueAnimator)//
.before(holderAnimator)//
.after(objAnimator)

其中这几个方法代表

  • set.play(A),第一个播放且返回builder
  • with(B),表示和一同播放B和A一同播放
  • before(C),表示C在A,B的后面,符合英文语义,A before C
  • after(D),表示D在A的前面,A after D

这些构建都要以第一play的动画为参照点的

1
play(A).with(B).after(C).before(D)//顺序是 C A(B) D

加入连续调用多个before,和after呢

比如

1
play(A).before(B).before(C).after(D).after(E)//顺序D(E)AB(C)

其实还是以第一个play的为基准,多个重复的after或者before,会被视为一同播放。

覆盖动画设置

在AnimatorSet设置某些东西,是会覆盖每个animator的

1
2
3
4
5
6
//设置单次动画时长
public AnimatorSet setDuration(long duration);
//设置插值器
public void setInterpolator(TimeInterpolator interpolator);
//设置目标
public void setTarget(Object target);

设置延时不会覆盖,因为这个的意义是animatorSet的延时

1
animatorSet.startDelay = 2000

其实就是延迟2秒后,开始这个动画集

总结下,AnimatorSet通过play开头的方法调用第一个动画都是同一时间触发,顺序操作要看playSequentiallyafter以及before来决定的,AnimatorSet的延时只是自身的延时,其它的属性比如,单个时长,插值器,目标,是会覆盖动画的。

Transation

Transation翻译为过渡,转场,意思从一个场景过渡到另一个场景,可以运用于一个activity间,fragment间,乃至更小的地方。两种变化之间需要过渡

首先分析下这个作用流程:

在这个板块里面有三个概念:

  • Transition 过渡,代表了具体的过渡动作
  • TransitionMananger 过渡管理器,代表了执行过渡动作的管理者
  • Scene 场景 ,抽象的的UI状态的表达

也就是说从一个Scene过渡到另一个Scene就完成一次场景过渡

Scene

场景这个挺抽象的,我的理解就是某一状态下的UI信息包装,比如在某一个LinearLayout为根的场景下,有一个图片坐标在某个位置,这就代表了一个场景。所以我们的场景最根本的是有一个根容器,表示这个舞台。

首先看下Scene的构造方法

1
2
3
4
val sceneRoot: ViewGroup
val sonView: View
val scene = Scene(sceneRoot, sonView)//手动搭建
Scene.getSceneForLayout(sceneRoot,resId,context)//从layout中注入形成

scene代表场景,sceneRoot代表整个场景的舞台,也就是要给ViewGroup,然后sonView里面的一个演员,舞台上包括了演员在内的只要是属性等状态变化的,就是一个瞬时的场景。我们只需要知道这个瞬时的场景就Ok了。

TransitionManager的方法

说白了过渡控制器,通过动画完成到达某个场景,往深层次看就是,从某一场景属性变化成另一个场景属性,即完成一次场景过渡。

1
2
3
4
5
TransitionManager.go(scene, transition)//以transition过渡,进入一个场景
TransitionManager.go(scene)//以默认过渡,进入一个场景
TransitionManager.beginDelayedTransition(sceneRoot)//当前舞台以默认方式开始过渡
TransitionManager.beginDelayedTransition(sceneRoot, transition)//当前舞台开始以某种方式过渡
TransitionManager.endTransitions(sceneRoot)//终止所有的过渡

go还好理解直接过渡就完事了,beginDelayedTransition的话是通知准备开始过渡了,但是场景不明确,也就是说接下来的以当前舞台的任何变化,都会以某种过渡完成。

go方式进行的切换是会将原来场景舞台上所有东西撤下来,过渡到以新场景的内容。

目前我还只是写出测试效果,我没找到具体可以实现的应用,从api来看,大概就是在添加view,以及某容器属性变化时可以使用,其transition内部实现也还是通过属性动画去完成的,所以实现再说。

除了下面的转场应用以外,待补充。。。

转场动画运用

到了这里的话就可以看到很常见的运用了

activity转场动画

activity之间的转换过渡是会有动画效果的,但是我们也可以自己定义

老式转场

android5.0之前的话,使用视图动画来做转场动画的。比较古老了,但是还是可以使用

简单手动使用

首先需要定义两个animation的xml动画,代表了

然后只需要在跳转后调用overridePendingTransition传入的动画效果就行了,进就是新的activity进入的动画,出就是本身离开的动画

xml示范:right_in

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%" android:toXDelta="0" android:duration="1000"/>
</set>

调用

1
2
3
4
btn_to_b.setOnClickListener {
go(AnimationBActivity::class.java)//startActivity
overridePendingTransition(R.anim.activity_rigth_in, R.anim.activity_left_out)
}

同理而言在finish使用后面调用也是需要手动重写动画的,而且还得重写动画,不能用之前的,当然用了会很奇怪,所以重写下。

1
2
3
4
btn_finish.setOnClickListener {
finish()
overridePendingTransition(R.anim.activity_left_in, R.anim.activity_rigth_out)
}

在这种情况我们使用系统的返回按钮还是默认的动画效果的,所以如果要改变的话,需要手动重写onBackPress或者直接重写finish也行

使用主题配置动画

这样就相当于直接修改默认的动画效果了。

首先在定义四种动画,分别代表了进入时的两个动画,和退出时两个动画。

1
2
3
4
5
6
<style name="default_animation" mce_bogus="1" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/activity_right_in</item>
<item name="android:activityOpenExitAnimation">@anim/activity_left_out</item>
<item name="android:activityCloseEnterAnimation">@anim/activity_left_in</item>
<item name="android:activityCloseExitAnimation">@anim/activity_right_out</item>
</style>

启用主题配置

在AppTheme主题种,配置android:windowAnimationStyle这一项

1
2
3
4
5
6
7
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowAnimationStyle">@style/default_animation</item>
</style>

这样做的话,嗯~,就相当于改变了默认的转场效果,自然back效果也是被应用了,如果代码中没有重写,则就会表现。

新式转场

在5.0之后由于推广MD风格的设计,自然之前的转场效果就不好看了,使用Transition来完成转场。

普通用法

启动方

1
2
3
4
5
6
//启动方
btn_to_b_new.setOnClickListener {
startActivity(
Intent(this@AActivity, BActivity::class.java),
ActivityOptions.makeSceneTransitionAnimation(this@AActivity).toBundle())//这里是重点
}

被启动方

1
2
3
4
5
//被启动方
val explode = Fade()//转场效果
explode.duration = 2000
window.enterTransition = explode//设置入场效果
window.exitTransition = explode//设置出场效果

动画种类

默认提供了三种

1
2
3
Expolde()//炸开
Slide()//平滑
Fade()//渐变

高级用法-共享元素

这个效果非常好用

转场的两个Activity中的控件进行关联,转场的时候,关联的控件会非常自然的过度

首先要设置关联共享元素,两个关联的控件设置同样的transitionName

1
android:transitionName="one"

启动

1
2
//启动
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, holder.fruitImage, "one").toBundle());

关联多个

准备多个Pair即可,后面的动画可随便选择,也或者

1
2
startActivity(Intent(this, Main2Activity.class),
ActivityOptions.makeSceneTransitionAnimation(this, Pair.create(((View) iv1),"myiv"), Pair.create(((View) textView),"mytv")).toBundle());

使用xml

之前的转场可以通过xml文件设置,新式转场自然也是可以设置全局默认效果的。

1
2
3
4
5
<item name="android:windowContentTransitions">true</item>
<item name="android:windowEnterTransition">@android:transition/fade</item>
<item name="android:windowExitTransition">@android:transition/fade</item>
<item name="android:windowReenterTransition">@android:transition/fade</item>
<item name="android:windowReturnTransition">@android:transition/fade</item>

其实这样设置和代码设置没有多大区别

1
2
3
4
5
6
val explode = Fade()
explode.duration = 2000
window.enterTransition = explode
window.exitTransition = explode
window.reenterTransition = explode
window.returnTransition = explode

对了,如果新式转场和旧式转场都设置了的话,我测试效果是旧式转场把新的覆盖了。

自定义效果

自定义Transition

待补充。。

Fragment转场动画

fragment也是和activity的转场时差不多的思路,包括了旧式animation转场,Transition过渡,以及共享元素

animation

使用比较简单,首先准备好animation的xml,然后在代码运用

1
2
3
4
5
6
supportFragmentManager
.beginTransaction()
.setCustomAnimations(R.anim.slide_up, 0)
.add(R.id.fragment_container, bFragment)
.addToBackStack(null)
.commit()

给本次事务加上动画

1
2
3
4
//enter 进入动画 exit退出动画,返回栈退出无效
FragmentTransaction setCustomAnimations (int enter, int exit);
//enter 进入动画,exit退出动画,popEnter返回栈进入动画 popExit从返回栈退出动画
FragmentTransaction setCustomAnimations (int enter, int exit, int popEnter, int popExit);

这种动画是作用在事务上的,所以,我们可以在hide和show之前设置动画,也会出现动画效果。

1
2
3
4
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setCustomAnimations(0, R.anim.fragment_slide_down);
ft.hide(musicPlayFragment);
ft.commitAllowingStateLoss();

当然也可以在代码中实现,需要重写

1
2
3
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation {
return super.onCreateAnimation(transit, enter, nextAnim)
}

Transition

这个就简单了

1
2
3
4
5
6
7
8
9
bFragment.enterTransition = Slide()//设置进入的过渡
bFragment.exitTransition = Slide()//退出的过渡
bFragment.sharedElementEnterTransition = Slide()//共享元素的过渡
supportFragmentManager
.beginTransaction()
.add(R.id.fragment_container, bFragment)
.addSharedElement(view, tag)//设置共享元素
.addToBackStack(null)
.commit()

通过这种方式设定的,比较彻底,过渡就绑定在fragment上了,无论是add,replace,以及show,hide,都会呈现设置的过渡形式

ViewPager切换动画

ViewPager的切换的话其实掌握三个点就行了。

第一,开启子控件允许超出父控件

通过这个属性:android:clipChildren="false",一般来说不论子控件怎么大,都是会被限定在父布局中,通过这个属性,子控件可以突破这个限制。一般而言viewpager静止的时候,布局中只会有一个fragment显示出来,而我们使用了这个属性,多个fragment就会同时显示出来,突破了viewpager布局的限制。而我们要做的许多效果基本上都需要静止时或多或少能够看到其它多个fragment。

第二、加载多张

有了第一点突破布局限制,那么我们viewpager是可以设置屏幕加载多少张的。

第三、自定义PageTransformer

这个就是我们的具体效果实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ScaleTransformer: ViewPager.PageTransformer{
val MIN_SCALE = 0.8f
val MIN_ALPHA = 0.5f
override fun transformPage(view: View, position: Float) {
when {
position <= -1 || position >= 1 ->{
view.alpha = 0.8f
view.scaleX = 0.8f
view.scaleY = 0.8f
}

position <= 0 -> {
val scale = MIN_SCALE + (1+position) * (1-MIN_SCALE)
val alpha = MIN_ALPHA + (1+position) * (1-MIN_ALPHA)
view.scaleX = scale
view.scaleY = scale
view.alpha = alpha
}
position <= 1 -> {
val scale = MIN_SCALE + (1-position) * (1-MIN_SCALE)
val alpha = MIN_ALPHA + (1-position) * (1-MIN_SCALE)
view.scaleX = scale
view.scaleY = scale
view.alpha = alpha
}
}
}
}

实现这个PageTransformer我们需要实现transformPage方法即可,这里面给出参数只有viewposition,view代表一个页面,而position代表这个页面所处的位置。我们以每个页面的中中心位置为参考点,当页面位于屏幕中心的时候,页面参考点和屏幕中心横坐标重合,这个时候position就是0,而当这个页面滑向左边,直到刚好不见时,页面中心参考点和屏幕中心参考点横向距离就是view的宽度,这个时候的position就是-1。反之右边就是1,然后依次类推

1
-2..-1..0..1..2

意思每个page都有对应的positon位置,然后原本的情况的是依次排列。而我们可以在这个基础上给它加上基于position变化的缩放比,透明度,平移,旋转,这样的话就可以完成一个切换动画。基本都是基于瞬时值的变化。

1
2
3
4
5
        viewpager.adapter = SimpleAdapter(supportFragmentManager)
// viewpager.setPageTransformer(true, DepthTransformer())
// viewpager.setPageTransformer(true, ScaleTransformer())
viewpager.setPageTransformer(true, StackCardTransformer())//第一参数代表绘制顺序,true表示左边要后绘制,效果就是左边的在右边上层。
viewpager.offscreenPageLimit = 6 //page数

ViewGroup加入子view动画

首先将viewgroup的android:animateLayoutChanges="true"属性开启

1
2
3
4
5
6
7
8
val layoutTransition = LayoutTransition()//这个transition和之前那个并没有关联
val objectAnimator = ObjectAnimator.ofFloat(null, "rotation", 0f, 90f,0f)//设置动画
layoutTransition.setAnimator(LayoutTransition.APPEARING, objectAnimator)//将动画设置给哪个目标
container.layoutTransition = layoutTransition//设置到viewgroup
btn_add_btn.setOnClickListener {
val btn = Button(this)
container.addView(btn)
}

步骤就是以上几个,动画的目标有以下

1
2
3
4
5
APPEARING//本身item出现时。目标自身
DISAPPEARING//本身item消失时,目标自身
CHANGING//因layout改变(而不是因为add,remove造成),动画,默认是没开启的
CHANGE_APPEARING//别的item出现,目标自身
CHANGE_DISAPPEARING//别的item消失,目标自身

APPERARINGDISAPPERARING直接添加动画就行了。

CHANGE_APPEARINGCHANGE_DISAPPEARING需要通过PropetryHolder进行完成,因为它必须包含lefttop的变化。

同样可以在动画设置的其它比如方法

1
2
3
4
5
public void setDuration(long duration);//所有动画所需时间
public void setDuration(int transitionType, long duration);//针对type
public void setInterpolator(int transitionType, TimeInterpolator interpolator);//针对不同type的插值器
public void setStartDelay(int transitionType, long delay);//针对不同type的延时
public void setStagger(int transitionType, long duration);//针对不同type,每个item动画间隔

触摸反馈动画Ripple Effect

比如我们的原生按钮,哪个触摸反馈就是用了这个。

使用起来很简单

使用系统定义的

只需要设置前景或者背景即可

1
2
3
4
//有边界
?android:attr/selectableItemBackground
//无边界 (要求API21以上)
?android:attr/selectableItemBackgroundBorderless

设置,前景和背景都可以设置

1
2
3
4
5
6
7
8
9
10
<LinearLayout
android:clickable="true"
android:background="@color/colorPrimary"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="287dp" android:layout_height="196dp"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="16dp" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp" android:layout_marginTop="100dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintHorizontal_bias="0.46">
</LinearLayout>

这样的话,只要这个view可以点击,就会触发触摸反馈

自定义

自定义也很简单,本质上其实是一个drawable,所以在drawable文件夹定义一个xml即可

1
2
3
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/colorAccent">
</ripple>

ripple根标签必须要给出color属性,这个代表了波纹的颜色,当然最好是带透明度的颜色,不然就会盖住了。

ripple里面还可以添加标签item,这个的作用是设置边界,额,我感觉啊,有没有边界好像差不多额。了解就差不多了

补充下,这个边界还是有用的,我在模拟器上没看出不同来,换到手机就看出来了,有边界的ripple触摸反馈,不会无限扩展到整个view,无边界则会。

揭露动画

看起来的效果和ripple的差不多,就是一个以圆半径从a到b的变化。

使用起来比较简单,通过ViewAnimationUtils.createCircularReveal就可创键一个揭露animator

1
2
3
4
5
6
7
val show = ViewAnimationUtils.createCircularReveal(
frame_b,//目标view
x,//圆心横坐标
y,//圆心纵坐标
0f,//起始半径
radius.toFloat()//结束半径
)

这个动画的过程就是以某点到位圆心得半径变化得圆。通过在开始或者设置可见性,比如开从半径0到100时,动画开始设置可见性位可见,动画开始,看到得效果就是view逐渐以圆扩张可见。反之也可以隐藏。

下面是完整show和hide栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private fun launchReveal() {
val x = frame_b.width
val y = frame_b.height
val radius = Math.hypot(x.toDouble(), y.toDouble())
if (flag) {
//hide
val hide = ViewAnimationUtils.createCircularReveal(
frame_b,
x,
y,
radius.toFloat(),
0f
)
hide.duration = 2000
hide.run {
hide.duration = 2000
addListener(object : Animator.AnimatorListener{
override fun onAnimationRepeat(animation: Animator?) {
}

override fun onAnimationCancel(animation: Animator?) {
}

override fun onAnimationStart(animation: Animator?) {
}

override fun onAnimationEnd(animation: Animator?) {
frame_b.visibility = View.GONE
}

})
start()
}
flag = false
} else {
//show
val show = ViewAnimationUtils.createCircularReveal(
frame_b, x, y, 0f,
radius.toFloat()
)
show.duration = 2000
frame_b.visibility = View.VISIBLE
show.start()
flag = true
}
}

视图状态动画

视图状态动画:即在view状态发生改变时的动画,状态?何为状态,比如说:按下,获得焦点等等,这也就我们经常使用selector,用于对不同状态的drawable。同样,在状态发生改变时可以进行动画效果过渡。

使用也比较简单,在animtor动画目录下定义以selector的动画即可

下面定义了一个按压是,z轴增加有卡片效果,同时绕x轴小幅度晃动。不按压时则变回原来属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<set>
<objectAnimator
android:propertyName="translationZ"
android:valueTo="10dp"
android:valueType="floatType"
android:duration="100"/>
<objectAnimator
android:propertyName="rotationX"
android:duration="100"
android:valueTo="20"
android:valueFrom="-20"
android:valueType="floatType"/>
</set>
</item>

<item android:state_pressed="false">
<set>
<objectAnimator
android:propertyName="translationZ"
android:valueTo="0"
android:valueType="floatType"
android:duration="100"/>

<objectAnimator
android:propertyName="rotationX"
android:duration="100"
android:valueTo="0"
android:valueType="floatType"/>
</set>
</item>
</selector>

后面只需要给view设置属性即可

1
android:stateListAnimator="@animator/state_z_up"

或者在代码中

1
2
3
4
//加载动画
val stateLAnim = AnimatorInflater.loadStateListAnimator(this,R.animator.state_z_up)
//设置动画
b.stateListAnimator(stateLAnim)

矢量图动画

矢量图它不是一个固定的图片,而是根据xml里面的规则,进行绘制出来的,所以无论你的放大多少,然后它还是按照你给的区域进行绘制,所以不会模糊之类的。简单来理解,我感觉就和我们之前进行自定义view中进行图形路径等绘制一样,只不过它是把绘制动作写到了xml文件中。而这些矢量图和自定义view一样,拥有路径绘制,旋转,颜色等属性。而我们的矢量图动画就是在矢量图的基础上控制矢量图属性的变化来完成动画过渡。

简单来说有三个步骤

准备矢量图和动画

比如,画一个,在path标签中,pathData使用M L函数,分别代表了,moveToLineTo是不是和canvas中画图操作差不多。贝塞尔函数也是可以用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:name="g_rotation"
android:pivotX="12"
android:pivotY="12"
android:rotation="0">

<path android:name="check"
android:strokeAlpha="1.0"
android:pathData="M4,10 L9,16 L20,4"
android:strokeColor="@color/colorPrimary"
android:strokeLineJoin="round"
android:strokeLineCap="round"
android:strokeWidth="3"/>
</group>
</vector>

定义一个动画文件,我们控制的旋转属性,而旋转是在group标签的,所以上面用group包裹了path

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="360">
</objectAnimator>

定义含动画的矢量图(animated-vector):

设定源矢量图,以及设置动画匹配的目标

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/hook">
<target android:animation="@animator/rotation_round" android:name="g_rotation"/>
<target android:animation="@animator/alpha" android:name="check"/>
<target android:animation="@animator/color" android:name="check"/>
</animated-vector>

开启动画

1
2
3
4
iv_hook.setOnClickListener {
val drawable = iv_hook.drawable
(drawable as Animatable).start()
}

相关属性

1
2
trimPathEnd 截取末尾,这个也用来实现一条路径的从无到有
pathData

这样就可以通过操纵矢量图属性进行动画了。我们一般看到的MD设计中那些非常细腻的图标变化动画就是通过矢量图动画表达的。当然上面只是操纵了rotation属性,更加牛逼的是可以直接过渡变换pathData路径表述,只要它们路径表述结构相同,也是可以实现很巧妙的动画。不过能上面也只是粗略的介绍,后续需要深入,考虑使用优美的矢量动画库,学习学学习。

约束布局实现的关键帧动画

在约束布局中,开启transition,动态改变约束布局中子view属性,ConstraintSet 会按照动画过渡完成属性转变,具体参看下面链接

约束布局动画

动画库