• android 自定义View 视差动画


    本系列自定义View全部采用kt

    **系统: **mac

    android studio: 4.1.3

    **kotlin version:**1.5.0

    gradle: gradle-6.5-bin.zip

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

    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 */
            )
        }
    }
    
    • 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
    • 47

    如果对这段代码有兴趣的,可以去看 上一篇: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]
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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 
            )
        }
    }
    
    • 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
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    这个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)}")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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:拖动停止了..")
                }
            }
        }
    })
    
    • 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

    这三个方法介绍一下:

    • 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) { ...  }
    })
    
    • 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
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    来看看现在的效果:

    8F7CCD955FC2F22FACCD1D2536105E42

    此时效果就基本完成了

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

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

    image-20220831142027559

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

    思路参考自

    完整代码

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

    热门文章:

  • 相关阅读:
    javascript中使用new来调用构造函数,生成新对象时发生了什么
    9、软件包管理
    mysql高阶语句
    从0开始python学习-53.python中flask创建简单接口
    多目标优化算法:基于非支配排序的海象优化算法(NSWOA)MATLAB
    JAVA 从入门到起飞 面向对象 day08 P2
    Lambda表达式支持的方法引用
    高数(上)
    李沐动手学深度学习V2-RNN原理
    source insight的使用方法
  • 原文地址:https://blog.csdn.net/weixin_44819566/article/details/126623382