需求
在項(xiàng)目中,我們常常遇到需要?jiǎng)討B(tài)調(diào)整 ViewPager 的高度,以適應(yīng)其內(nèi)容大小的需求。默認(rèn)情況下,ViewPager 的高度是固定的,無(wú)法根據(jù)每個(gè)頁(yè)面的內(nèi)容高度進(jìn)行調(diào)整。這會(huì)導(dǎo)致在內(nèi)容高度不一致時(shí),出現(xiàn)不必要的空白區(qū)域或者內(nèi)容被裁剪的情況。為了解決這個(gè)問(wèn)題,我們?cè)O(shè)計(jì)了一個(gè) AutoHeightViewPager,能夠根據(jù)當(dāng)前顯示頁(yè)面的內(nèi)容高度動(dòng)態(tài)調(diào)整自身的高度,保證內(nèi)容完整且沒(méi)有多余的空白。
效果
去哪兒效果:

仿的效果:

實(shí)現(xiàn)思路
1. 動(dòng)態(tài)高度調(diào)整
首先,我們需要一個(gè)自定義的 ViewPager 類 AutoHeightViewPager,這個(gè)類可以根據(jù)當(dāng)前頁(yè)面的內(nèi)容高度來(lái)動(dòng)態(tài)調(diào)整自身的高度。通過(guò)重寫(xiě) onMeasure 方法,可以在滑動(dòng)過(guò)程中動(dòng)態(tài)計(jì)算頁(yè)面的高度并調(diào)整布局。
2. 監(jiān)聽(tīng)頁(yè)面滑動(dòng)事件
為了平滑過(guò)渡,我們需要監(jiān)聽(tīng)頁(yè)面的滑動(dòng)過(guò)程,并計(jì)算滑動(dòng)比例,將當(dāng)前頁(yè)面的高度與下一個(gè)頁(yè)面的高度按比例過(guò)渡,實(shí)現(xiàn)平滑過(guò)渡效果。
3. 自定義 Adapter
自定義的 PagerAdapter 必須實(shí)現(xiàn)一個(gè)接口 AutoHeightPager,用于獲取指定位置頁(yè)面的 View,這樣我們可以測(cè)量頁(yè)面內(nèi)容的高度。
實(shí)現(xiàn)代碼
1. AutoHeightViewPager
package com.yxlh.androidxy.demo.ui.viewpager.vp
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager.widget.ViewPager
interface AutoHeightPager {
fun getView(position: Int): View?
}
class AutoHeightViewPager @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : ViewPager(context, attrs) {
private var lastWidthMeasureSpec: Int = 0
private var currentHeight: Int = 0
private var lastPosition: Int = 0
private var isScrolling: Boolean = false
init {
addOnPageChangeListener(object : SimpleOnPageChangeListener() {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
if (positionOffset == 0f) {
isScrolling = false
requestLayout()
return
}
val srcPosition = if (position >= lastPosition) position else position + 1
val destPosition = if (position >= lastPosition) position + 1 else position
val srcHeight = getViewHeight(srcPosition)
val destHeight = getViewHeight(destPosition)
currentHeight = (srcHeight + (destHeight - srcHeight) *
if (position >= lastPosition) positionOffset else 1 - positionOffset).toInt()
isScrolling = true
requestLayout()
}
override fun onPageScrollStateChanged(state: Int) {
if (state == SCROLL_STATE_IDLE) {
lastPosition = currentItem
}
}
})
}
override fun setAdapter(adapter: PagerAdapter?) {
require(adapter == null || adapter is AutoHeightPager) { "PagerAdapter must implement AutoHeightPager." }
super.setAdapter(adapter)
}
private fun getViewHeight(position: Int): Int {
val adapter = adapter as? AutoHeightPager ?: return 0
return run {
val view = adapter.getView(position) ?: return 0
view.measure(
lastWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
)
view.measuredHeight
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
lastWidthMeasureSpec = widthMeasureSpec
var heightSpec = heightMeasureSpec
if (isScrolling) {
heightSpec = MeasureSpec.makeMeasureSpec(currentHeight, MeasureSpec.EXACTLY)
} else {
getViewHeight(currentItem).takeIf { it > 0 }?.let {
heightSpec = MeasureSpec.makeMeasureSpec(it, MeasureSpec.EXACTLY)
}
}
super.onMeasure(widthMeasureSpec, heightSpec)
}
}
2. AutoHeightPagerAdapter
package com.yxlh.androidxy.demo.ui.viewpager
import android.view.View
import android.view.ViewGroup
import androidx.viewpager.widget.PagerAdapter
import com.yxlh.androidxy.demo.ui.viewpager.vp.AutoHeightPager
class AutoHeightPagerAdapter : PagerAdapter(), AutoHeightPager {
private val viewList = mutableListOf<View>()
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = viewList[position]
val parent = view.parent as? ViewGroup
parent?.removeView(view)
container.addView(view)
return view
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
fun setViews(views: List<View>) {
viewList.clear()
viewList.addAll(views)
notifyDataSetChanged()
}
override fun getView(position: Int): View? {
return viewList.getOrNull(position)
}
override fun getCount(): Int {
return viewList.size
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
}
3. Activity 代碼
package com.yxlh.androidxy.demo.ui.viewpager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.yxlh.androidxy.R
import com.yxlh.androidxy.demo.ui.viewpager.vp.AutoHeightViewPager
class VpActivity : AppCompatActivity() {
private var mAutoHeightVp: AutoHeightViewPager? = null
private var mAdapter: AutoHeightPagerAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_vp)
val viewList: MutableList<View> = ArrayList()
viewList.add(LayoutInflater.from(this).inflate(R.layout.view_demo_1, null))
viewList.add(LayoutInflater.from(this).inflate(R.layout.view_demo_2, null))
mAutoHeightVp = findViewById(R.id.viewpager)
mAutoHeightVp?.setAdapter(AutoHeightPagerAdapter().also { mAdapter = it })
mAdapter?.setViews(viewList)
mAutoHeightVp?.setCurrentItem(1)
}
}
4. 布局 XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.yxlh.androidxy.demo.ui.viewpager.vp.AutoHeightViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitXY"
android:src="@drawable/vp_content" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
總結(jié)
通過(guò)自定義 ViewPager 的動(dòng)態(tài)高度適配功能,我們可以解決內(nèi)容高度不一致導(dǎo)致的布局問(wèn)題。這種方案可以適應(yīng)不同頁(yè)面內(nèi)容的高度變化,實(shí)現(xiàn)平滑的過(guò)渡效果,非常適用于動(dòng)態(tài)內(nèi)容展示的場(chǎng)景。
github:github.com/yixiaolunhui/AndroidXY