Android Studio Java 动态添加TextView实现聊天消息上推效果
问题描述
我开发了一款简易聊天原型应用,点击发送按钮即可动态创建新的消息TextView,当前运行效果参考截图:
当前布局逻辑存在异常:最早添加、底部边距值为32dp的消息显示在布局最底部,最新添加、底部边距值为560dp的消息反而显示在布局最顶部。我需要实现主流即时通讯App的聊天列表效果:最新发送的消息固定在输入框上方的底部位置,更早的历史消息会随着新消息发送自动向上推移,如何通过代码实现该效果?
注:提问者备注自身英语不流利,表述可能存在疏漏。
当前使用的实现代码如下:
button.setOnClickListener(new View.OnClickListener() { @SuppressLint("SetTextI18n") public void onClick(View v) { EditText editText = (EditText) findViewById(R.id.send_message); String message = editText.getText().toString(); editText.getText().clear(); ConstraintLayout layout = (ConstraintLayout) findViewById(R.id.mainConstraint); TextView view = new TextView(MainActivity.this); Button send_button = (Button) findViewById(R.id.send_button); view.setId(View.generateViewId()); ConstraintSet set = new ConstraintSet(); layout.addView(view,0); set.clone(layout); set.constrainHeight(view.getId(), ConstraintSet.WRAP_CONTENT); set.constrainWidth(view.getId(), ConstraintSet.WRAP_CONTENT); set.connect(view.getId(), ConstraintSet.BOTTOM, send_button.getId(), ConstraintSet.TOP); set.connect(view.getId(), ConstraintSet.END, layout.getId(), ConstraintSet.END); set.setMargin(view.getId(), ConstraintSet.END, dpToPx(16, MainActivity.this)); set.setMargin(view.getId(), ConstraintSet.BOTTOM, dpToPx(i[0], MainActivity.this)); view.setText("message with the id:" + i[0]); i[0] = i[0] + 48; view.setBackground(ContextCompat.getDrawable(MainActivity.this, R.drawable.sent_message_shape)); set.applyTo(layout); } });
解决方案
现有实现存在两个核心错误:
- 手动累加固定值底部边距的逻辑完全不可靠:不同消息文本长度、系统字体大小变化都会导致消息高度不固定,写死48dp的边距增量必然出现消息重叠、顺序错乱问题
- 直接在ConstraintLayout中堆叠子View、手动维护全量约束的开发成本极高,完全不适配聊天列表这类动态追加内容的场景
生产环境标准实现方案
直接将消息展示区域替换为RecyclerView,这是所有主流即时通讯App实现聊天列表的通用方案,不需要手动维护任何边距和约束关系:
- 将聊天消息区域单独设置为RecyclerView,放在输入框上方,配置
reverseLayout = true即可自动实现「最新消息贴底部、新消息插入后旧消息自动上推」的效果 - 新消息发送后直接向消息数据源尾部追加一条数据,调用适配器的
notifyItemInserted方法即可自动完成布局刷新 - 消息间距直接通过RecyclerView的ItemDecoration或者item布局的margin实现,不需要手动计算位置
原型场景快速修复方案(纯ConstraintLayout实现,不推荐正式项目使用)
放弃手动累加边距的逻辑,改为垂直链式约束布局:每次添加新消息时,将新消息的底部约束绑定到发送按钮顶部,再把上一条消息的底部约束绑定到新消息的顶部,按顺序堆叠即可。
修正后的核心代码参考:
// 定义Activity成员变量,记录上一条消息的View ID private int lastMsgViewId = View.NO_ID; private final int msgVerticalGap = dpToPx(8, this); // 消息之间的垂直间距 private final int msgBottomGap = dpToPx(16, this); // 最底部消息和输入框的间距 button.setOnClickListener(new View.OnClickListener() { @SuppressLint("SetTextI18n") public void onClick(View v) { EditText editText = (EditText) findViewById(R.id.send_message); String message = editText.getText().toString(); if (message.isEmpty()) return; editText.getText().clear(); ConstraintLayout layout = (ConstraintLayout) findViewById(R.id.mainConstraint); Button send_button = (Button) findViewById(R.id.send_button); // 初始化新消息View TextView newMsg = new TextView(MainActivity.this); newMsg.setId(View.generateViewId()); newMsg.setText(message); newMsg.setBackground(ContextCompat.getDrawable(MainActivity.this, R.drawable.sent_message_shape)); ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams( ConstraintLayout.LayoutParams.WRAP_CONTENT, ConstraintLayout.LayoutParams.WRAP_CONTENT ); newMsg.setLayoutParams(lp); layout.addView(newMsg); ConstraintSet set = new ConstraintSet(); set.clone(layout); // 约束新消息位置:靠右对齐、底部贴近输入框顶部 set.connect(newMsg.getId(), ConstraintSet.END, layout.getId(), ConstraintSet.END, dpToPx(16, MainActivity.this)); set.connect(newMsg.getId(), ConstraintSet.BOTTOM, send_button.getId(), ConstraintSet.TOP, msgBottomGap); // 存在历史消息时,将上一条消息的底部约束改绑定到新消息顶部 if (lastMsgViewId != View.NO_ID) { set.clear(lastMsgViewId, ConstraintSet.BOTTOM); set.connect(lastMsgViewId, ConstraintSet.BOTTOM, newMsg.getId(), ConstraintSet.TOP, msgVerticalGap); } set.applyTo(layout); // 更新上一条消息的ID记录 lastMsgViewId = newMsg.getId(); } });
注意:纯ConstraintLayout实现仅适合消息量极少的演示场景,消息量超过50条后会因为布局层级过深出现明显卡顿,正式开发必须使用RecyclerView方案。
内容的提问来源于stack exchange,提问作者atomtables
相关产品推荐
相关产品推荐

