最近想試試用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());
}
這是實現之后的效果
實現畫廊效果首先我們要考慮的是,如何讓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,界面的效果為:
可以看出,中間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>
界面效果為:
可以看出,ImageView超出了它的父布局繪制出了剩余的部分,由此如果一個ViewPager2要顯示多個Item,我們可以這樣,給ViewPager左邊和右邊設置一個margin、固定ViewPager大小,或者根據想要顯示的Item個數動態計算ViewPager的大小,然后設置clipChildren=false,允許ViewPager中看不到的界面繪制出來。
由此我將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
其次,我們需要設置每個頁面Item的間距,ViewPager2和ViewPager不同,ViewPager使用setPageMargin,但是因為ViewPager2內部是RecyclerView,有類似addItemDecoration的功能,我們添加自帶的MarginPageTransformer
mSuperViewPager.setPageTransformer(MarginPageTransformer(20))
mSuperViewPager.visibleItem = 3
mSuperViewPager.setAdapter(HomePagerAdapter(this))
就實現了這樣的效果:step2
然后我們還要為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
目前我們看似完成了期望效果,但目前有小伙伴應該發現因為我們設置了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
并沒有解決這個問題,因為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
到這里,我們看似完成了一切的工作,但是目前有這樣一個問題
經過多次試驗,我用這種方式解決了這個問題,講跟布局的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,但是出現一個問題
我又仔細看了一次View,ViewGroup的事件分發機制的,但是按理說左邊已經響應的話,右邊也應該響應,由于Android 11 的API ViewGroup這塊 dispatchTouchEvent內容有點多,打斷點由于使用的API和手機版本不同也沒找到原因。有沒有小伙伴清楚這個問題出現的原因能夠分享一下