ViewPager2 使用說明書
零、Demo
如果對你有用,希望能給個 star,謝謝。
一、功能
官方關於使用 ViewPager2 建立滑動檢視的說明:
Swipe views allow you to navigate between sibling screens, such as tabs, with a horizontal finger gesture, or swipe. This navigation pattern is also referred to as horizontal paging. This topic teaches you how to create a tab layout with swipe views for switching between tabs, along with how to show a title strip instead of tabs.
大意是說,使用 ViewPager2 可以實現 Views 或頁面的水平方向(常用水平,垂直也支援)的滑動。也可以結合 Tab 元件使用。
二、基本使用
2.1 依賴引用
implementation "androidx.viewpager2:viewpager2:1.0.0"
2.2 版本說明
1.0.0 版本是 2019 年 11 月 20 日 更新的。
1.1.0-beta01 測試版本是 2021 年 8 月 4 日 ,最近才更新的。
具體的更新內容,和最新版本的資訊,可以在這個連結查到。
2.3 基本使用
ViewPager2 使用方式簡單。學習需要掌握以下幾個要素:XML 宣告、定義 Adapter 、設定滑動監聽。
2.3.1 XML 佈局中使用
<androidx.viewpager2.widget.ViewPager2
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
2.3.2 常用 Adapter 型別
ViewPager2.java 部分原始碼:
private void initialize(Context context, AttributeSet attrs) {
mAccessibilityProvider = sFeatureEnhancedA11yEnabled
? new PageAwareAccessibilityProvider()
: new BasicAccessibilityProvider();
mRecyclerView = new RecyclerViewImpl(context);
mRecyclerView.setId(ViewCompat.generateViewId());
mRecyclerView.setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
mLayoutManager = new LinearLayoutManagerImpl(context);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);
setOrientation(context, attrs);
mRecyclerView.setLayoutParams(
new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
mRecyclerView.addOnChildAttachStateChangeListener(enforceChildFillListener());
...
}
通過 ViewPager2 的原始碼,可以看到它內部維護了一個 RecyclerView,來實現列表檢視的顯示和滑動控制。
所以,RecyclerView.Adapter 可以直接用於 ViewPager2 ,這個應該是大家在使用 RecyclerView 元件時經常用到的。
另外,ViewPager2 的依賴包中,還提供了 FragmentStateAdapter ,繼承自RecyclerView.Adapter。主要用於 ViewPager2 和 Fragment 的結合使用。下文中會介紹如何使用。
2.3.3 滑動事件監聽
void registerOnPageChangeCallback(@NonNull OnPageChangeCallback callback)
通過該函式,註冊 Page 變化的事件監聽。
OnPageChangeCallback 類的原始碼如下:
public abstract static class OnPageChangeCallback {
/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
*
*
* @param position Position index of the first page currently being displayed.
* Page position+1 will be visible if positionOffset is nonzero.
* @param positionOffset Value from [0, 1) indicating the offset from the page at position.
* @param positionOffsetPixels Value in pixels indicating the offset from position.
*/
public void onPageScrolled(int position, float positionOffset,
@Px int positionOffsetPixels) {
}
/**
* This method will be invoked when a new page becomes selected. Animation is not
* necessarily complete.
*
* @param position Position index of the new selected page.
*/
public void onPageSelected(int position) {
}
/**
* Called when the scroll state changes. Useful for discovering when the user begins
* dragging, when a fake drag is started, when the pager is automatically settling to the
* current page, or when it is fully stopped/idle. {@code state} can be one of {@link
* #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
*/
public void onPageScrollStateChanged(@ScrollState int state) {
}
}
有三個回撥方法:
- onPageScrolled ,返回 Page 滑動位置偏移或畫素的變化。
- onPageSelected ,滑動後的 page position。
- onPageScrollStateChanged,滑動狀態的變化。
從一個 Page 滑動到另一個 Page ,Callback 的回撥情況:
2021-08-18 23:28:14.046 onPageSelected() called with: position = 0
2021-08-18 23:28:14.047 onPageScrolled() called with: position = 0, positionOffset = 0.0, positionOffsetPixels = 0
2021-08-18 23:28:37.333 onPageScrollStateChanged() called with: state = 1
2021-08-18 23:28:37.350 onPageScrolled() called with: position = 0, positionOffset = 0.016666668, positionOffsetPixels = 18
......
2021-08-18 23:28:37.427 onPageScrolled() called with: position = 0, positionOffset = 0.20277777, positionOffsetPixels = 219
2021-08-18 23:28:37.431 onPageScrollStateChanged() called with: state = 2
2021-08-18 23:28:37.450 onPageSelected() called with: position = 1
2021-08-18 23:28:37.451 onPageScrolled() called with: position = 0, positionOffset = 0.28611112, positionOffsetPixels = 309
......
2021-08-18 23:28:37.717 onPageScrolled() called with: position = 0, positionOffset = 0.99814814, positionOffsetPixels = 1078
2021-08-18 23:28:37.734 onPageScrolled() called with: position = 1, positionOffset = 0.0, positionOffsetPixels = 0
2021-08-18 23:28:37.734 onPageScrollStateChanged() called with: state = 0
onPageScrollStateChanged 滑動狀態 state:
SCROLL_STATE_IDLE = 0
SCROLL_STATE_DRAGGING = 1
SCROLL_STATE_SETTLING = 2
觀察日誌,回撥的特徵:
- 初次載入,會回撥 onPageSelected 和 onPageScrolled 方法,position 為 0。
- 向右滑動一頁時,首先觸發 onPageScrollStateChanged 回撥,state 為 SCROLL_STATE_DRAGGING 。然後 onPageScrolled 多次回撥,可以看到位置偏移量的變化。在 onPageScrolled 多次回撥中間,會回撥 onPageScrollStateChanged 方法,state 變為 SCROLL_STATE_SETTLING 位置固定。然後回撥 onPageSelected ,position 為滑動後的位置。
- 最後一次 onPageScrolled 回撥,position 變為 1。之後回撥 onPageScrollStateChanged ,state 變為 SCROLL_STATE_IDLE 。
三、使用方式
3.1 + View
3.1.1 效果演示
3.1.2 Adapter 程式碼實現
首先定義一個 item xml 佈局。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<TextView
android:id="@+id/tv_text"
android:background="@color/black"
android:layout_width="match_parent"
android:layout_height="280dp"
android:gravity="center"
android:textColor="#ffffff"
android:textSize="22sp" />
</LinearLayout>
自定義 ViewAdapter。
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class ViewAdapter : RecyclerView.Adapter<ViewAdapter.PagerViewHolder>() {
var data: List<Int> = ArrayList()
class PagerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val mTextView: TextView = itemView.findViewById(R.id.tv_text)
private val colors = arrayOf("#CCFF99", "#41F1E5", "#8D41F1", "#FF99CC")
fun bindData(i: Int) {
mTextView.text = i.toString()
mTextView.setBackgroundColor(Color.parseColor(colors[i]))
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagerViewHolder {
return PagerViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_page, parent, false))
}
override fun onBindViewHolder(holder: PagerViewHolder, position: Int) {
holder.bindData(position)
}
override fun getItemCount(): Int {
return data.size
}
}
3.1.3 ViewPager2 配置
在 Activity 或 Fragment 中的佈局檔案,直接使用 ViewPager2 標籤。
然後在程式碼中,配置 Adapter 便完成了,實現 3.1.1 中的效果。
val viewAdapter = ViewAdapter()
viewAdapter.data = listOf(1, 2, 3, 4)
viewPager2 = findViewById(R.id.view_pager)
viewPager2.apply {
adapter = viewAdapter
}
3.1.4 使用場景
Banner ,輪播廣告圖。可以配合定時器,進行定時滾動展示。
3.2 + Fragment
3.2.1 效果展示
整體效果,看上去與使用 RecyclerView.Aadapter + Views 的形式差不多。
前者在於區域性控制元件,和 Fragment 配合,更多是整頁的滑動。
這個例子中,增加了一些動畫效果,及一屏多頁的效果,在下文中會詳細說明。
3.2.2 Adapter 程式碼實現
ViewPager2 和 Fragment 配合使用, Adapter 前文也提到過,ViewPager2 的依賴包中,特別提供了 FragmentStateAdapter 。
使用起來比較方便,只用重寫兩個方法:getItemCount 和 createFragment。示例如下:
TestFragment 是用 AndroidStudio 生成的模板 BlankFragment ,並將兩個入參,分別設定為頁面的文字內容及背景顏色。
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class FragmentPagerAdapter(fragmentActivity: FragmentActivity) :
FragmentStateAdapter(fragmentActivity) {
override fun getItemCount(): Int {
return 4
}
private val colors = arrayOf("#CCFF99", "#41F1E5", "#8D41F1", "#FF99CC")
override fun createFragment(position: Int): Fragment {
return when (position) {
PAGE_MESSAGE -> TestFragment.newInstance("訊息$position", colors[0])
PAGE_CONTACT -> TestFragment.newInstance("通訊錄$position", colors[1])
PAGE_SETTING -> TestFragment.newInstance("設定$position", colors[2])
PAGE_MINE -> TestFragment.newInstance("我$position", colors[3])
else -> TestFragment.newInstance("$position", colors[0])
}
}
companion object {
const val PAGE_MESSAGE = 0
const val PAGE_CONTACT = 1
const val PAGE_SETTING = 2
const val PAGE_MINE = 3
}
}
3.2.3 ViewPager2 配置
使用方式簡單, 直接將 Adapter 例項化賦值給 ViewPager2 物件的 adapter 。
3.2.4 場景
頁面間的切換,支援手勢滑動。支援過渡的動畫。
還可以結合 Tab 元件,進行元件間的聯動。
3.3 + TabLayout
3.3.1 效果展示
3.3.2 程式碼實現
TabLayout 一般放在頁面的頂部位置。在頁面佈局的 xml 中,用 com.google.android.material.tabs.TabLayout 標籤宣告。
TabLayout 元件是 material 包中的元件,Android Studio 新建專案,會自動引入這個依賴。
如果是老版本的專案,想要引入 TabLayout 元件,可以引入以下依賴,具體版本以官網為準。
'com.google.android.material:material:1.3.0'
XML 中宣告:
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
程式碼中,通過 TabLayoutMediator 將 TabLayout 和 ViewPager2 進行繫結。
val tabLayout = findViewById<TabLayout>(R.id.tab_layout)
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
//設定標籤名稱
tab.text = "OBJECT ${(position + 1)}"
}.attach()
這樣就實現了,標籤導航和頁面滑動的聯動。
3.3.3 使用場景
一般可以用於類似新聞或者電商 ,各種類目頁面間的切換。
3.4 + BottomNavigationView
3.4.1 效果展示
3.4.2 程式碼實現
監聽 BottomNavigationView 的 setOnNavigationItemSelectedListener ,改變 ViewPager2 的 item。
監聽 ViewPager2 的 onPageSelected ,改變 BottomNavigationView 的 item。
注意兩元件的數量和位置要對應。
// 頁面介面卡
val fragmentPagerAdapter = FragmentPagerAdapter(this)
val viewPager2 = findViewById<ViewPager2>(R.id.viewPager2)
viewPager2.apply {
adapter = fragmentPagerAdapter
// 不提前載入
offscreenPageLimit = fragmentPagerAdapter.itemCount
// 單動畫效果
setPageTransformer(ScaleInTransformer())
}
// 底部選單
val bottomNavigation = findViewById<BottomNavigationView>(R.id.bottomNavigation)
// 設定監聽
bottomNavigation.setOnNavigationItemSelectedListener { menuItem ->
when (menuItem.itemId) {
//設定 viewPager2 item 位置
R.id.menu_messages -> {
viewPager2.setCurrentItem(0, true)
// 如返回 false ,bottomNavigation 被點選的 item ,不會被置為選中狀態
true
}
R.id.menu_contacts -> {
viewPager2.setCurrentItem(1, true)
true
}
R.id.menu_setting -> {
viewPager2.setCurrentItem(2, true)
true
}
R.id.menu_mine -> {
viewPager2.setCurrentItem(3, true)
true
}
else -> throw IllegalArgumentException("未設定的 menu position,請檢查引數")
}
}
// 監聽頁面變化
viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
// 設定 bottomNavigation item 狀態
bottomNavigation.menu.getItem(position).isChecked = true
}
})
3.4.3 使用場景
帶底部導航的多頁面導航 APP 。
3.5 其它
3.5.1 預載入
ViewPager2 的 offscreenPageLimit 屬性。
設定為 ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT ,不進行預載入和快取。
3.5.2 過渡效果
支援單效果和多效果設定,都是通過 setPageTransformer 函式。
單效果
直接傳入 ViewPager2.PageTransformer 的子類,即可以設定對應的效果。
ViewPager2 包中提供了 MarginPageTransformer 頁面邊距效果,可以直接在專案中使用。
也可以自定義實現 PageTransformer 。
組合效果
ViewPager2 包中的 CompositePageTransformer 類,該類中,維護了 List。
可以設定多種 PageTransformer 組合效果。
下面提供幾種效果的展示和程式碼,可以學習下自定義實現自己的效果:
1. 深度變化效果
import android.view.View
import androidx.viewpager2.widget.ViewPager2
private const val MIN_SCALE = 0.75f
class DepthPageTransformer : ViewPager2.PageTransformer {
override fun transformPage(view: View, position: Float) {
view.apply {
val pageWidth = width
when {
position < -1 -> { // [-Infinity,-1)
// This page is way off-screen to the left.
alpha = 0f
}
position <= 0 -> { // [-1,0]
// Use the default slide transition when moving to the left page
alpha = 1f
translationX = 0f
translationZ = 0f
scaleX = 1f
scaleY = 1f
}
position <= 1 -> { // (0,1]
// Fade the page out.
alpha = 1 - position
// Counteract the default slide transition
translationX = pageWidth * -position
// Move it behind the left page
translationZ = -1f
// Scale the page down (between MIN_SCALE and 1)
val scaleFactor = (MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position)))
scaleX = scaleFactor
scaleY = scaleFactor
}
else -> { // (1,+Infinity]
// This page is way off-screen to the right.
alpha = 0f
}
}
}
}
}
2. 比例放大進入效果
import android.view.View
import androidx.viewpager2.widget.ViewPager2
import java.lang.Math.abs
class ScaleInTransformer : ViewPager2.PageTransformer {
private val mMinScale = DEFAULT_MIN_SCALE
override fun transformPage(view: View, position: Float) {
view.elevation = -abs(position)
val pageWidth = view.width
val pageHeight = view.height
view.pivotY = (pageHeight / 2).toFloat()
view.pivotX = (pageWidth / 2).toFloat()
if (position < -1) {
view.scaleX = mMinScale
view.scaleY = mMinScale
view.pivotX = pageWidth.toFloat()
} else if (position <= 1) {
if (position < 0) {
val scaleFactor = (1 + position) * (1 - mMinScale) + mMinScale
view.scaleX = scaleFactor
view.scaleY = scaleFactor
view.pivotX = pageWidth * (DEFAULT_CENTER + DEFAULT_CENTER * -position)
} else {
val scaleFactor = (1 - position) * (1 - mMinScale) + mMinScale
view.scaleX = scaleFactor
view.scaleY = scaleFactor
view.pivotX = pageWidth * ((1 - position) * DEFAULT_CENTER)
}
} else {
view.pivotX = 0f
view.scaleX = mMinScale
view.scaleY = mMinScale
}
}
companion object {
const val DEFAULT_MIN_SCALE = 0.85f
const val DEFAULT_CENTER = 0.5f
}
}
3.縮放進入退出效果
private const val MIN_SCALE = 0.85f
private const val MIN_ALPHA = 0.5f
class ZoomOutPageTransformer : ViewPager2.PageTransformer {
override fun transformPage(view: View, position: Float) {
view.apply {
val pageWidth = width
val pageHeight = height
when {
position < -1 -> { // [-Infinity,-1)
// This page is way off-screen to the left.
alpha = 0f
}
position <= 1 -> { // [-1,1]
// Modify the default slide transition to shrink the page as well
val scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position))
val vertMargin = pageHeight * (1 - scaleFactor) / 2
val horzMargin = pageWidth * (1 - scaleFactor) / 2
translationX = if (position < 0) {
horzMargin - vertMargin / 2
} else {
horzMargin + vertMargin / 2
}
// Scale the page down (between MIN_SCALE and 1)
scaleX = scaleFactor
scaleY = scaleFactor
// Fade the page relative to its size.
alpha = (MIN_ALPHA +
(((scaleFactor - MIN_SCALE) / (1 - MIN_SCALE)) * (1 - MIN_ALPHA)))
}
else -> { // (1,+Infinity]
// This page is way off-screen to the right.
alpha = 0f
}
}
}
}
}
4. PageTransformer 頁面邊距效果
3.5.3 禁止手動滑動
viewPager2.isUserInputEnabled = true or false
3.5.4 模擬滑動
viewPager2.beginFakeDrag()
if (viewPager2.fakeDragBy(-300f)) {
viewPager2.endFakeDrag()
}
3.5.5 滑動方向
viewPager2.orientation = ViewPager2.ORIENTATION_VERTICAL or ViewPager2.ORIENTATION_HORIZONTAL
四、延伸擴充套件
- 如何自定義 PageTransformer
- ViewPager2 中 Fragment 的生命週期變化
- 更多使用場景擴充套件
五、參考資料
《還在用 ViewPager?是時候替換成 ViewPager2 了!》
感謝!
關注 autismbug,不寫 bug!