带你走一波Android转场动画的相关事项

/   今日快讯   /

大家早上好,新年最好的问候就是报个平安。

受疫情影响,春节假期一延再延。虽然今天是春节假期后的首个法定工作日,但是相信大部分的朋友们依然没有正常返工。目前不同省市采取的是不同的返工政策,比如江苏、上海、广东等19个省都明确规定企业复工时间不得早于2月9日24时。但也有不少省份并没有宣布类似的政策,比如北京从今天开始就正式复工了。

我们公司在2月10号才会正式上班,虽然今天推送了年后的首篇技术文章,但接下来可能不会像往常一样每个工作日都推送,暂时会以不定期的形式推送文章,随着疫情的逐步缓解再慢慢再恢复到平日的推送频率。

对于今天已经恢复上班的读者朋友们,请一定要做好个人防护,出门必须佩戴口罩,尽量避免去人群密集区域,如果可能,请不要乘坐公共交通。一定要把个人的健康摆在第一的位置,我们共同挺过这个难关。

/   作者简介   /

本篇文章来自未扬帆的小船的投稿,详细的介绍了官方推荐的转场动画,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

未扬帆的小船的博客地址:

https://juejin.im/user/57d822c90bd1d000585cad57

/   简述   /

Transition 可以简单理解为一个过渡框架方便在开始场景到结束场景(不局限于 Activity 跟 Fragment 等页面跳转过程,页面中的控件的变化过程也是场景)设置转场动画(例如,淡入/淡出视图或更改视图尺寸)的一个API。在Andorid 4.4.2引入的 Transition 框架,Andorid 5.0以上的版本跳转过渡则建立在该功能上。

/   关键概念   /

有两个关键概念:场景 scene 跟转场 transition。

  • scene : 定义给定应用程序的UI。

  • transition:定义两个场景之间的动态变化。

当 scene 开始时,Transition 有两个主要职责:

  1. 在开始和结束的 scene 捕捉每个视图的状态。

  2. 创建一个 Animator 根据视图,将动画的差异从一个场景到另一个。

/   关键类TransitionManager   /

将 Scene 和 Transition 联系起来,提供了几个设置场景跟转场的设置方法。

/   Transition相关内容   /

系统有实现了部分的转场动画的类,自己根据需求去处理,我这里就简单演示一下里面的几个类,其它的大家自己去试试。

transition的创建

使用布局的方式:在res下创建transition目录,接着创建.xml文件

创建单一转场效果res/transition/slide_transition.xml。

<slide  xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="500"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:slideEdge="top" />

创建多转场res/transition/mulity_transition。

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="together">
    <explode
        android:duration="1000"
        android:interpolator="@android:interpolator/accelerate_decelerate" />
    <fade
        android:duration="1000"
        android:fadingMode="fade_in_out"
        android:interpolator="@android:interpolator/accelerate_decelerate" />
    <slide
        android:duration="500"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:slideEdge="top" />
</transitionSet>

载入.xml文件(多转场跟单一转场都是使用同一方法)。

val transition =TransitionInflater.from(this).inflateTransition(R.transition.fade_transition)

使用代码创建translation的方式

//------------------------------- 创建单一转场效果
val translation =  Slide().apply { 
       duration = 500
       interpolator = AccelerateDecelerateInterpolator()
       slideEdge = Gravity.TOP
}

//------------------------------- 创建多转场效果
val transitionSet = TransitionSet()
                transitionSet.addTransition(Fade())
                transitionSet.addTransition(Slide())
                transitionSet.setOrdering(ORDERING_TOGETHER)

使用&常用API

基本使用
//root_view是本布局中的最底层的布局,自己可以指定 但是要包含将要进行动画的控件
//单转场
TransitionManager.beginDelayedTransition(root_view, translation) 
toggleVisibility(view_text,view_blue, view1_red, view_yellow)

//多转场
TransitionManager.beginDelayedTransition(root_view, transitionSet) //多转场
toggleVisibility(view_text,view_blue, view1_red, view_yellow)
    /**
     * 四个有颜色的方块的隐藏跟显示
     */
private fun toggleVisibility(vararg views: View?) {
  for (view in views) {
   view!!.visibility =
      if (view!!.visibility == View.VISIBLE) View.INVISIBLE else View.VISIBLE
   }
}

效果图

这里你可以略清楚转场动画的用意,就是你指定两个场景 比如例子中的开始是View都显示,第二个场景是View都隐藏,设置的transitionSet或者translation就是用于中间变化的过程使用的动画。实际上也是里面使用了属性动画进行处理的。(下面自定义转场动画的时候会说到)

//点击按钮
R.id.btn_change_bounds -> {
  TransitionManager.beginDelayedTransition(root_view, ChangeBounds())
  var lp = view1_red.layoutParams
  if (lp.height == 500) {
    lp.height = 200
    } else {
    lp.height = 500
    }
  view1_red.layoutParams = lp
}
//红框剪切的
R.id.btn_change_clip_bounds -> {
  TransitionManager.beginDelayedTransition(root_view, ChangeClipBounds())
  val r = Rect(20, 20, 100, 100)
  if (r == view1_red.clipBounds) {
    view1_red.clipBounds = null
  } else {
    view1_red.clipBounds = r
  }
}
// 蓝色方块中的字内部滑动
R.id.btn_change_scroll -> {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    val t = ChangeScroll()
    TransitionManager.beginDelayedTransition(root_view, t)
  }
  if(view_text.scrollX == -50 && view_text.scrollY == -50){
    view_text.scrollTo(0,0)
  }else{
    view_text.scrollTo(-50,-50)
  }
}

translation.Targets

配置Transition可以给一些特殊目标的View或者去掉目标View指定Transitions.

增加动画目标addTarget(View target)

  • addTarget(int targetViewId)

  • addTarget(String targetName) : 与 TransitionManager.setTransitionName方法设定的标识符相对应。

  • addTarget(Class targetType) : 类的类型 ,比如android.widget.TextView.class。

移除动画目标:

  • removeTarget(View target)

  • removeTarget(int targetId)

  • removeTarget(String targetName)

  • removeTarget(Class target)

排除不进行动画的view:

  • excludeTarget(View target, boolean exclude)

  • excludeTarget(int targetId, boolean exclude)

  • excludeTarget(Class type, boolean exclude)

  • excludeTarget(Class type, boolean exclude)

排除某个 ViewGroup 的所有子View:

  • excludeChildren(View target, boolean exclude)

  • excludeChildren(int targetId, boolean exclude)

  • excludeChildren(Class type, boolean exclude)

自定义 Transition动画

主要三个方法,跟属性定义。

属性定义:官网提醒我们避免跟其他的属性名同名,建议我们命名规则:package_name:transition_class:property_name。

三个方法:captureStartValues()、captureEndValues()、createAnimator()

  • captureStartValues(transitionValues: TransitionValues)开始场景会多次调用,在这里你调用transitionValues.values[你定义的属性名]并将此时属性的值赋值给它

  • captureEndValues(transitionValues: TransitionValues)结束场景会多次调用,在这里你调用transitionValues.values[你定义的属性名]并将此时属性的值赋值给它

  • createAnimator(sceneRoot:ViewGroup?,startValues:TransitionValues?, endValues:TransitionValues? ): Animator?重点是这个函数,我们在这里根据开始的场景跟结束的场景值,定义对应的属性动画,并通过监听属性动画addUpdateListener的方法,进行对应的属性改变。

补充说明:captureStartValues()、captureEndValues()实际上是用于将此时的改变的属性值,存储到TransitionValues中的hashMap中(定义的属性名为key属性值为对应的value),方便我们在后面createAnimator根据存储的值进行属性动画的创建。

例子:自定义背景颜色属性转场动画。

class ChangeColorTransition : Transition() {

    companion object {
        /**
         *  根据官网提供的命名规则 package_name:transition_class:property_name,避免跟与其他 TransitionValues 键起冲突
         *  将颜色值存储在TransitionValues对象中的键
         */
        private const val PROPNAME_BACKGROUND = "com.mzs.myapplication:transition_colors:background"
    }

    /**
     * 添加背景Drawable的属性值到目标的TransitionsValues.value映射
     */
    private fun captureValues(transitionValues: TransitionValues?) {
        val view = transitionValues?.view ?: return
        //保存背景的值,供后面使用
        transitionValues.values[PROPNAME_BACKGROUND] = (view.background as ColorDrawable).color
    }
    //关键方法一 :捕获开始的场景值,多次调用
    override fun captureStartValues(transitionValues: TransitionValues) {
        if (transitionValues.view.background is ColorDrawable)
            captureValues(transitionValues)
    }
   //关键方法二 :捕获结束的场景值,多次调用。
   // 将场景中的属性值存储到transitionValues的
    override fun captureEndValues(transitionValues: TransitionValues) {
        if (transitionValues.view.background is ColorDrawable)
            captureValues(transitionValues)
    }

 //关键方法三:根据    override fun createAnimator(
        sceneRoot: ViewGroup?,
        startValues: TransitionValues?,
        endValues: TransitionValues?
    ): Animator? {
        //存储一个方便的开始和结束参考目标。
        val view = endValues!!.view
        //存储对象包含背景属性为开始和结束布局
        var startBackground = startValues!!.values[PROPNAME_BACKGROUND]
        var endBackground = endValues!!.values[PROPNAME_BACKGROUND]
        //如果没有背景等的直接忽略掉
        if (startBackground != endBackground) {
          //定义属性动画。
            var animator = ValueAnimator.ofObject(ArgbEvaluator(), startBackground, endBackground)
        //设置监听更新属性
            animator.addUpdateListener { animation ->
                var value = animation?.animatedValue
                if (null != value) {
                    view.setBackgroundColor(value as Int)
                }
            }
            return animator
        }
        return null
    }
}

代码中使用

var changeColorTransition = ChangeColorTransition()
changeColorTransition.duration = 2000
TransitionManager.beginDelayedTransition(root_view, changeColorTransition)
val backDrawable = view1_red.background as ColorDrawable
if (backDrawable.color == Color.RED) {
  view1_red.setBackgroundColor(Color.BLUE)
} else {
  view1_red.setBackgroundColor(Color.RED)
}

/   Scene的相关内容   /

Scene的创建

sceneRoot是要进行场景变化的根布局,不用非得是整个布局的根布局,只要是包含了场景变化的根布局可以了。R.layout.scene_layout0与R.layout.scene_layout1中的要进行转场动画的控件id一致。

通过 Scene.getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) 方法。

var scene0 = Scene.getSceneForLayout(sceneRoot,R.layout.scene_layout0,this)
var scene1 = Scene.getSceneForLayout(sceneRoot,R.layout.scene_layout1,this)

通过Scene()构造函数。

val view =  LayoutInflater.from(this).inflate(R.layout.scene_layout0,sceneRoot,false)
val scene0 = Scene(sceneRoot,view)
val view1 =  LayoutInflater.from(this).inflate(R.layout.scene_layout1,sceneRoot,false)
val scene1 = Scene(sceneRoot,view1)

这里有一点需要注意:

LayoutInflater.from(this).inflate(R.layout.scene_layout0,sceneRoot,false),最后一个参数要传false,不然一旦你的view添加到sceneRoot中,你去调用TransitionManager.go()传入参数就会报错IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.

----------------------------scene_layout0的布局----------------------------------
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/black_circle"
        android:layout_width="160dp"
        android:layout_height="160dp"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:layout_marginStart="18dp"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="48dp"
        android:src="@drawable/shape_black_circle" />

    <ImageView
        android:id="@+id/yellow_circle"
        android:layout_width="160dp"
        android:layout_height="160dp"
        android:layout_alignParentTop="true"
        android:layout_alignParentEnd="true"
        android:layout_marginTop="48dp"
        android:layout_marginEnd="40dp"
        android:layout_marginRight="10dp"
        android:src="@drawable/shape_yellow_circle" />

    <ImageView
        android:id="@+id/red_circle"
        android:layout_width="160dp"
        android:layout_height="160dp"
        android:layout_below="@+id/black_circle"
        android:layout_alignParentStart="true"
        android:layout_marginStart="13dp"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="39dp"
        android:src="@drawable/shape_red_circle" />

    <ImageView
        android:id="@+id/blue_circle"
        android:layout_width="160dp"
        android:layout_height="160dp"
        android:layout_alignParentTop="true"
        android:layout_alignParentEnd="true"
        android:layout_marginTop="241dp"
        android:layout_marginEnd="45dp"
        android:layout_marginRight="10dp"
        android:src="@drawable/shape_blue_circle" />
</RelativeLayout>

----------------------------scene_layout1的布局----------------------------------
与scene_layout0一样,只是ImageView的位置更换了一下。

两个场景要进行转场变化的控件id是一致的。

我通过实践发现了一个问题:当多个转场控件放到不同的ViewGroup下面,而不是在同一个ViewGroup的布局下面,产生的动画会有不一致的情况。

上面的所有ImageView都放在RelativeLayout的布局下面,与使用Linearlayout为纵向根布局再加上两个子横向Linearlayout,再将ImageView两两放置到子横向Linearlayout中,你会看到位置变化的转场效果可能不是你所期望的。(这里应该是因为不在同一个ViewGroup下导致的)

使用
//场景1:
val transition = TransitionInflater.from(this).inflateTransition(R.transition.explore_transtion)
TransitionManager.go(scene0,transition)

//场景2:
val transition = TransitionInflater.from(this).inflateTransition(R.transition.explore_transtion)
TransitionManager.go(scene1,transition)

/   Activity的转场动画   /

基本主要API
  • window.enterTransition: 进入时候的转场效果

  • window.exitTransition: 退出时候的转场效果

  • window.reenterTransition: 重新进入的转场效果

  • window.returnTransition: 回退的时候的转场效果

对应样式下的:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
  <item name="android:windowEnterTransition"></item>
  <item name="android:windowExitTransition"></item>
  <item name="android:windowReenterTransition"></item>
  <item name="android:windowReturnTransition"></item>
 </style>

Android 支持以下进入和退出过渡:
  • explore : 将视图移入场景中心或从中移出。

  • slide : 将视图从场景的其中一个边缘移入或移出。

  • fade : 通过更改视图的不透明度,在场景中添加视图或从中移除视图。

系统支持将任何扩展 Visibility 类的过渡作为进入或退出过渡。

基本使用

在onCreate()中设置转场动画。

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setUpWindow()
}

private fun setUpWindow() {
       window.let {
            it.exitTransition = TransitionInflater.from(this).inflateTransition(R.transition.fade_transtion)
            it.enterTransition = Explode().apply {
                duration = 500
            }
            it.reenterTransition = Explode().apply {
                duration = 500
            }
            it.returnTransition = Slide().apply {
                duration = 500
            }
        }
        }
}

跳转的时候,startActivity增加bundle。

val intent = Intent(this@MainActivity, SampleTranslateActivity::class.java)
val bundle =  ActivityOptionsCompat.makeSceneTransitionAnimation(this).toBundle()//Androidx提供的类
//val bundle =  ActivityOptions.makeSceneTransitionAnimation(this).toBundle()//不是Andoridx的时候使用ActivityOptions
startActivity(intent,bundle)

上面的效果存在一些问题,有些动画重叠在一块了。我们需要设置一下代码让进入退出的动画按序完成而不重叠到一块的时候。

setWindowAllowEnterTransitionOverlap(false)
setWindowAllowReturnTransitionOverlap(false)

或者在Activity或者Application对应的样式下面增加。

<item name="android:windowAllowEnterTransitionOverlap">false</item>
<item name="android:windowAllowReturnTransitionOverlap">false</item>

/   Activity共享转场动画   /

基本API

对应各方法进入时候的转场效果,跟上面的转场动画的api是相对的。

  • window.sharedElementEnterTransition

  • window.sharedElementExitTransition

  • window.sharedElementReenterTransition

  • window.sharedElementReturnTransition对应样式下的

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:windowSharedElementEnterTransition"></item>
        <item name="android:windowSharedElementExitTransition"></item>
        <item name="android:windowSharedElementReenterTransition"></item>
        <item name="android:windowSharedElementReturnTransition"></item>
</style>

基本使用

注意:版本要大于android5.0以上的,才有提供共享元素场景动画,用的时候记得做一下版本兼容。

    // Check if we're running on Android 5.0 or higher
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        // Apply activity transition
    } else {
        // Swap without transition
    }

先在xml样式中开启。

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
  <item name="android:windowContentTransitions">true</item>
</style>

或者代码中开启。

requestWindowFeature(Window.FEATURE_CONTENT_TRANSITIONS)

定义两个布局都要设置android:transitionName,跳转布局一

    <ImageView
        android:id="@+id/image_blue"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:src="@drawable/shape_blue_circle"
        android:transitionName="blue_name"
        />
    <TextView
        android:id="@+id/text1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:transitionName="textName"
        android:text="这个是我等下转场假装变大的数据~~~~"
        />

布局二

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_marginTop="68dp"
        android:src="@drawable/shape_blue_circle"
        android:transitionName="blue_name"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.497"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="68dp"
        android:text="TextView"
        android:transitionName="textName"
        android:textSize="23sp"
        android:textColor="@color/black"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

在两个Activity中分别设置共享元素的转场动画。

window.sharedElementEnterTransition = ChangeBounds()
window.sharedElementExitTransition = ChangeBounds()

跳转开始。

val intent = Intent(this@MainActivity, ShareElementActivity2::class.java)
// 构造多个Pair 一个Pair对应一个共享元素 
val pair = Pair(image_blue as View, image_blue.transitionName)
val pair1 = Pair(text1 as View, text1.transitionName)
// 将多个共享元素传入
val options = ActivityOptions.makeSceneTransitionAnimation(
  this@MainActivity,
  pair, pair1
)
startActivity(intent, options.toBundle())

限制(选自Android官方文档)
  • Android 版本在 4.0(API Level 14)到4.4.2(API Level 19) 使用 Android Support Library’s

  • 应用于 SurfaceView 的动画可能无法正确显示。SurfaceView 实例是从非界面线程更新的,因此这些更新与其他视图的动画可能不同步。

  • 当应用于 TextureView 时,某些特定过渡类型可能无法产生所需的动画效果。

  • 扩展 AdapterView 的类(例如 ListView)会以与过渡框架不兼容的方式管理它们的子视图。如果您尝试为基于 AdapterView 的视图添加动画效果,则设备显示屏可能会挂起。

  • 如果您尝试使用动画调整 TextView 的大小,则文本会在该对象完全调整过大小之前弹出到新位置。为了避免出现此问题,请勿为调整包含文本的视图的大小添加动画效果。


本文的源码地址:

https://github.com/lovebluedan/AnimatorPro

推荐阅读:

现在,我已经不再恐慌了

这份AS快捷键大全,让你的开发效率快10倍

Kotlin vs Flutter,我到底应该怎么选?

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值