Android使用ViewPager2中遇到的問題(使用ViewPager2實現(xiàn)畫廊效果)

最近想試試用ViewPager2來實現(xiàn)畫廊的效果,ViewPager2和ViewPager在API上有的地方不同,ViewPager2是通過內(nèi)部嵌套一個RecyclerView來實現(xiàn)的

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());
}

這是實現(xiàn)之后的效果

1625307993508.gif

實現(xiàn)畫廊效果首先我們要考慮的是,如何讓ViewPager2同時顯示多個頁面Item

clipChildren
我們知道,在Android中,布局中的控件超出父布局的大小部分不會被繪制,但是當(dāng)clipChildren設(shè)置為false時,子View的內(nèi)容可以超出父布局被繪制出來。

<?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>

當(dāng)前沒有設(shè)置根布局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左邊和右邊設(shè)置一個margin、固定ViewPager大小,或者根據(jù)想要顯示的Item個數(shù)動態(tài)計算ViewPager的大小,然后設(shè)置clipChildren=false,允許ViewPager中看不到的界面繪制出來。
image-20210703190257247.png

由此我將ViewPager2封裝了一下,目的只是為了給ViewPager2套一層父布局,方便使用

class SuperViewPager : RelativeLayout {

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

    //自己定義了一個比率,來調(diào)整畫廊效果最左側(cè)和最右側(cè)占用的寬度
    var edgeRatio = 0.3
        set(value) {
            field = value
            refreshPageSize()
        }

    //為了保證畫廊效果,可見的Page處理為單數(shù)
    var visibleItem: Int = 1
        set(value) {
            field = if (value.rem(2) == 0) {
                value - 1
            } else {
                value
            }
            refreshPageSize()
        }


    //刷新頁面大小
    private fun refreshPageSize() {
        //使用post為了保證獲取根布局width的時候結(jié)果不為0
        mViewPager.post {
            mViewPager.offscreenPageLimit = visibleItem

            //根據(jù)想要顯示的頁面?zhèn)€數(shù),動態(tài)給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設(shè)置一個適配器,ViewPager2的適配器不再是PagerAdapter,而是RecyclerView.Adapter類型
     */
    fun setAdapter(adapter: RecyclerView.Adapter<*>) {
        mViewPager.adapter = adapter
    }

    /**
     * 設(shè)置頁面切換的效果
     */
    fun setPageTransformer(pageTransformer: ViewPager2.PageTransformer) {
        mViewPager.setPageTransformer(pageTransformer)
    }
}

然后我們要為ViewPager2設(shè)置一個適配器,因為我這里是用Fragment作為單頁內(nèi)容來實現(xiàn)的多頁面效果

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

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

}

關(guān)于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中調(diào)用

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

我們就得到了這樣的效果:step1

1625312004978.gif

其次,我們需要設(shè)置每個頁面Item的間距,ViewPager2和ViewPager不同,ViewPager使用setPageMargin,但是因為ViewPager2內(nèi)部是RecyclerView,有類似addItemDecoration的功能,我們添加自帶的MarginPageTransformer

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

就實現(xiàn)了這樣的效果:step2

1625313367895.gif

然后我們還要為ViewPager2添加一個畫廊縮放的效果,ViewPager2的頁面切換效果是通過PageTransformer實現(xiàn)的

  public interface PageTransformer {

        /**
         * Apply a property transformation to the given page.
         *
         * @param page 當(dāng)前頁的View
         * @param 代表當(dāng)前頁面值和一個滑動距離的數(shù)值,在當(dāng)前手機屏幕能看到的頁面永遠為0,往左遞減,往右遞增
         */
        void transformPage(@NonNull View page, float position);
    }

由此,我們實現(xiàn)PageTransformer,除去position=0(當(dāng)前頁面),其他頁面設(shè)置一個默認效果,透明度0.5,縮放0.9,然后為頁面由非0到0,以及0到非0設(shè)置一個過渡。

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) {
            //當(dāng)前頁面左側(cè)以及右側(cè)的頁面效果
            page.alpha = TARGET_ALPHA
            page.scaleX = TARGET_SCALE
            page.scaleY = TARGET_SCALE
        } else {
            //從不可見變?yōu)榭梢娦Ч?
            //透明度效果
            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設(shè)置PageTransformer,目前我們已經(jīng)為ViewPager2設(shè)置過一個PageTransformer了,ViewPager2為我們提供了CompositePageTransformer,可以同時設(shè)置多個PageTransformer如下:

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

最后就實現(xiàn)了如下效果:step3

1625315177308.gif

目前我們看似完成了期望效果,但目前有小伙伴應(yīng)該發(fā)現(xiàn)因為我們設(shè)置了ViewPager的寬度是沒有填滿根布局的,過渡滑動的效果很影響美感,我們第一反應(yīng)肯定實在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內(nèi)部并沒有對overScrollMode進行處理,并且內(nèi)部使用RecyclerView實現(xiàn)的,RecyclerView是ViewPager2的第一個子View,由此我們在SuperViewPager中加入

val mViewPager: ViewPager2 by lazy {
        findViewById<ViewPager2>(R.id.mViewPager).apply {
            //設(shè)置關(guān)閉過度滑動的效果
            getChildAt(0).overScrollMode = View.OVER_SCROLL_NEVER
        }
    }

再次運行,過渡滑動的效果就被去除了:step5

1625307993508.gif

到這里,我們看似完成了一切的工作,但是目前有這樣一個問題
image-20210703204012681.png

經(jīng)過多次試驗,我用這種方式解決了這個問題,講跟布局的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 {
            //設(shè)置關(guān)閉過度滑動的效果
            getChildAt(0).overScrollMode = View.OVER_SCROLL_NEVER
        }
    }

    //自己定義了一個比率,來調(diào)整畫廊效果最左側(cè)和最右側(cè)占用的寬度
    var edgeRatio = 0.3
        set(value) {
            field = value
            refreshPageSize()
        }

    //為了保證畫廊效果,可見的Page處理為單數(shù)
    var visibleItem: Int = 1
        set(value) {
            field = if (value.rem(2) == 0) {
                value - 1
            } else {
                value
            }
            refreshPageSize()
        }


    //刷新頁面大小
    private fun refreshPageSize() {
        //使用post為了保證獲取根布局width的時候結(jié)果不為0
        mViewPager.post {
            mViewPager.offscreenPageLimit = visibleItem
            //根據(jù)想要顯示的頁面?zhèn)€數(shù),動態(tài)給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設(shè)置一個適配器,ViewPager2的適配器不再是PagerAdapter,而是RecyclerView.Adapter類型
     */
    fun setAdapter(adapter: RecyclerView.Adapter<*>) {
        mViewPager.adapter = adapter
    }

    /**
     * 設(shè)置頁面切換的效果
     */
    fun setPageTransformer(pageTransformer: ViewPager2.PageTransformer) {
        mViewPager.setPageTransformer(pageTransformer)
    }
}

調(diào)用時

<?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))

遺留的問題

  • 有心的小伙伴可以發(fā)現(xiàn),step1中,ViewPager2多頁面的情況下,頁面切換時,邊緣的頁面會出現(xiàn)閃動,目前還沒發(fā)現(xiàn)什么原因。

  • 在SuperViewPager的layout布局中,我為ViewPager2設(shè)置了android:clipChildren="false",然后在初始化SuperViewPager,我為根布局也設(shè)置了clipChildren=false,我搜了下資料,因為ViewPager2 設(shè)置android:clipChildren="false"是為了使得內(nèi)部的View突破限制顯示,根布局再設(shè)置一次是為了承載頁面的ViewPager2 能突破限制,所以要設(shè)置兩次,但目前我在上面講clipChildren的時候,根LinearLayout嵌套了一個子LinearLayout,在子LinearLayout中添加的ImageView,我只在根LinearLayout設(shè)置了android:clipChildren="false",就實現(xiàn)了我想要的效果,不知道這里是為何,是因為ViewPager2 內(nèi)部是RecyclerView嗎?

  • 在處理多頁面邊緣手勢事件時,我一開始使用的方法是

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

將事件分發(fā)給內(nèi)部的ViewPager,但是出現(xiàn)一個問題


image-20210703210509548.png

我又仔細看了一次View,ViewGroup的事件分發(fā)機制的,但是按理說左邊已經(jīng)響應(yīng)的話,右邊也應(yīng)該響應(yīng),由于Android 11 的API ViewGroup這塊 dispatchTouchEvent內(nèi)容有點多,打斷點由于使用的API和手機版本不同也沒找到原因。有沒有小伙伴清楚這個問題出現(xiàn)的原因能夠分享一下

小弟是第一次寫技術(shù)分享,也是第一次使用ViewPager2,目前工作正好有畫廊效果的需求,雖然網(wǎng)上有許多第三方例如DiscreteScrollView,MZBannerView,但是自己想用ViewPager2實現(xiàn)一次,有不足或者錯誤的地方還請各位大佬能夠指出來,另外遺留的問題如果由大佬知道的話,希望可以分享一下,小弟先謝謝啦!

我的郵箱 546956906@qq.com

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容