import android.animation.ValueAnimatorimport android.content.Contextimport android.os.Parcelimport android.os.Parcelableimport android.util.AttributeSetimport android.util.Logimport android.view.Viewimport androidx.viewpager.widget.ViewPager/** * 支持 wrap_content 高度的 ViewPager - 完全自动化版本 * * 核心特性: * 1. 自动识别当前显示的页面 * 2. 自动测量当前页面的实际高度 * 3. 页面切换时自动调整高度(带平滑动画) * 4. 外部使用零配置,只需在XML中声明即可 * * 实现原理: * - 监听页面切换事件(OnPageChangeListener) * - 当页面滚动完成(SCROLL_STATE_IDLE)时,测量当前页面高度 * - 使用ValueAnimator实现平滑的高度过渡动画(250ms) * - 只测量当前显示的页面,避免被预加载页面的高度影响 */class WrapContentHeightViewPager @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null) : ViewPager(context, attrs) { companion object { private const val TAG = "WrapContentViewPager" private const val ANIMATION_DURATION = 250L // 高度动画时长(毫秒) } // 当前ViewPager的高度 private var currentHeight = 0 // 是否正在执行动画 private var isAnimating = false // 高度动画对象 private var heightAnimator: ValueAnimator? = null init { // 添加页面切换监听器 addOnPageChangeListener(object : OnPageChangeListener { override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { // 页面滚动中,不做处理 } override fun onPageSelected(position: Int) { // 页面被选中,不做处理(等待滚动完成) Log.d(TAG, "页面选中: $position") } override fun onPageScrollStateChanged(state: Int) { when (state) { SCROLL_STATE_IDLE -> { // 滚动完成,调整高度 Log.d(TAG, "滚动完成,准备调整高度") // 使用post确保布局已完成 post { adjustHeightToCurrentPage() } } SCROLL_STATE_DRAGGING -> { // 用户开始拖动,取消正在进行的动画 cancelHeightAnimation() } SCROLL_STATE_SETTLING -> { // 页面正在settling,不做处理 } } } }) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val lp = layoutParams // 如果 layoutParams.height 被手动设置为具体值(> 0),优先使用该值 // 这是动画过程中设置的高度 val finalHeightMeasureSpec = if (lp != null && lp.height > 0) { MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY) } else { // 初始状态:测量当前页面的高度 val currentPageHeight = measureCurrentPageHeight(widthMeasureSpec) if (currentPageHeight > 0) { // 初始化currentHeight if (currentHeight == 0) { currentHeight = currentPageHeight } MeasureSpec.makeMeasureSpec(currentPageHeight, MeasureSpec.EXACTLY) } else { // 如果测量失败,使用原始的heightMeasureSpec heightMeasureSpec } } // 使用计算出的高度重新测量 super.onMeasure(widthMeasureSpec, finalHeightMeasureSpec) } /** * 测量当前页面的高度 */ private fun measureCurrentPageHeight(widthMeasureSpec: Int): Int { if (childCount == 0) { Log.w(TAG, "没有子视图,无法测量高度") return 0 } // 找到当前显示的子视图 val currentChild = findCurrentPageView() if (currentChild == null) { Log.w(TAG, "未找到当前页面的视图") return 0 } // 测量子视图的高度 currentChild.measure( widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) ) val measuredHeight = currentChild.measuredHeight Log.d(TAG, "测量当前页面高度: $measuredHeight px") return measuredHeight } /** * 找到当前显示的页面视图 */ private fun findCurrentPageView(): View? { if (childCount == 0) { return null } // 通过scrollX找到当前可见的child // ViewPager的每个child的宽度都等于ViewPager的宽度 // scrollX表示当前滚动的位置,通过它可以计算出当前显示的是哪个child for (i in 0 until childCount) { val child = getChildAt(i) val childLeft = child.left val childRight = child.right // 当前页面应该在可见区域内 // scrollX在child的left和right之间,说明这个child是当前显示的页面 if (scrollX >= childLeft && scrollX < childRight) { Log.d(TAG, "找到当前页面视图: index=$i, scrollX=$scrollX, childLeft=$childLeft, childRight=$childRight") return child } } // 如果上面的方法失败(可能是初始化阶段,child还没有layout), // 尝试使用currentItem作为索引 val currentPosition = currentItem if (currentPosition < childCount) { val child = getChildAt(currentPosition) Log.d(TAG, "使用currentItem索引找到页面: position=$currentPosition") return child } // 最后的fallback:返回第一个child Log.w(TAG, "使用fallback,返回第一个子视图") return getChildAt(0) } /** * 调整高度到当前页面 */ private fun adjustHeightToCurrentPage() { if (isAnimating) { Log.d(TAG, "动画进行中,跳过调整") return } val targetHeight = measureCurrentPageHeight( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY) ) if (targetHeight <= 0) { Log.w(TAG, "目标高度无效: $targetHeight,保持当前高度") return } // 如果高度没有变化,不执行动画 if (targetHeight == currentHeight) { Log.d(TAG, "高度未变化: $currentHeight,无需调整") return } // 如果是第一次设置高度(currentHeight == 0),直接设置,不需要动画 if (currentHeight == 0) { Log.d(TAG, "初始化高度(无动画): $targetHeight") val lp = layoutParams if (lp != null) { lp.height = targetHeight layoutParams = lp currentHeight = targetHeight } } else { // 后续的高度变化,使用动画 Log.d(TAG, "开始高度动画: $currentHeight -> $targetHeight") animateHeight(currentHeight, targetHeight) } } /** * 执行高度动画 */ private fun animateHeight(fromHeight: Int, toHeight: Int) { // 取消之前的动画 cancelHeightAnimation() heightAnimator = ValueAnimator.ofInt(fromHeight, toHeight).apply { duration = ANIMATION_DURATION addUpdateListener { animator -> val animatedHeight = animator.animatedValue as Int // 更新layoutParams的高度 val lp = layoutParams if (lp != null) { lp.height = animatedHeight layoutParams = lp } } addListener(object : android.animation.AnimatorListenerAdapter() { override fun onAnimationStart(animation: android.animation.Animator) { isAnimating = true Log.d(TAG, "高度动画开始") } override fun onAnimationEnd(animation: android.animation.Animator) { isAnimating = false currentHeight = toHeight Log.d(TAG, "高度动画结束: currentHeight=$currentHeight") } override fun onAnimationCancel(animation: android.animation.Animator) { isAnimating = false Log.d(TAG, "高度动画取消") } }) start() } } /** * 取消高度动画 */ private fun cancelHeightAnimation() { heightAnimator?.cancel() heightAnimator = null } /** * 公开方法:手动触发高度调整(用于初始化场景) * 外部可以调用此方法来立即调整到当前页面的高度 */ fun adjustHeightNow() { post { adjustHeightToCurrentPage() } } override fun onDetachedFromWindow() { super.onDetachedFromWindow() // 清理动画,避免内存泄漏 cancelHeightAnimation() } // ==================== 状态保存和恢复 ==================== override fun onSaveInstanceState(): Parcelable { val superState = super.onSaveInstanceState() val savedState = SavedState(superState) savedState.currentHeight = this.currentHeight Log.d(TAG, "保存状态: currentHeight=$currentHeight") return savedState } override fun onRestoreInstanceState(state: Parcelable?) { if (state is SavedState) { super.onRestoreInstanceState(state.superState) this.currentHeight = state.currentHeight Log.d(TAG, "恢复状态: currentHeight=$currentHeight") // 恢复后需要重新调整高度 post { // 清除可能不正确的layoutParams.height val lp = layoutParams if (lp != null) { lp.height = -2 // WRAP_CONTENT layoutParams = lp } // 重新触发高度调整 postDelayed({ adjustHeightNow() }, 200) } } else { super.onRestoreInstanceState(state) } } /** * 保存ViewPager状态的类 */ private class SavedState : BaseSavedState { var currentHeight: Int = 0 constructor(superState: Parcelable?) : super(superState) private constructor(parcel: Parcel) : super(parcel) { currentHeight = parcel.readInt() } override fun writeToParcel(out: Parcel, flags: Int) { super.writeToParcel(out, flags) out.writeInt(currentHeight) } companion object CREATOR : Parcelable.Creator<SavedState> { override fun createFromParcel(parcel: Parcel): SavedState { return SavedState(parcel) } override fun newArray(size: Int): Array<SavedState?> { return arrayOfNulls(size) } } }}