Android仿Telegram文本消息布局:预计算TextView行数解决列表跳动
解决Telegram风格消息布局的列表跳动问题
这问题我之前做聊天应用时也踩过坑,列表跳动本质是因为你用post()是等TextView绘制完成后才调整时间戳的显示状态,这时候RecyclerView已经计算好Item的高度了,突然改布局必然触发重绘导致跳动。咱们的核心目标是在TextView渲染前就计算出它的行数,提前决定时间戳的显示方式,从根源避免跳动。
方案一:用StaticLayout提前计算行数(最可靠)
StaticLayout是Android提供的可以模拟TextView排版逻辑的工具,能在不绘制视图的情况下,精准计算文本会占用多少行。
步骤1:封装计算逻辑
先写一个扩展函数来计算TextView的文本行数:
fun TextView.calculateLineCount(text: String): Int { val textPaint = this.paint.apply { textSize = this@calculateLineCount.textSize typeface = this@calculateLineCount.typeface } // 计算文本的最大可用宽度:要减去自身padding、时间戳宽度和间距 val timeStampWidth = (this.tag as? TextView)?.width ?: 0 val maxTextWidth = (this.parent as ViewGroup).width - this.paddingLeft - this.paddingRight - timeStampWidth - 8.dpToPx() // 时间戳与文本的间距 // 构建StaticLayout模拟排版 return StaticLayout.Builder.obtain(text, 0, text.length, textPaint, maxTextWidth) .setAlignment(Layout.Alignment.ALIGN_NORMAL) .setLineSpacing(this.lineSpacingExtra, this.lineSpacingMultiplier) .setIncludePad(this.includeFontPadding) .build() .lineCount } // 辅助工具:dp转px fun Int.dpToPx(): Int { return (this * Resources.getSystem().displayMetrics.density).toInt() }
步骤2:在绑定数据时提前判断
在RecyclerView的onBindViewHolder里,先计算行数再设置时间戳状态:
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) { val message = messages[position] val date = message.stamp.toString(Constants.DEFAULT_TIME_FORMAT) // 先给文本TextView赋值 holder.textViewItemMessageText.text = message.text // 把单行时间戳TextView存为tag,方便计算宽度 holder.textViewItemMessageText.tag = holder.textViewItemMessageTimeForSingleLine // 提前计算行数 val lineCount = holder.textViewItemMessageText.calculateLineCount(message.text) // 直接设置时间戳显示状态,无需等待绘制 if (lineCount > 1) { holder.textViewItemMessageTimeForSingleLine.hide() holder.textViewItemMessageTime.show() holder.textViewItemMessageTime.text = date } else { holder.textViewItemMessageTimeForSingleLine.show() holder.textViewItemMessageTime.hide() holder.textViewItemMessageTimeForSingleLine.text = date } }
注意点
- 如果是首次加载时父容器宽度还未确定,可以在
viewTreeObserver.addOnGlobalLayoutListener()里执行计算(比post()更早,还没到绘制阶段); - 可以把计算后的行数缓存到Message数据类里,避免每次绑定都重复计算,提升RecyclerView性能。
方案二:优化布局,用一个时间戳TextView实现(更简洁)
其实没必要用两个时间戳TextView,改用ConstraintLayout通过约束变化来控制时间戳位置,既能减少视图数量,又能让布局变化更平滑:
修改布局为ConstraintLayout
<android.support.v7.widget.CardView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end|center_vertical" android:layout_margin="4dp" android:layout_marginLeft="60dp" android:layout_marginRight="20dp" app:cardBackgroundColor="@color/thin_green" app:cardCornerRadius="4dp"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/rootLayout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dp"> <TextView android:id="@+id/textViewItemMessageText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:fontFamily="sans-serif" android:textColor="@color/black_light_87" android:textSize="14sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="My generation"/> <TextView android:id="@+id/textViewItemMessageTime" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="8dp" android:drawableLeft="@drawable/ic_read" android:fontFamily="sans-serif" android:gravity="center_vertical" android:textColor="@color/grass_two_54" android:textSize="12sp" app:layout_constraintStart_toEndOf="@id/textViewItemMessageText" app:layout_constraintTop_toTopOf="@id/textViewItemMessageText" tools:text="16:20"/> </androidx.constraintlayout.widget.ConstraintLayout> </android.support.v7.widget.CardView>
动态修改约束
同样用StaticLayout计算行数后,调整时间戳的约束:
val lineCount = holder.textViewItemMessageText.calculateLineCount(message.text) val constraintSet = ConstraintSet().apply { clone(holder.rootLayout) } if (lineCount > 1) { // 多行时:时间戳移到文本下方,靠右对齐 constraintSet.clear(holder.textViewItemMessageTime.id, ConstraintSet.START) constraintSet.connect( holder.textViewItemMessageTime.id, ConstraintSet.TOP, holder.textViewItemMessageText.id, ConstraintSet.BOTTOM, 2.dpToPx() ) constraintSet.connect( holder.textViewItemMessageTime.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END ) } else { // 单行时:时间戳回到文本右侧 constraintSet.connect( holder.textViewItemMessageTime.id, ConstraintSet.START, holder.textViewItemMessageText.id, ConstraintSet.END, 8.dpToPx() ) constraintSet.connect( holder.textViewItemMessageTime.id, ConstraintSet.TOP, holder.textViewItemMessageText.id, ConstraintSet.TOP ) constraintSet.clear(holder.textViewItemMessageTime.id, ConstraintSet.END) } // 应用约束变化,可添加平滑动画 constraintSet.applyTo(holder.rootLayout) holder.textViewItemMessageTime.text = date
这种方式只需要一个时间戳TextView,布局更简洁,约束变化还能通过TransitionManager添加动画,进一步消除视觉跳动。
内容的提问来源于stack exchange,提问作者Kota1921
相关产品推荐
相关产品推荐

