1. import android.animation.ValueAnimator
    2. import android.content.Context
    3. import android.os.Parcel
    4. import android.os.Parcelable
    5. import android.util.AttributeSet
    6. import android.util.Log
    7. import android.view.View
    8. import androidx.viewpager.widget.ViewPager
    9. /**
    10. * 支持 wrap_content 高度的 ViewPager - 完全自动化版本
    11. *
    12. * 核心特性:
    13. * 1. 自动识别当前显示的页面
    14. * 2. 自动测量当前页面的实际高度
    15. * 3. 页面切换时自动调整高度(带平滑动画)
    16. * 4. 外部使用零配置,只需在XML中声明即可
    17. *
    18. * 实现原理:
    19. * - 监听页面切换事件(OnPageChangeListener)
    20. * - 当页面滚动完成(SCROLL_STATE_IDLE)时,测量当前页面高度
    21. * - 使用ValueAnimator实现平滑的高度过渡动画(250ms)
    22. * - 只测量当前显示的页面,避免被预加载页面的高度影响
    23. */
    24. class WrapContentHeightViewPager @JvmOverloads constructor(
    25. context: Context,
    26. attrs: AttributeSet? = null
    27. ) : ViewPager(context, attrs) {
    28. companion object {
    29. private const val TAG = "WrapContentViewPager"
    30. private const val ANIMATION_DURATION = 250L // 高度动画时长(毫秒)
    31. }
    32. // 当前ViewPager的高度
    33. private var currentHeight = 0
    34. // 是否正在执行动画
    35. private var isAnimating = false
    36. // 高度动画对象
    37. private var heightAnimator: ValueAnimator? = null
    38. init {
    39. // 添加页面切换监听器
    40. addOnPageChangeListener(object : OnPageChangeListener {
    41. override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
    42. // 页面滚动中,不做处理
    43. }
    44. override fun onPageSelected(position: Int) {
    45. // 页面被选中,不做处理(等待滚动完成)
    46. Log.d(TAG, "页面选中: $position")
    47. }
    48. override fun onPageScrollStateChanged(state: Int) {
    49. when (state) {
    50. SCROLL_STATE_IDLE -> {
    51. // 滚动完成,调整高度
    52. Log.d(TAG, "滚动完成,准备调整高度")
    53. // 使用post确保布局已完成
    54. post {
    55. adjustHeightToCurrentPage()
    56. }
    57. }
    58. SCROLL_STATE_DRAGGING -> {
    59. // 用户开始拖动,取消正在进行的动画
    60. cancelHeightAnimation()
    61. }
    62. SCROLL_STATE_SETTLING -> {
    63. // 页面正在settling,不做处理
    64. }
    65. }
    66. }
    67. })
    68. }
    69. override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    70. val lp = layoutParams
    71. // 如果 layoutParams.height 被手动设置为具体值(> 0),优先使用该值
    72. // 这是动画过程中设置的高度
    73. val finalHeightMeasureSpec = if (lp != null && lp.height > 0) {
    74. MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY)
    75. } else {
    76. // 初始状态:测量当前页面的高度
    77. val currentPageHeight = measureCurrentPageHeight(widthMeasureSpec)
    78. if (currentPageHeight > 0) {
    79. // 初始化currentHeight
    80. if (currentHeight == 0) {
    81. currentHeight = currentPageHeight
    82. }
    83. MeasureSpec.makeMeasureSpec(currentPageHeight, MeasureSpec.EXACTLY)
    84. } else {
    85. // 如果测量失败,使用原始的heightMeasureSpec
    86. heightMeasureSpec
    87. }
    88. }
    89. // 使用计算出的高度重新测量
    90. super.onMeasure(widthMeasureSpec, finalHeightMeasureSpec)
    91. }
    92. /**
    93. * 测量当前页面的高度
    94. */
    95. private fun measureCurrentPageHeight(widthMeasureSpec: Int): Int {
    96. if (childCount == 0) {
    97. Log.w(TAG, "没有子视图,无法测量高度")
    98. return 0
    99. }
    100. // 找到当前显示的子视图
    101. val currentChild = findCurrentPageView()
    102. if (currentChild == null) {
    103. Log.w(TAG, "未找到当前页面的视图")
    104. return 0
    105. }
    106. // 测量子视图的高度
    107. currentChild.measure(
    108. widthMeasureSpec,
    109. MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
    110. )
    111. val measuredHeight = currentChild.measuredHeight
    112. Log.d(TAG, "测量当前页面高度: $measuredHeight px")
    113. return measuredHeight
    114. }
    115. /**
    116. * 找到当前显示的页面视图
    117. */
    118. private fun findCurrentPageView(): View? {
    119. if (childCount == 0) {
    120. return null
    121. }
    122. // 通过scrollX找到当前可见的child
    123. // ViewPager的每个child的宽度都等于ViewPager的宽度
    124. // scrollX表示当前滚动的位置,通过它可以计算出当前显示的是哪个child
    125. for (i in 0 until childCount) {
    126. val child = getChildAt(i)
    127. val childLeft = child.left
    128. val childRight = child.right
    129. // 当前页面应该在可见区域内
    130. // scrollX在child的left和right之间,说明这个child是当前显示的页面
    131. if (scrollX >= childLeft && scrollX < childRight) {
    132. Log.d(TAG, "找到当前页面视图: index=$i, scrollX=$scrollX, childLeft=$childLeft, childRight=$childRight")
    133. return child
    134. }
    135. }
    136. // 如果上面的方法失败(可能是初始化阶段,child还没有layout),
    137. // 尝试使用currentItem作为索引
    138. val currentPosition = currentItem
    139. if (currentPosition < childCount) {
    140. val child = getChildAt(currentPosition)
    141. Log.d(TAG, "使用currentItem索引找到页面: position=$currentPosition")
    142. return child
    143. }
    144. // 最后的fallback:返回第一个child
    145. Log.w(TAG, "使用fallback,返回第一个子视图")
    146. return getChildAt(0)
    147. }
    148. /**
    149. * 调整高度到当前页面
    150. */
    151. private fun adjustHeightToCurrentPage() {
    152. if (isAnimating) {
    153. Log.d(TAG, "动画进行中,跳过调整")
    154. return
    155. }
    156. val targetHeight = measureCurrentPageHeight(
    157. MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
    158. )
    159. if (targetHeight <= 0) {
    160. Log.w(TAG, "目标高度无效: $targetHeight,保持当前高度")
    161. return
    162. }
    163. // 如果高度没有变化,不执行动画
    164. if (targetHeight == currentHeight) {
    165. Log.d(TAG, "高度未变化: $currentHeight,无需调整")
    166. return
    167. }
    168. // 如果是第一次设置高度(currentHeight == 0),直接设置,不需要动画
    169. if (currentHeight == 0) {
    170. Log.d(TAG, "初始化高度(无动画): $targetHeight")
    171. val lp = layoutParams
    172. if (lp != null) {
    173. lp.height = targetHeight
    174. layoutParams = lp
    175. currentHeight = targetHeight
    176. }
    177. } else {
    178. // 后续的高度变化,使用动画
    179. Log.d(TAG, "开始高度动画: $currentHeight -> $targetHeight")
    180. animateHeight(currentHeight, targetHeight)
    181. }
    182. }
    183. /**
    184. * 执行高度动画
    185. */
    186. private fun animateHeight(fromHeight: Int, toHeight: Int) {
    187. // 取消之前的动画
    188. cancelHeightAnimation()
    189. heightAnimator = ValueAnimator.ofInt(fromHeight, toHeight).apply {
    190. duration = ANIMATION_DURATION
    191. addUpdateListener { animator ->
    192. val animatedHeight = animator.animatedValue as Int
    193. // 更新layoutParams的高度
    194. val lp = layoutParams
    195. if (lp != null) {
    196. lp.height = animatedHeight
    197. layoutParams = lp
    198. }
    199. }
    200. addListener(object : android.animation.AnimatorListenerAdapter() {
    201. override fun onAnimationStart(animation: android.animation.Animator) {
    202. isAnimating = true
    203. Log.d(TAG, "高度动画开始")
    204. }
    205. override fun onAnimationEnd(animation: android.animation.Animator) {
    206. isAnimating = false
    207. currentHeight = toHeight
    208. Log.d(TAG, "高度动画结束: currentHeight=$currentHeight")
    209. }
    210. override fun onAnimationCancel(animation: android.animation.Animator) {
    211. isAnimating = false
    212. Log.d(TAG, "高度动画取消")
    213. }
    214. })
    215. start()
    216. }
    217. }
    218. /**
    219. * 取消高度动画
    220. */
    221. private fun cancelHeightAnimation() {
    222. heightAnimator?.cancel()
    223. heightAnimator = null
    224. }
    225. /**
    226. * 公开方法:手动触发高度调整(用于初始化场景)
    227. * 外部可以调用此方法来立即调整到当前页面的高度
    228. */
    229. fun adjustHeightNow() {
    230. post {
    231. adjustHeightToCurrentPage()
    232. }
    233. }
    234. override fun onDetachedFromWindow() {
    235. super.onDetachedFromWindow()
    236. // 清理动画,避免内存泄漏
    237. cancelHeightAnimation()
    238. }
    239. // ==================== 状态保存和恢复 ====================
    240. override fun onSaveInstanceState(): Parcelable {
    241. val superState = super.onSaveInstanceState()
    242. val savedState = SavedState(superState)
    243. savedState.currentHeight = this.currentHeight
    244. Log.d(TAG, "保存状态: currentHeight=$currentHeight")
    245. return savedState
    246. }
    247. override fun onRestoreInstanceState(state: Parcelable?) {
    248. if (state is SavedState) {
    249. super.onRestoreInstanceState(state.superState)
    250. this.currentHeight = state.currentHeight
    251. Log.d(TAG, "恢复状态: currentHeight=$currentHeight")
    252. // 恢复后需要重新调整高度
    253. post {
    254. // 清除可能不正确的layoutParams.height
    255. val lp = layoutParams
    256. if (lp != null) {
    257. lp.height = -2 // WRAP_CONTENT
    258. layoutParams = lp
    259. }
    260. // 重新触发高度调整
    261. postDelayed({
    262. adjustHeightNow()
    263. }, 200)
    264. }
    265. } else {
    266. super.onRestoreInstanceState(state)
    267. }
    268. }
    269. /**
    270. * 保存ViewPager状态的类
    271. */
    272. private class SavedState : BaseSavedState {
    273. var currentHeight: Int = 0
    274. constructor(superState: Parcelable?) : super(superState)
    275. private constructor(parcel: Parcel) : super(parcel) {
    276. currentHeight = parcel.readInt()
    277. }
    278. override fun writeToParcel(out: Parcel, flags: Int) {
    279. super.writeToParcel(out, flags)
    280. out.writeInt(currentHeight)
    281. }
    282. companion object CREATOR : Parcelable.Creator<SavedState> {
    283. override fun createFromParcel(parcel: Parcel): SavedState {
    284. return SavedState(parcel)
    285. }
    286. override fun newArray(size: Int): Array<SavedState?> {
    287. return arrayOfNulls(size)
    288. }
    289. }
    290. }
    291. }