You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:09:19