android 自定义View 视差动画

Posted 史大拿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android 自定义View 视差动画相关的知识,希望对你有一定的参考价值。

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

**系统: **mac

android studio: 4.1.3

**kotlin version:**1.5.0

gradle: gradle-6.5-bin.zip

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

上一篇: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()解析,

思路分析

  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与调用:

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

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

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

来看看当前的效果

自定义属性

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

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

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

我们自己定义属性:

并且给需要变换的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)")

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

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

这里我们的属性是保存到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)  ...  
)

来看看现在的效果:

此时效果就基本完成了

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

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

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

思路参考自

完整代码

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

热门文章:

以上是关于android 自定义View 视差动画的主要内容,如果未能解决你的问题,请参考以下文章

android自定义view --视差动画

android自定义view --视差动画

UICollectionViewCell 与自定义 UICollectionViewLayout 仅在一个子视图上创建视差动画

Ultra-Pull-To-Refresh 自定义下拉刷新视差动画

Android实现ViewPager视差动画效果及背景渐变过渡

视差动画 - 雅虎新闻摘要加载