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閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容