需求
在项目中,我们常常遇到需要动态调整 ViewPager 的高度,以适应其内容大小的需求。默认情况下,ViewPager 的高度是固定的,无法根据每个页面的内容高度进行调整。这会导致在内容高度不一致时,出现不必要的空白区域或者内容被裁剪的情况。为了解决这个问题,我们设计了一个 AutoHeightViewPager,能够根据当前显示页面的内容高度动态调整自身的高度,保证内容完整且没有多余的空白。
效果
实现思路
1. 动态高度调整
首先,我们需要一个自定义的 ViewPager 类 AutoHeightViewPager,这个类可以根据当前页面的内容高度来动态调整自身的高度。通过重写 onMeasure 方法,可以在滑动过程中动态计算页面的高度并调整布局。
2. 监听页面滑动事件
为了平滑过渡,我们需要监听页面的滑动过程,并计算滑动比例,将当前页面的高度与下一个页面的高度按比例过渡,实现平滑过渡效果。
3. 自定义 Adapter
自定义的 PagerAdapter 必须实现一个接口 AutoHeightPager,用于获取指定位置页面的 View,这样我们可以测量页面内容的高度。
实现代码
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>
总结
通过自定义 ViewPager 的动态高度适配功能,我们可以解决内容高度不一致导致的布局问题。这种方案可以适应不同页面内容的高度变化,实现平滑的过渡效果,非常适用于动态内容展示的场景。