Android使用ViewPager2中遇到的问题(使用ViewPager2实现画廊效果)

最近想试试用ViewPager2来实现画廊的效果,ViewPager2和ViewPager在API上有的地方不同,ViewPager2是通过内部嵌套一个RecyclerView来实现的

ViewPager2初始化的部分代码

private void initialize(Context context, AttributeSet attrs) {
        ...
        mRecyclerView = new RecyclerViewImpl(context);
        mRecyclerView.setId(ViewCompat.generateViewId());
        mRecyclerView.setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);

        mLayoutManager = new LinearLayoutManagerImpl(context);
        mRecyclerView.setLayoutManager(mLayoutManager);
        ...
        attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
}

这是实现之后的效果

1625307993508.gif

实现画廊效果首先我们要考虑的是,如何让ViewPager2同时显示多个页面Item

clipChildren
我们知道,在Android中,布局中的控件超出父布局的大小部分不会被绘制,但是当clipChildren设置为false时,子View的内容可以超出父布局被绘制出来。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mLlRoot"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/material_on_background_disabled"
    android:gravity="bottom"
    android:orientation="horizontal">

    <LinearLayout
        android:id="@+id/mLlFather"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:background="@color/black"
        android:gravity="bottom">

        <ImageView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />

        <ImageView
            android:layout_width="0dp"
            android:layout_height="100dp"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />

        <ImageView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />
    </LinearLayout>
</LinearLayout>

当前没有设置根布局LinearLayout(mLlRoot) 的clipChildren属性,黑色部分为ImageView的父布局,clipChildren默认为true,界面的效果为:


image-20210703184907145.png

可以看出,中间ImageView限制在了它的父布局中,此时我们修改clipChildren为false

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mLlRoot"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/material_on_background_disabled"
    android:gravity="bottom"
    android:clipChildren="false"
    android:orientation="horizontal">

    <LinearLayout
        android:id="@+id/mLlFather"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:background="@color/black"
        android:gravity="bottom">

        <ImageView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />

        <ImageView
            android:layout_width="0dp"
            android:layout_height="100dp"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />

        <ImageView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:src="@mipmap/ic_launcher" />
    </LinearLayout>
</LinearLayout>

界面效果为:

image-20210703185043886.png

可以看出,ImageView超出了它的父布局绘制出了剩余的部分,由此如果一个ViewPager2要显示多个Item,我们可以这样,给ViewPager左边和右边设置一个margin、固定ViewPager大小,或者根据想要显示的Item个数动态计算ViewPager的大小,然后设置clipChildren=false,允许ViewPager中看不到的界面绘制出来。
image-20210703190257247.png

由此我将ViewPager2封装了一下,目的只是为了给ViewPager2套一层父布局,方便使用

class SuperViewPager : RelativeLayout {

    val mViewPager: ViewPager2 by lazy {
        findViewById<ViewPager2>(R.id.mViewPager)
    }

    //自己定义了一个比率,来调整画廊效果最左侧和最右侧占用的宽度
    var edgeRatio = 0.3
        set(value) {
            field = value
            refreshPageSize()
        }

    //为了保证画廊效果,可见的Page处理为单数
    var visibleItem: Int = 1
        set(value) {
            field = if (value.rem(2) == 0) {
                value - 1
            } else {
                value
            }
            refreshPageSize()
        }


    //刷新页面大小
    private fun refreshPageSize() {
        //使用post为了保证获取根布局width的时候结果不为0
        mViewPager.post {
            mViewPager.offscreenPageLimit = visibleItem

            //根据想要显示的页面个数,动态给ViewPager2计算一个大小
            val mPageWidth = if (visibleItem == 1) {
                width
            } else {
                width.toDouble().div(visibleItem.minus(2).plus(edgeRatio)).toInt()
            }
            mViewPager.layoutParams = LayoutParams(
                LayoutParams(
                    mPageWidth,
                    ViewGroup.LayoutParams.MATCH_PARENT
                ).apply { gravity = Gravity.CENTER })
        }
    }
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    init {
        clipChildren=false
        LayoutInflater.from(context).inflate(R.layout.super_viewpager_layout, this, true)
    }

    /**
     * 为ViewPager2设置一个适配器,ViewPager2的适配器不再是PagerAdapter,而是RecyclerView.Adapter类型
     */
    fun setAdapter(adapter: RecyclerView.Adapter<*>) {
        mViewPager.adapter = adapter
    }

    /**
     * 设置页面切换的效果
     */
    fun setPageTransformer(pageTransformer: ViewPager2.PageTransformer) {
        mViewPager.setPageTransformer(pageTransformer)
    }
}

然后我们要为ViewPager2设置一个适配器,因为我这里是用Fragment作为单页内容来实现的多页面效果

class HomePagerAdapter(fragmentActivity: FragmentActivity) :
    FragmentStateAdapter(fragmentActivity) {
    override fun getItemCount(): Int {
        return 3
    }

    override fun createFragment(position: Int): Fragment {
        return SimpleFragment()
    }

}

关于ViewPager以及Adapter的正确使用方式,这里推荐看一下鸿神的一篇博客,讲的很详细:https://mp.weixin.qq.com/s/MOWdbI5IREjQP1Px-WJY1Q
最后在Activity中使用xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.utils.core.weight.viewpager.SuperViewPager
        android:id="@+id/mSuperViewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black"
        android:clipChildren="true" />
</LinearLayout>

onCreate中调用

   mSuperViewPager.visibleItem = 3
   mSuperViewPager.setAdapter(HomePagerAdapter(this))

我们就得到了这样的效果:step1

1625312004978.gif

其次,我们需要设置每个页面Item的间距,ViewPager2和ViewPager不同,ViewPager使用setPageMargin,但是因为ViewPager2内部是RecyclerView,有类似addItemDecoration的功能,我们添加自带的MarginPageTransformer

        mSuperViewPager.setPageTransformer(MarginPageTransformer(20))
        mSuperViewPager.visibleItem = 3
        mSuperViewPager.setAdapter(HomePagerAdapter(this))

就实现了这样的效果:step2

1625313367895.gif

然后我们还要为ViewPager2添加一个画廊缩放的效果,ViewPager2的页面切换效果是通过PageTransformer实现的

  public interface PageTransformer {

        /**
         * Apply a property transformation to the given page.
         *
         * @param page 当前页的View
         * @param 代表当前页面值和一个滑动距离的数值,在当前手机屏幕能看到的页面永远为0,往左递减,往右递增
         */
        void transformPage(@NonNull View page, float position);
    }

由此,我们实现PageTransformer,除去position=0(当前页面),其他页面设置一个默认效果,透明度0.5,缩放0.9,然后为页面由非0到0,以及0到非0设置一个过渡。

class GalleryTransformer : ViewPager2.PageTransformer {
    companion object {
        private const val TARGET_ALPHA = 0.5f
        private const val TARGET_SCALE = 0.8f
    }
    override fun transformPage(page: View, position: Float) {
        if (position < -1 || position > 1) {
            //当前页面左侧以及右侧的页面效果
            page.alpha = TARGET_ALPHA
            page.scaleX = TARGET_SCALE
            page.scaleY = TARGET_SCALE
        } else {
            //从不可见变为可见效果

            //透明度效果
            if (position <= 0) {
                page.alpha =
                    TARGET_ALPHA + TARGET_ALPHA * (1 + position)
            } else {
                page.alpha =
                    TARGET_ALPHA + TARGET_ALPHA * (1 - position)
            }
            //缩放效果
            val scale = Math.max(TARGET_SCALE, 1 - Math.abs(position))
            page.scaleX = scale
            page.scaleY = scale
        }
    }
}

最后在Activity设置PageTransformer,目前我们已经为ViewPager2设置过一个PageTransformer了,ViewPager2为我们提供了CompositePageTransformer,可以同时设置多个PageTransformer如下:

        mSuperViewPager.setPageTransformer(CompositePageTransformer().apply {
            addTransformer(
                GalleryTransformer()
            )
            addTransformer(MarginPageTransformer(20))
        })

最后就实现了如下效果:step3

1625315177308.gif

目前我们看似完成了期望效果,但目前有小伙伴应该发现因为我们设置了ViewPager的宽度是没有填满根布局的,过渡滑动的效果很影响美感,我们第一反应肯定实在xml中加入android:overScrollMode="never"

<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager2.widget.ViewPager2 xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mViewPager"
    android:clipChildren="false"
    android:layout_width="match_parent"
    android:overScrollMode="never"
    android:layout_height="match_parent">
</androidx.viewpager2.widget.ViewPager2>

再次运行效果如下:step4

1625315177308.gif

并没有解决这个问题,因为ViewPager2内部并没有对overScrollMode进行处理,并且内部使用RecyclerView实现的,RecyclerView是ViewPager2的第一个子View,由此我们在SuperViewPager中加入

val mViewPager: ViewPager2 by lazy {
        findViewById<ViewPager2>(R.id.mViewPager).apply {
            //设置关闭过度滑动的效果
            getChildAt(0).overScrollMode = View.OVER_SCROLL_NEVER
        }
    }

再次运行,过渡滑动的效果就被去除了:step5

1625307993508.gif

到这里,我们看似完成了一切的工作,但是目前有这样一个问题
image-20210703204012681.png

经过多次试验,我用这种方式解决了这个问题,讲跟布局的Touch事件直接传递给ViewPager中的RecyclerView,在SuperViewPager中添加

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        return mViewPager.getChildAt(0).onTouchEvent(event)
    }

到这,达到了我们期望的效果,下面是SuperViewPager完整代码

class SuperViewPager : RelativeLayout {

    val mViewPager: ViewPager2 by lazy {
        findViewById<ViewPager2>(R.id.mViewPager)
            .apply {
            //设置关闭过度滑动的效果
            getChildAt(0).overScrollMode = View.OVER_SCROLL_NEVER
        }
    }

    //自己定义了一个比率,来调整画廊效果最左侧和最右侧占用的宽度
    var edgeRatio = 0.3
        set(value) {
            field = value
            refreshPageSize()
        }

    //为了保证画廊效果,可见的Page处理为单数
    var visibleItem: Int = 1
        set(value) {
            field = if (value.rem(2) == 0) {
                value - 1
            } else {
                value
            }
            refreshPageSize()
        }


    //刷新页面大小
    private fun refreshPageSize() {
        //使用post为了保证获取根布局width的时候结果不为0
        mViewPager.post {
            mViewPager.offscreenPageLimit = visibleItem
            //根据想要显示的页面个数,动态给ViewPager2计算一个大小
            val mPageWidth = if (visibleItem == 1) {
                width
            } else {
                width.toDouble().div(visibleItem.minus(2).plus(edgeRatio)).toInt()
            }
            mViewPager.layoutParams = LayoutParams(
                LayoutParams(
                    mPageWidth,
                    ViewGroup.LayoutParams.MATCH_PARENT
                ).apply { gravity = Gravity.CENTER })
        }
    }

    /**
     * 将根布局的触摸事件直接传递给ViewPager
     */
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        return mViewPager.getChildAt(0).onTouchEvent(event)
    }

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    init {
        clipChildren=false
        LayoutInflater.from(context).inflate(R.layout.super_viewpager_layout, this, true)
    }

    /**
     * 为ViewPager2设置一个适配器,ViewPager2的适配器不再是PagerAdapter,而是RecyclerView.Adapter类型
     */
    fun setAdapter(adapter: RecyclerView.Adapter<*>) {
        mViewPager.adapter = adapter
    }

    /**
     * 设置页面切换的效果
     */
    fun setPageTransformer(pageTransformer: ViewPager2.PageTransformer) {
        mViewPager.setPageTransformer(pageTransformer)
    }
}

调用时

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.utils.core.weight.viewpager.SuperViewPager
        android:id="@+id/mSuperViewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black" />
</LinearLayout>
        mSuperViewPager.setPageTransformer(CompositePageTransformer().apply {
            addTransformer(
                GalleryTransformer()
            )
            addTransformer(MarginPageTransformer(20))
        })
        mSuperViewPager.visibleItem = 3
        mSuperViewPager.setAdapter(HomePagerAdapter(this))

遗留的问题

  • 有心的小伙伴可以发现,step1中,ViewPager2多页面的情况下,页面切换时,边缘的页面会出现闪动,目前还没发现什么原因。

  • 在SuperViewPager的layout布局中,我为ViewPager2设置了android:clipChildren="false",然后在初始化SuperViewPager,我为根布局也设置了clipChildren=false,我搜了下资料,因为ViewPager2 设置android:clipChildren="false"是为了使得内部的View突破限制显示,根布局再设置一次是为了承载页面的ViewPager2 能突破限制,所以要设置两次,但目前我在上面讲clipChildren的时候,根LinearLayout嵌套了一个子LinearLayout,在子LinearLayout中添加的ImageView,我只在根LinearLayout设置了android:clipChildren="false",就实现了我想要的效果,不知道这里是为何,是因为ViewPager2 内部是RecyclerView吗?

  • 在处理多页面边缘手势事件时,我一开始使用的方法是

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        return mViewPager.dispatchTouchEvent(event)
    }

将事件分发给内部的ViewPager,但是出现一个问题


image-20210703210509548.png

我又仔细看了一次View,ViewGroup的事件分发机制的,但是按理说左边已经响应的话,右边也应该响应,由于Android 11 的API ViewGroup这块 dispatchTouchEvent内容有点多,打断点由于使用的API和手机版本不同也没找到原因。有没有小伙伴清楚这个问题出现的原因能够分享一下

小弟是第一次写技术分享,也是第一次使用ViewPager2,目前工作正好有画廊效果的需求,虽然网上有许多第三方例如DiscreteScrollView,MZBannerView,但是自己想用ViewPager2实现一次,有不足或者错误的地方还请各位大佬能够指出来,另外遗留的问题如果由大佬知道的话,希望可以分享一下,小弟先谢谢啦!

我的邮箱 546956906@qq.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容