注册

android 自定义View: 视差动画

废话不多说,先来看今天要完成的效果:


9F7025B4D02C70198934C0CA7812ECE7


上一篇:android setContentView()解析中我们介绍了,如何通过Factory2来自己解析View,


那么我们就通过这个机制,来完成今天的效果《视差动画》,


回顾


先来回顾一下如何在Fragment中自己解析View


 class MyFragment : Fragment(), LayoutInflater.Factory2 {
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
         savedInstanceState: Bundle?,
    ): View {
         val newInflater = inflater.cloneInContext(activity)
         LayoutInflaterCompat.setFactory2(newInflater, this)
         return newInflater.inflate(R.layout.my_fragment, container, false)
    }
   
   // 重写Factory2的方法
   override fun onCreateView(
         parent: View?,
         name: String,
         context: Context,
         attrs: AttributeSet,
    ): View? {
     
      val view = createView(parent, name, context, attrs)
      // 此时的view就是自己创建的view!
     
     // ...................
     
 return view
  }
   
   // 重写Factory2的方法
   override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
         return onCreateView(null, name, context, attrs)
    }
   
   // SystemAppCompatViewInflater() 复制的系统源码
   private var mAppCompatViewInflater = SystemAppCompatViewInflater()
    private fun createView(
         parent: View?, name: String?, mContext: Context,
         attrs: AttributeSet,
    ): View? {
         val is21 = Build.VERSION.SDK_INT < 21
      // 自己去解析View
         return mAppCompatViewInflater.createView(parent, name, mContext, attrs, false,
             is21,  /* Only read android:theme pre-L (L+ handles this anyway) */
             true,  /* Read read app:theme as a fallback at all times for legacy reasons */
             false /* Only tint wrap the context if enabled */
        )
    }
 }

如果对这段代码有兴趣的,可以去看 上一篇:android setContentView()解析,


思路分析


9F7025B4D02C70198934C0CA7812ECE7




  1. viewpager + fragment




  2. 自定义属性:



    • 旋转: parallaxRotate
    • 缩放 : parallaxZoom
    • 出场移动:parallaxTransformOutX,parallaxTransformOutY
    • 入场移动:parallaxTransformInX,parallaxTransformInY



  3. 给需要改变变换的view设置属性




  4. 在fragment的时候自己创建view,并且通过AttributeSet解析所有属性




  5. 将需要变换的view保存起来,




  6. 在viewpager滑动过程中,通过addOnPageChangeListener{} 来监听viewpager变化,当viewpager变化过程中,设置对应view对应变换即可!




viewPager+Fragment


首先先实现最简单的viewpager+Fragment


代码块1.1


 class ParallaxBlogViewPager(context: Context, attrs: AttributeSet?) : ViewPager(context, attrs) {
 
     fun setLayout(fragmentManager: FragmentManager, @LayoutRes list: ArrayList<Int>) {
         val listFragment = arrayListOf<C3BlogFragment>()
         // 加载fragment
         list.map {
             C3BlogFragment.instance(it)
        }.forEach {
             listFragment.add(it)
        }
 
         adapter = ParallaxBlockAdapter(listFragment, fragmentManager)
    }
 
     private inner class ParallaxBlockAdapter(
         private val list: List<Fragment>,
         fm: FragmentManager
    ) : FragmentPagerAdapter(fm) {
         override fun getCount(): Int = list.size
         override fun getItem(position: Int) = list[position]
    }
 }

C3BlogFragment:


代码块1.2


 class C3BlogFragment private constructor() : Fragment(), LayoutInflater.Factory2 {
     companion object {
         @NotNull
         private const val LAYOUT_ID = "layout_id"
       
         fun instance(@LayoutRes layoutId: Int) = let {
             C3BlogFragment().apply {
                 arguments = bundleOf(LAYOUT_ID to layoutId)
            }
        }
    }
 
     private val layoutId by lazy {
         arguments?.getInt(LAYOUT_ID) ?: -1
    }
 
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
         savedInstanceState: Bundle?,
    ): View {
         val newInflater = inflater.cloneInContext(activity)
         LayoutInflaterCompat.setFactory2(newInflater, this)
         return newInflater.inflate(layoutId, container, false)
    }
 
     override fun onCreateView(
         parent: View?,
         name: String,
         context: Context,
         attrs: AttributeSet,
    ): View? {
         val view = createView(parent, name, context, attrs)
         /// 。。。 在这里做事情。。。 
         return view
    }
 
     private var mAppCompatViewInflater = SystemAppCompatViewInflater()
 
     override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
         return onCreateView(null, name, context, attrs)
    }
     private fun createView(
         parent: View?, name: String?, mContext: Context,
         attrs: AttributeSet,
    ): View? {
         val is21 = Build.VERSION.SDK_INT < 21
         return mAppCompatViewInflater.createView(parent, name, mContext, attrs, false,
             is21, 
             true, 
             false 
        )
    }
 }

这个fragment目前的作用就是接收传过来的布局,展示,


并且自己解析view即可!


xml与调用:


image-20220831110733672


R.layout.c3_1.item,这些布局很简单,就是



  • 一张静态图片
  • 一张动态图片

image-20220831111933761


其他的布局都是一样的,这里就不看了.


来看看当前的效果


74E509428BBC17F5C5745B2E019032A7


自定义属性


通常我们给一个view自定义属性,我们会选择在attrs.xml 中来进行,例如这样:


image-20220831112659868


但是很明显,这么做并不适合我们的场景,因为我们想给任何view都可以设置属性,


那么我们就可以参考ConstraintLayout中的自定义属性:


image-20220831113040794


我们自己定义属性:


image-20220831113206896


并且给需要变换的view设置值



  • app:parallaxRotate="10" 表示在移动过程中旋转10圈
  • app:parallaxTransformInY="0.5" 表示入场的时候,向Y轴方向偏移 height * 0.5
  • app:parallaxZoom="1.5" 表示移动过程中慢慢放大1.5倍

Fragment中解析自定义属性


我们都知道,所有的属性都会存放到AttributeSet中,先打印看一看:


 (0 until attrs.attributeCount).forEach {
     Log.i("szj属性",
         "key:${attrs.getAttributeName(it)}\t" +
                 "value:${attrs.getAttributeValue(it)}")
 }

image-20220831131135741


这样一来就可以打印出所有的属性,并且找到需要用的属性!


那么接下来只需要将这些属性保存起来,在当viewpager滑动过程中取出用即可!


image-20220831131719926


这里我们的属性是保存到view的tag中,


需要注意的是,如果你的某一个view需要变换,那么你的view就一定得设置一个id,因为这里是通过id来存储tag!


监听ViewPager滑动事件


 # ParallaxBlogViewPager.kt
 
 // 监听变化
 addOnPageChangeListener(object : OnPageChangeListener {
     // TODO 滑动过程中一直回调
     override fun onPageScrolled(
         position: Int,
         positionOffset: Float,
         positionOffsetPixels: Int,
    ) {
         Log.e("szjParallaxViewPager",
            "onPageScrolled:position:$position\tpositionOffset:${positionOffset}\tpositionOffsetPixels:${positionOffsetPixels}")
 
    }
 
     //TODO 当页面切换完成时候调用 返回当前页面位置
     override fun onPageSelected(position: Int) {
         Log.e("szjParallaxViewPager", "onPageSelected:$position")
    }
 
     // 
     override fun onPageScrollStateChanged(state: Int) {
         when (state) {
             SCROLL_STATE_IDLE -> {
                 Log.e("szjParallaxViewPager", "onPageScrollStateChanged:页面空闲中..")
            }
             SCROLL_STATE_DRAGGING -> {
                 Log.e("szjParallaxViewPager", "onPageScrollStateChanged:拖动中..")
            }
             SCROLL_STATE_SETTLING -> {
                 Log.e("szjParallaxViewPager", "onPageScrollStateChanged:拖动停止了..")
            }
        }
    }
 })

这三个方法介绍一下:




  • onPageScrolled(position:Int , positionOffset:Float, positionOffsetPixels)



    • @param position: 当前页面下标
    • @param positionOffset:当前页面滑动百分比
    • @param positionOffsetPixels: 当前页面滑动的距离

    在这个方法中需要注意的是,当假设当前是第0个页面,从左到右滑动,



    • position = 0
    • positionOffset = [0-1]
    • positionOffsetPixels = [0 - 屏幕宽度]

    当第1个页面的时候,从左到右滑动,和第0个页面的状态都是一样的


    但是从第1个页面从右到左滑动的时候就不一样了,此时



    • position = 0
    • positionOffset = [1-0]
    • positionOffsetPixels = [屏幕宽度 - 0]






  • onPageSelected(position:Int)



    • @param position: 但页面切换完成的时候调用



  • onPageScrollStateChanged(state:Int)




    • @param state: 但页面发生变化时候调用,一共有3种状体



      • SCROLL_STATE_IDLE 空闲状态
      • SCROLL_STATE_DRAGGING 拖动状态
      • SCROLL_STATE_SETTLING 拖动停止状态





了解了viewpager滑动机制后,那么我们就只需要在滑动过程中,


获取到刚才在tag种保存的属性,然后改变他的状态即可!


 # ParallaxBlogViewPager.kt
 
 // 监听变化
 addOnPageChangeListener(object : OnPageChangeListener {
     // TODO 滑动过程中一直回调
     override fun onPageScrolled(
         position: Int,
         positionOffset: Float,
         positionOffsetPixels: Int,
    ) {
         // TODO 当前fragment
         val currentFragment = listFragment[position]
         currentFragment.list.forEach { view ->
 // 获取到tag中的值
             val tag = view.getTag(view.id)
 
            (tag as? C3Bean)?.also {
                 // 入场
                 view.translationX = -it.parallaxTransformInX * positionOffsetPixels
                 view.translationY = -it.parallaxTransformInY * positionOffsetPixels
                 view.rotation = -it.parallaxRotate * 360 * positionOffset
 
 
                 view.scaleX =
                     1 + it.parallaxZoom - (it.parallaxZoom * positionOffset)
                 view.scaleY =
                     1 + it.parallaxZoom - (it.parallaxZoom * positionOffset)
 
            }
        }
 
         // TODO 下一个fragment
         // 防止下标越界
         if (position + 1 < listFragment.size) {
             val nextFragment = listFragment[position + 1]
             nextFragment.list.forEach { view ->
                 val tag = view.getTag(view.id)
 
                (tag as? C3Bean)?.also {
                     view.translationX =
                         it.parallaxTransformInX * (width - positionOffsetPixels)
                     view.translationY =
                         it.parallaxTransformInY * (height - positionOffsetPixels)
 
                     view.rotation = it.parallaxRotate * 360 * positionOffset
 
                     view.scaleX = (1 + it.parallaxZoom * positionOffset)
                     view.scaleY = (1 + it.parallaxZoom * positionOffset)
                }
            }
        }
    }
 
     //TODO 当页面切换完成时候调用 返回当前页面位置
     override fun onPageSelected(position: Int) {...}
 
     override fun onPageScrollStateChanged(state: Int) { ... }
 })

来看看现在的效果:


8F7CCD955FC2F22FACCD1D2536105E42


此时效果就基本完成了


但是一般情况下,引导页面都会在最后一个页面有一个跳转到主页的按钮


为了方便起见,我们只需要将当前滑动到的fragment页面返回即可!


image-20220831142027559


这么一来,我们就可以在layout布局中为所欲为,因为我们可以自定义属性,并且自己解析,可以做任何自己想做的事情!


思路参考自


完整代码


原创不易,您的点赞与关注就是对我最大的支持!


作者:史大拿
链接:https://juejin.cn/post/7137925163336597517
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册