观心静

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

前言

  ViewPage2是ViewPage的取代者,解决了一些解决了其前辈ViewPage的大部分难题,包括从右到左的布局支持,垂直方向,可修改的Fragment集合等。从易用性上来说ViewPage2的确会比ViewPage更简单,并且它实际上是使用RecyclerView实现的。源码里可以很容易看到,另外它的适配器也是RecyclerView.Adapter。

特别注意

在developer官网上,已经说明了 ViewPager2在导航键翻页(TV模式),根本没实现。所有,如果你的是TV应用,原则上不建议使用ViewPager2开发(TV应用用它浪费时间),而是使用ViewPager。

这下面说需要完善,都2022年了,居然还没完善。 本着钻研的精神(钻牛角尖),本博客最下面给了一个不太稳定的方法实现焦点的触控。 

 

 

依赖

dependencies {
        implementation "androidx.viewpager2:viewpager2:1.0.0"
    }

使用RecyclerView.Adapter

使用场景一般是首页图片轮播,或者引导页面

xml

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".viewpage2.ViewPager2Activity">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/imagePager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" />

</androidx.constraintlayout.widget.ConstraintLayout>

Adapter

class ViewPager2Adapter1() : RecyclerView.Adapter<ViewPager2Adapter1.ViewHolder>() {
    val mImageList = mutableListOf<Int>()

    fun refreshData(list :MutableList<Int>?){
        list?.let {
            mImageList.addAll(list)
        }
        notifyDataSetChanged()
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val imageView = itemView.findViewById<ImageView>(R.id.image)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewPager2Adapter1.ViewHolder {
        return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_view_pager_demo_1, parent, false))
    }

    override fun onBindViewHolder(holder: ViewPager2Adapter1.ViewHolder, position: Int) {
        holder.imageView.setImageResource(mImageList[position])
    }

    override fun getItemCount(): Int = mImageList.size
}

activity

class ViewPager2Activity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view_pager2)
        val adapter = ViewPager2Adapter1()
        val imagePager = findViewById<ViewPager2>(R.id.imagePager)
        imagePager.adapter = adapter
        adapter.refreshData(mutableListOf(R.drawable.ic_landscape_1, R.drawable.ic_landscape_2, R.drawable.ic_landscape_3, R.drawable.ic_landscape_4))

    }
}

效果图

 

 

使用FragmentStateAdapter

使用场景一般是主页tab分页,每一页都是Fragment

class MessageAdapter(fragments: List<Fragment>, fragment: Fragment) : FragmentStateAdapter(fragment) {
    private var mFragments :List<Fragment> = fragments

    override fun getItemCount() = mFragments.size

    override fun createFragment(position: Int) = mFragments[position]
}

上面的代码是在Fragment中使用,如果需要在activity中使用就使用另一个构造方法

public FragmentStateAdapter(@NonNull FragmentActivity fragmentActivity)

然后是添加到ViewPager2里

        val fragmentList = listOf(AlertFragment.newInstance(), FamilyFragment.newInstance(), NoticeFragment.newInstance())
        val adapter = MessageAdapter(fragmentList, this)
        mBinding.viewPager.adapter = adapter

与TabLayout配合使用

TabLayout tabLayout = view.findViewById(R.id.tab_layout);
        new TabLayoutMediator(tabLayout, viewPager,
                (tab, position) -> tab.setText("OBJECT " + (position + 1))
        ).attach();

 但是请注意!在添加数据的时候不需要在newTab了,参考如下代码

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val fragmentList = listOf(AlertFragment.newInstance(), FamilyFragment.newInstance(), NoticeFragment.newInstance())
        val adapter = MessageAdapter(fragmentList, this)
        mBinding.viewPager.adapter = adapter
        val tabIconArray = intArrayOf(R.drawable.message_ic_alert_3, R.drawable.message_ic_family_3, R.drawable.message_ic_notice_3)
        TabLayoutMediator(mBinding.tabLayout, mBinding.viewPager) { tab, position ->

        }.attach()

        for ((index, itemData)in tabIconArray.withIndex()) {
            //这里不需要newTab(),因为上面的TabLayoutMediator已经创建好了tab
            val tab: TabLayout.Tab? = mBinding.tabLayout.getTabAt(index)
            val binding = MessageItemMessageBinding.inflate(layoutInflater, mBinding.tabLayout, false)
            binding.icon.setImageResource(itemData)
            tab?.customView = binding.root
            mItemRedDotArray.add(binding.redDot)
        }
        initListener()
    }

禁用左右滑动切换

viewPager.setUserInputEnabled(false)

设置方向

android:orientation="horizontal"

android:orientation="vertical"

设置滑动方向

从左到右

android:layoutDirection="ltr"

从右到左

android:layoutDirection="rtl"

翻页结果监听

        imagePager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {

            override fun onPageScrollStateChanged(state: Int) {
                super.onPageScrollStateChanged(state)
                /**
                表示ViewPager2处于空闲、稳定状态。当前页面完全可见,并且没有动画正在进行中。
                 SCROLL_STATE_IDLE = 0
                表示ViewPager2当前正在被用户拖动,或者通过虚假拖动功能以编程方式进行拖动。
                 SCROLL_STATE_DRAGGING = 1
                表示ViewPager2正在稳定到最终位置。
                 SCROLL_STATE_SETTLING = 2
                 */
                Log.e("zh", "滚动状态变化 : state = $state")
            }

            override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
                super.onPageScrolled(position, positionOffset, positionOffsetPixels)
                /**
                position – 当前显示的第一页的位置索引。如果 positionOffset 不为零,则页面位置+1 将可见。
                positionOffset – 表示在位置处与页面的偏移量, 取值范围[0,1]
                positionOffsetPixels – 以像素为单位的值,指示与位置的偏移量。
                 */
                Log.e("zh", "正在翻页滚动 : position = $position positionOffset = $positionOffset positionOffsetPixels = $positionOffsetPixels")

            }

            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
                Log.e("zh", "当前页面 : position = $position")
            }
        })

翻页动画

ViewPager2的翻页动画稍微一点点复杂,需要一些耐心理解.

首先动画的实现是依靠 ViewPager2.PageTransformer 接口回调实现的, 在下面的代码中有注释说明,

        imagePager.setPageTransformer(object : ViewPager2.PageTransformer {
            override fun transformPage(page: View, position: Float) {
                if (position < -1) { // 范围 -无穷数 到 -1
                     // 此页面在左侧屏幕外,在这个位置的view已经看不到了,一般不需要处理,只要给初始默认值就行
                    Log.e("zh", "此页面在左侧屏幕外: $position")
                } else if (position <= 1) { //范围-1 到 1
                    /*
                     * 在这个位置的view正在在屏幕中显示,是需要实现动画效果的view
                     * 这里其实有2个view会交替回调, 分别是position小于0的左边的view 与 position大于0的右边的view
                     */
                    if (position < 0) {
                        //viewpager左边item的显示
                        Log.e("zh", "viewpager左边item的显示 位置 = $position view的内存地址 = ${page}")
                       
                    } else {
                        //viewpager右边item的显示
                        Log.e("zh", "viewpager右边item的显示 位置 = $position view的内存地址 = ${page}")
                    }

                } else { //  范围 1 到 +无穷数
                    // 此页面在右侧屏幕外,在这个位置的view已经看不到了,一般不需要处理,只要给初始默认值就行
                    Log.e("zh", "此页面在右侧屏幕外: $position")
                }
            }
        })

如果你看了上面的注释依然不太明白,我们可以用图片解释一下

图片1

图片2

理解这张图片很重要! 这张图片中的效果出现的时候,就是position 正在返回 -1 到 1的值. 

这正是我们需要实现动画效果(透明度,放大缩小,旋转抛出等等效果)的时候

 

举例一个淡入淡出动画效果

在上面说明了动画的取值范围后,这里举例一个最容易实现的动画,淡入淡出动画.

        imagePager.setPageTransformer(object : ViewPager2.PageTransformer {
            override fun transformPage(page: View, position: Float) {
                if (position < -1) {
                    page.alpha = 0f

                } else if (position <= 1) { //范围-1 到 1
                    /*
                    * 求绝对值,作为动画的变量值
                    */
                    val animationValue: Float = Math.abs(position)
                    if (position < 0) {
                        //viewpager左边item的显示
                        page.alpha = 1 - animationValue
                    } else {
                        //viewpager右边item的显示
                        page.alpha = 1 - animationValue
                    }

                } else {                     
            page.alpha = 0f } } })

效果图

举一反三实现旋转与缩小放大动画效果

代码

   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view_pager2)
        val adapter = ViewPager2Adapter1()
        val imagePager = findViewById<ViewPager2>(R.id.imagePager)
        imagePager.adapter = adapter
        adapter.refreshData(mutableListOf(R.drawable.ic_landscape_1, R.drawable.ic_landscape_2, R.drawable.ic_landscape_3, R.drawable.ic_landscape_4))
        imagePager.setPageTransformer(object : ViewPager2.PageTransformer {
            override fun transformPage(page: View, position: Float) {
                if (position < -1) {
                    page.alpha = 0f

                } else if (position <= 1) { //范围-1 到 1
                    /*
                    * 求绝对值,作为动画的变量值
                    */
                    val animationValue: Float = Math.abs(position)
                    if (position < 0) {
                        //viewpager左边item的显示
                        page.alpha = 1 - animationValue
                        setRotate(page, animationValue)
                        setScale(page, animationValue)
                    } else {
                        //viewpager右边item的显示
                        page.alpha = 1 - animationValue
                        setRotate(page, animationValue)
                        setScale(page, animationValue)
                    }

                } else {
                    page.alpha = 0f
                }
            }
        })
    }

    /**
     * 旋转效果
     */
    fun setRotate(view: View, value: Float) {
        view.rotationX = 0.5f
        view.rotationY = 0.5f
        view.rotation = 360 * value
    }

    /**
     * 缩放效果
     */
    fun setScale(view: View, value: Float) {
        view.scaleX = 1 - value
        view.scaleY = 1 - value
    }

效果图

看看google给的例子

例子1

public class DepthPageTransformer implements PageTransformer {
    private static float MIN_SCALE = 0.75f;
 
    @SuppressLint("NewApi")
    @Override
    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();
        if (position < -1) { // [-Infinity,-1)//This page is way off-screen to the left.
            view.setAlpha(0);
        } else if (position <= 0) { // [-1,0]Use //the default slide transition when moving to the left page
            view.setAlpha(1);
            view.setTranslationX(0);
            view.setScaleX(1);
            view.setScaleY(1);
        } else if (position <= 1) { // (0,1]// Fade the page out.
            view.setAlpha(1 - position);
            // Counteract the default slide transition
            view.setTranslationX(pageWidth * -position);
            // Scale the page down (between MIN_SCALE and 1)
            float scaleFactor = MIN_SCALE + (1 - MIN_SCALE)
                    * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);
        } else { // (1,+Infinity]
                    // This page is way off-screen to the right.
            view.setAlpha(0);
 
        }
    }
 
}

 效果图

 

例子2

public class ZoomOutPageTransformer implements PageTransformer { 
    private static float MIN_SCALE = 0.85f; 
   
    private static float MIN_ALPHA = 0.5f; 
   
    @Override 
    public void transformPage(View view, float position) { 
        int pageWidth = view.getWidth(); 
        int pageHeight = view.getHeight(); 
   
        if (position < -1) { // [-Infinity,-1) 
                                // This page is way off-screen to the left. 
            view.setAlpha(0); 
        } else if (position <= 1) { // [-1,1] 
                                    // Modify the default slide transition to 
                                    // shrink the page as well 
            float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position)); 
            float vertMargin = pageHeight * (1 - scaleFactor) / 2; 
            float horzMargin = pageWidth * (1 - scaleFactor) / 2; 
            if (position < 0) { 
                view.setTranslationX(horzMargin - vertMargin / 2); 
            } else { 
                view.setTranslationX(-horzMargin + vertMargin / 2); 
            } 
            // Scale the page down (between MIN_SCALE and 1) 
            view.setScaleX(scaleFactor); 
            view.setScaleY(scaleFactor); 
            // Fade the page relative to its size. 
            view.setAlpha(MIN_ALPHA + (scaleFactor - MIN_SCALE) 
                    / (1 - MIN_SCALE) * (1 - MIN_ALPHA)); 
        } else { // (1,+Infinity] 
                    // This page is way off-screen to the right. 
            view.setAlpha(0); 
        } 
    } 
} 

效果图

背景图片与ViewPager2 左右滑动的联动动画实现

xml

    <ImageView
        android:id="@+id/bgImage"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleX="1.1"
        android:scaleY="1.1"
        android:background="@mipmap/launcher_ic_bg" />

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/viewPage"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="parent"
        app:layout_constraintVertical_bias="1.0"
        tools:layout_editor_absoluteX="0dp" />

java

        mBinding.viewPage.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
            var lastPosition = 0
            var animationValue = 50f
            lateinit var objectAnimator : ObjectAnimator
            override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
                mDotsAdapter.setSelectItem(position)
                if (lastPosition == position){
                    return
                }
                if (lastPosition < position){
                    //
                    animationValue = animationValue -50f
                    objectAnimator  = ObjectAnimator.ofFloat(mBinding.bgImage,"translationX", animationValue)
                } else{
                    //
                    animationValue = animationValue + 50f
                    objectAnimator  = ObjectAnimator.ofFloat(mBinding.bgImage,"translationX", animationValue)
                }
                objectAnimator.duration = 500
                objectAnimator.start()
                lastPosition = position
            }

            override fun onPageSelected(position: Int) {
            }

            override fun onPageScrollStateChanged(state: Int) {
            }
        })

TV应用焦点与ViewPager2的兼容处理

在developer官网上,已经说明了 ViewPager2在导航键翻页(TV模式),根本没实现。所有,如果你的是TV应用,原则上不建议使用ViewPager2开发,而是使用ViewPager。 如果你非得杆上ViewPager2,下面这个代码也提供一个实现思路。但是十分不稳定

TV应用特别是支持触控又支持遥控器的TV设备,如何让焦点翻页? 特别是如何只使用遥控器方向键的情况下翻页呢?

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(mBinding.root)
        //注册全局焦点监听    
        mBinding.root.getViewTreeObserver().addOnGlobalFocusChangeListener(focusChangeListener)
    }

    override fun onDestroy() {
        super.onDestroy()
        mBinding.root.viewTreeObserver.removeOnGlobalFocusChangeListener(focusChangeListener)
    }

    private val focusChangeListener: ViewTreeObserver.OnGlobalFocusChangeListener = object : ViewTreeObserver.OnGlobalFocusChangeListener {
        override fun onGlobalFocusChanged(oldFocus: View?, newFocus: View?) {
            Log.e("zh", "newFocus: ${newFocus} ")
            if (newFocus != null) {
                /**
                 * viewPage里的当前焦点view的第3个父类其实是RecyclerView,这里需要准确找到它。
                 * 当前你也可以使用另一种方法,那就是继承ViewPage,然后让mRecyclerView 全局变量暴露到外包
                 */
                mBinding.viewPage.findFocus()?.parent?.parent?.parent?.let { recyclerView ->
                    if (recyclerView is RecyclerView) {
                        /**
                         * 重点方法:findContainingViewHolder
                         * 根据当前焦点view找到当前的ViewHolder
                         */
                        val viewHolder = recyclerView.findContainingViewHolder(mBinding.viewPage.findFocus())
                        if (viewHolder != null) {
                            if (viewHolder.layoutPosition != RecyclerView.NO_POSITION) {
                                //使其翻页
                                mBinding.viewPage.currentItem = viewHolder.layoutPosition
                            }
                        }
                    }
                }
            }
        }
    }

 

ViewPager2的已知bug

 因为ViewPager与SwipeRefreshLayout冲突导致RecyclerView或者其他列表布局的item无法点击的问题

 

End

posted on 2020-08-19 13:58  观心静  阅读(2318)  评论(0编辑  收藏  举报