Android Material Motion 过渡模式使用总结

戚同
2023-12-01
写在前面

本文是对 Material Motion过渡模式的使用进行总结, 适用于已经明白过渡模式的使用但忘记具体代码实现的开发者. 本文用例基于 androidxkotlin . 详细完整教程请查阅: material-motion-android.


一. 简介

material-motion-android 是MDC-Android库中的一组过渡模式; 其中主要包含四种过渡模式:

  1. Container Transform : 包含 container 的UI元素之间的转换 在两个不同的UI元素之间创建可见连接, 使一个UI元素无缝过渡到另一个UI元素.
  2. Shared Axis : 具有空间或导航关系的UI元素之间的过渡; 在x, y或z轴上使用共享变换来加强元素之间的关系.
  3. Fade Through : 在彼此之间没有密切关系的UI元素之间进行过渡; 使用顺序淡入和淡入, 以及传入元素的比例.
  4. Fade : 用于在屏幕范围内进入或退出的UI元素.

MDC-Android库在AndroidX Transition库(androidx.transition)和 Android Transition Framework(android.transition)的基础上,为这些模式提供了转换类 :

  • AndroidX (androidx.transition)

    1. com.google.android.material.transition 包下;
    2. 支持API14及以上;
    3. 支持 FragmentsViews , 不支持 ActivityiesWindows ;
    4. 包含反向移植的错误修复程序和跨API级别的一致行为;
    5. 依赖: implementation 'com.google.android.material:material:1.2.1'
  • Framework (android.transition)

    1. com.google.android.material.transition.platform 包下;
    2. 支持API21及以上;
    3. 支持 Fragments | Views | Activityies | Windows ;
    4. 错误修复未向后移植, 并且在各个API级别上可能具有不同的行为;

本文使用 AndroidX Transition 库; Fragmentsnavigation 库导航.

二. Container Transform 过渡模式
  1. 核心类 MaterialContainerTransform ;
  2. 本用例为跳转的目标Fragment中包含 Container
    1. 通过 transitionName 标记, 使过渡系统在不同布局获取两个控件;
      1. 在API 21以上可以直接使用 android:transitionName ;
      2. 如果支持API 21 以下使用 ViewCompat#setTransitionName 方法;
      3. 对于 RecyclerView 中的 item 布局中的控件, transitionName 不能相同;
    2. 点击 RecyclerView item 跳转 Fragment
      val emailCardDetailTransitionName = getString(R.string.email_card_detail_transition_name)
      val extras = FragmentNavigatorExtras(cardView to emailCardDetailTransitionName)
      val directions = HomeFragmentDirections.actionHomeFragmentToEmailFragment(email.id)
      findNavController().navigate(directions, extras)
      
    3. 在要跳转的 FragmentonCreate 中添加 sharedElementEnterTransition (Fragmen.setSharedElementEnterTransition()) ,完成这一步便实现了跳转新 Fragment的过渡
      sharedElementEnterTransition = MaterialContainerTransform().apply {   
          drawingViewId = R.id.nav_host_fragment   
          duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()     
          scrimColor = Color.TRANSPARENT      
          setAllContainerColors(requireContext().themeColor(R.attr.colorSurface))
      }
      
    4. 在设置 sharedElementEnterTransition 以后, 返回之前页面时过渡与 sharedElementEnterTransition 过渡相反; 但现在不起作用是因为, 上一个页面列表并未填充到 RecyclerView 中; 解决方法如下:
      // 在上一个页面的 onViewCreated 方法中添加以下代码
      postponeEnterTransition()
      view.doOnPreDraw { startPostponedEnterTransition() }
      
    5. 在跳转与重新返回时为页面添加过渡
      // 在点击跳转其他页面之前添加以下代码
      exitTransition = MaterialElevationScale(false).apply {   
          duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
      }
      reenterTransition = MaterialElevationScale(true).apply {   
          duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
      }
      
    6. 为了确保过渡用于整个屏幕, 而不是布局下的控件在根布局 RecyclerView 中标记 transition group
      android:transitionGroup="true"
      
      API 21 及以上直接使用 android:transitionGroup="true" , 兼容 API 21 以下版本使用 ViewGroupCompat#setTransitionGroup 方法;
  3. 本用例为跳转的目标页面在 Container
    1. 在跳转的目标页面 onViewCreated 方法中添加以下代码, (注意: Slide 包为 androidx.transition , 如使用 android.transition 会崩溃)
      enterTransition = MaterialContainerTransform().apply {   
         startView = requireActivity().findViewById(R.id.fab)   
         endView = emailCardView   
         duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()     
         scrimColor = Color.TRANSPARENT   
         containerColor = requireContext().themeColor(R.attr.colorSurface)   
         startContainerColor = requireContext().themeColor(R.attr.colorSecondary)   
         endContainerColor = requireContext().themeColor(R.attr.colorSurface)
      }
      returnTransition = Slide().apply {   
         duration = resources.getInteger(R.integer.reply_motion_duration_medium).toLong()   
         addTarget(R.id.email_card_view)
      }
      
    2. 返回之前控件过渡, 在跳转目标页面之前添加以下代码:
      currentNavigationFragment?.apply {   
          exitTransition = MaterialElevationScale(false).apply {       
              duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
          }
          reenterTransition = MaterialElevationScale(true).apply {       
              duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()   
          }
      }
      
  4. 本用例为同页面内两个控件( chiprecipientCardView )过渡, endView中包含 container
    1. 在显示 recipientCardView 之前, 添加以下代码, 其中 addTarget 确保只为传入控件提供过渡效果;
      val transform = MaterialContainerTransform().apply {  
          startView = chip   
          endView = binding.recipientCardView   
          scrimColor = Color.TRANSPARENT   
          endElevation = requireContext().resources.getDimension(
              R.dimen.email_recipient_card_popup_elevation_compat   
          )
          addTarget(binding.recipientCardView)
      }
      TransitionManager.beginDelayedTransition(binding.composeConstraintLayout, transform)
      
    2. 在显示 chip 之前, 添加以下代码, 其中 addTarget 确保只为传入控件提供过渡效果;
      val transform = MaterialContainerTransform().apply {   
          startView = binding.recipientCardView   
          endView = chip   
          scrimColor = Color.TRANSPARENT  
          startElevation = requireContext().resources.getDimension(    
              R.dimen.email_recipient_card_popup_elevation_compat  
          )   
          addTarget(chip)
      }
      TransitionManager.beginDelayedTransition(binding.composeConstraintLayout, transform)
      
三. Shared Axis 过渡模式
  1. 核心类 MaterialSharedAxis ;
  2. 本用例适用于没有相关 Container 时使用;
  3. 在跳转目标页面前添加添加本页面Z轴缩放过渡
    currentNavigationFragment?.apply {
        // 离开时放大过渡
        exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true).apply {       
            duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()   
        } 
        // 重新返回时缩小过渡
        reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false).apply {       
            duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()   
        }
    }
    
  4. 在跳转的目标页面中 onCreate 方法添加以下代码 (过渡方向与之前页面对应)
    // 进入时放大过渡
    enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true).apply {   
        duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
    }
    // 返回时缩小过渡
    returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false).apply {   
        duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
    }
    
  5. 为了确保 MaterialSharedAxis 过渡用于整个屏幕, 在根布局控件添加 transition group 标记
    android:transitionGroup="true"
    
    API 21 及以上直接使用 android:transitionGroup="true" , 兼容 API 21 以下版本使用 ViewGroupCompat#setTransitionGroup 方法;
四. Fade Through 过渡模式
  1. 核心类 MaterialFadeThrough;
  2. 本用例适用于如果不强调空间和层次关系, 可以使用该过渡模式;
  3. 在跳转目标布局前添加以下代码
    currentNavigationFragment?.apply {   
        exitTransition = MaterialFadeThrough().apply {       
            duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()   
        }
    }
    
  4. 在跳转的目标页面 onCreate 方法中添加 enterTransition
    enterTransition = MaterialFadeThrough().apply {
        duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
    }
    
  5. 本用例使用 navigation 重复启动相同 HomeFragment , HomeFramentnavigation_graph.xlm 中设置为全局操作, 并出栈当前Fragment, 相当于设置了 singleTop=true ,因此不需要处理返回或重新进入过渡; 以下为当前 HomeFragmentnavigation_graph 中的操作:
    <action
        android:id="@+id/action_global_homeFragment"
        app:destination="@+id/homeFragment"
        app:launchSingleTop="true"
        app:popUpTo="@+id/navigation_graph"
        app:popUpToInclusive="true"/>
    

参考文章: material-motion-android

 类似资料: