说明:本文只把思想代码贴出来,有些东西需要根据跟人要求(布局要求、拓展功能等)去完善
功能说明:
1.通过调用DeepSeek接口获取问答结果内容,返回结果是流式输出(例如:<你>、<好>),获取到数据后向item中的TextView控件中持续append追加显示内容
2.在获取回答内容过程中,列表支持上下滑动查看上边的历史对话内容,当滑动到列表最后position位置后,列表自动滚动到底部显示最新内容
3.在获取回答内容过程中,支持局部更新(只更新TextView中显示的内容)item布局中其他控件不再频繁设置更新(简单点儿:不会出现闪屏情况)
了解功能点后,直接上硬货
无脑复制类代码
1.自定义的Application中
DeepSeek是https请求所以需要加以下代码
//忽略https的证书校验
public static void handleSSLHandshake(){
try {
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}};
SSLContext sc = SSLContext.getInstance("TLS");
// trustAllCerts信任所有的证书
sc.init(null, trustAllCerts, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
} catch (Exception e) {
System.out.println("MainApplication:::"+e.fillInStackTrace());
}
}
2.DataBoundViewHolder
import androidx.databinding.ViewDataBinding;
import androidx.recyclerview.widget.RecyclerView;
public class DataBoundViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
public final T binding;
DataBoundViewHolder(T binding) {
super(binding.getRoot());
this.binding = binding;
}
}
3.DataBoundListAdapter2
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.databinding.ViewDataBinding;
import androidx.recyclerview.widget.RecyclerView;
import com.zbxh.chat.model.ChatMessage;
import java.util.List;
public abstract class DataBoundListAdapter2<V extends ViewDataBinding>
extends RecyclerView.Adapter< DataBoundViewHolder<V>> {
List<ChatMessage> list;
protected DataBoundListAdapter2(List<ChatMessage> list) {
this.list = list;
}
@NonNull
@Override
public DataBoundViewHolder<V> onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
V binding = createBinding(parent, viewType);
return new DataBoundViewHolder<>(binding);
}
public ChatMessage getItemData(int position){
return list.get(position);
}
public List<ChatMessage> getListData(){
return list;
}
public void setListData(List<ChatMessage> list){
this.list.clear();
this.list.addAll(list);
}
protected abstract V createBinding(ViewGroup parent, int viewType);
@Override
public void onBindViewHolder(@NonNull DataBoundViewHolder<V> holder, int position) {
// bind(holder.binding, getItem(position),position);
// holder.binding.executePendingBindings();
}
@Override
public final void onBindViewHolder(@NonNull DataBoundViewHolder<V> holder, int position, @NonNull List<Object> payloads) {
//payloads标签用来判断是否是局部更新
bind(holder.binding,list.get(position),position,payloads);
holder.binding.executePendingBindings();
}
protected abstract void bind(V binding, ChatMessage item, int pos,List<Object> payloads);
}
4.MessageListAdapter2(我的item布局控件用的是binding获取的,为了省事儿)
public class MessageListAdapter2 extends DataBoundListAdapter2<ViewDataBinding>{
public static String PAYLOAD_UPDATE_TEXT = "Update_Text";
private final ChatFragment mFragment;
List<ChatMessage> list;
public MessageListAdapter2(ChatFragment fragment, List<ChatMessage> list) {
super(list);
mFragment = fragment;
this.list = list;
}
@Override
protected ViewDataBinding createBinding(ViewGroup parent, int viewType) {
//自己完善,viewType指的是对话布局类型(一般是两种:1.用户说的话、2.返回的答案内容<有多种类型:图片、文字等>)
}
@Override
public int getItemViewType(int position) {
//根据数据自己判断需要使用哪种类型的布局
}
@Override
public int getItemCount() {
return getListData().size();
}
@Override
protected void bind(ViewDataBinding binding, ChatMessage item, int pos, List<Object> payloads) {
//重点
if (payloads.isEmpty()) {
//初始化布局控件及配置所需控件的点击事件等
//例如:
TextView text = binding.text;
text.setText(item.content);
}else{
for (Object obj: payloads){
if (obj.equals(PAYLOAD_UPDATE_TEXT)){
//向TextView中追加内容
TextView text = binding.text;
text.append(item.content);
return;
}
}
}
}
//拓展功能:如果TextView有展开、收起功能,收起不是完全隐藏,而是显示固定lins行数数据,则收起后TextView可以上下滑动,不影响列表的上下滑动
// 关键:监听 TextView 的触摸事件
private void setTextViewTouch(TextView textView){
startY = 0f;
isDragging = false;
textView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
// 记录按下的 Y 坐标
startY = event.getY();
isDragging = false;
// 告诉父容器(RecyclerView)不要拦截 ACTION_DOWN,让 TextView 有机会处理
v.getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
// 计算 Y 方向的移动距离
float deltaY = Math.abs(event.getY() - startY);
// 计算 X 方向的移动距离 (使用 rawX 更准确,或者用 event.getX())
float deltaX = Math.abs(event.getX() - ((int) startY != 0 ? 0 : 0)); // 注意:这里 startX 未定义,我们用 event.getX() 的变化
// 更简单的做法是比较 deltaY 和一个阈值,并假设主要是垂直滑动
if (deltaY > 10) { // 简化:只判断 Y 方向移动是否超过阈值
isDragging = true;
// 明确告诉父容器不要拦截,让 TextView 处理垂直滚动
v.getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 手指抬起或取消,重置状态
isDragging = false;
// 恢复父容器的事件拦截权限
v.getParent().requestDisallowInterceptTouchEvent(false);
break;
}
// 将触摸事件继续传递给 TextView 的 MovementMethod 处理
textView.onTouchEvent(event);
return true; // 消费此事件,非常重要!
}
});
}
}
5.ScrollSpeedLinearLayoutManger
import android.content.Context;
import android.graphics.PointF;
import android.util.DisplayMetrics;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
/**
* 平滑滚动RecycleView Layout
*/
public class ScrollSpeedLinearLayoutManger extends LinearLayoutManager {
private float MILLISECONDS_PER_INCH = 0.03f;
private Context context;
public ScrollSpeedLinearLayoutManger(Context context) {
super(context);
this.context = context;
}
@Override
protected boolean isLayoutRTL() {
return true;
}
@Override
public boolean supportsPredictiveItemAnimations() {
return false;
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
LinearSmoothScroller linearSmoothScroller =
new LinearSmoothScroller(recyclerView.getContext()) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return ScrollSpeedLinearLayoutManger.this
.computeScrollVectorForPosition(targetPosition);
}
//This returns the milliseconds it takes to
//scroll one pixel.
@Override
protected float calculateSpeedPerPixel
(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.density;
//返回滑动一个pixel需要多少毫秒
}
};
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
public void setSpeedSlow() {
//自己在这里用density去乘,希望不同分辨率设备上滑动速度相同
//0.3f是自己估摸的一个值,可以根据不同需求自己修改
MILLISECONDS_PER_INCH = context.getResources().getDisplayMetrics().density * 0.3f;
}
public void setSpeedFast() {
MILLISECONDS_PER_INCH = context.getResources().getDisplayMetrics().density * 0.03f;
}
}
6.网络请求
调用DeepSeek接口获取答案(流式)<kotlin代码>想用Java自己转一下就行
class HttpRequest{
val BASE_URL = "https://api.siliconflow.cn/v1/chat/completions"
val API_KEY = "自己申请的API_Key"
val model = "Pro/deepseek-ai/DeepSeek-R1"
private var currentCall: Call? = null // 保存对当前请求的引用
var isEnd = false//是否结束
fun netWorkChat(question: String): String?{
Question = question
// 使用 Kotlin 字符串模板构建 JSON
val json = """
{
"model": "$model",
"messages": [
{"role": "user", "content": "$question"}
],
"stream": true
}
""".trimIndent()//"stream": true不加这句返回的数据是完整数据,加上就是流式数据
val client = OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS) // 连接超时
.writeTimeout(60, TimeUnit.SECONDS) // 写入超时
.readTimeout(300, TimeUnit.SECONDS) // 读取超时 - 这是关键!根据需要调整(时间尽量长一些)
.callTimeout(500, TimeUnit.SECONDS) // 总调用超时 (OkHttp 4.0+)(时间尽量长一些)
.build()
val mediaType = "application/json; charset=utf-8".toMediaType();
val body = RequestBody.create(mediaType, json)
val request = Request.Builder()
.url(BASE_URL)
.post(body)
.addHeader("Authorization", "Bearer $API_KEY")
.build()
isEnd = false
// 异步请求
currentCall = client.newCall(request)
currentCall?.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("Request onFailure: ${e.printStackTrace()}")
if (listener!=null)
listener?.getNetWorkChatData(null)
}
override fun onResponse(call: Call, response: Response) {
try {
if (response.isSuccessful) {
val inputStream = response.body?.byteStream()
//创建BufferedReader
val reader = BufferedReader(InputStreamReader(inputStream))
var line: String?
var typeS:Int = -1
//接收返回的流式数据
while (reader.readLine().also { line = it } != null&&!isEnd) {
Logger.d("line===netWork="+line)
//DeepSeek结束标签数据: data: [DONE]
if (line!!.isNotEmpty()){
if (listener!=null)
listener?.getNetWorkChatData(line)
if (line!=null&&line!!.contains("[DONE]")){
inputStream!!.close()
break
}
}
}
//val responseBody = response.body?.string()
//println("联网搜索结果:$responseBody")
// 注意:这里是在后台线程中,更新 UI 需要切回主线程
// 例如在 Android 中使用 Handler 或 Coroutine 的 main dispatcher
} else {
println("Request failed with code: ${response.code}")
if (listener!=null)
listener?.getNetWorkChatData(null)
}
} catch (e: IOException) {
println("Request failed with onResponse: ${e.printStackTrace()}")
if (listener!=null)
listener?.getNetWorkChatData(null)
} finally {
response.close()
}
}
})
return ""
}
fun closeNetWorkChat(): String{
isEnd = true
return Question
}
//数据监听
interface ChatListener{
fun getNetWorkChatData(data: String?)
}
var listener: ChatListener? = null
fun setChatListener(listener: ChatListener){
this.listener = listener
}
}
7.Fragment中使用
class ChatFragment{
boolean isScrollToFoot = true;
private boolean isUserScrolling = false; // 用户是否正在主动滑动
private RecyclerView chatList;//列表控件
ScrollSpeedLinearLayoutManger layoutManger;
MessageListAdapter2 mMsgAdapter;
List<ChatMessage> mInteractMessages = new ArrayList();
boolean isFrist = true;
private viod test(){
chatList = findViewById(R.id.chatList);
layoutManger = new ScrollSpeedLinearLayoutManger(getActivity());
layoutManger.setSpeedSlow();
chatList.setLayoutManager(layoutManger);
list = new ArrayList<>();
mMsgAdapter = new MessageListAdapter2(this,list);
chatList.setAdapter(mMsgAdapter);
mMsgAdapter.setRecyclerView(chatList);
chatList.setClipChildren(true);
chatList.setVerticalScrollBarEnabled(true);
//RecyclerView的滑动监听
chatList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//判断用户手指或者惯性在滑动
isUserScrolling = (newState == RecyclerView.SCROLL_STATE_DRAGGING ||
newState == RecyclerView.SCROLL_STATE_SETTLING);
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (isUserScrolling){
//随着上下滑动,实时更新当前是否处于列表底部
updateScrollState();
}
}
});
new Thread(){
@Override
public void run() {
super.run();
HttpRequest.netWorkChat("今天天气怎么样")
}
}.start();
ChatRepository.setChatListener(new ChatRepository.ChatListener() {
@Override
public void getNetWorkChatData(@Nullable String data) {
//向列表中添加、追加数据
if(isFrist){
isFrist = false;
ChatMessage chat = new ChatMessage();
chat.content = data;
chat.isupdate = false;
mInteractMessages.addd(chat);
}else{
ChatMessage chat = mInteractMessages.get(mInteractMessages.size()-1);
chat.content = data;
chat.isupdate = true;
}
mMsgAdapter.setListData(mInteractMessages);
goBottom()
}
})
}
private void goBottom(){
if (mMsgAdapter.getItemCount() > 1) {
if (height==0)
height = getRecyclerViewHeight(mChatBinding.chatList);
int i = getItemHeight(mChatBinding.chatList,mMsgAdapter.getItemCount()-1);
int n = 0;
if (i!=0)
n = i - height;
if (isScrollToFoot){
layoutManger.scrollToPositionWithOffset(mMsgAdapter.getItemCount() - 1, -n);
}else{
if (!isUserScrolling&&updateScrollState2()){
layoutManger.scrollToPositionWithOffset(mMsgAdapter.getItemCount() - 1, -n);
}
}
int pos = messages.size() - 1;
ChatMessage msg = messages.get(pos);
if (msg!=null&& msg.isUpdate) { // 假设你有这个标志
// 👉 关键:使用 payload 局部刷新
mMsgAdapter.notifyItemChanged(pos, MessageListAdapter.PAYLOAD_UPDATE_TEXT);
Logger.d("更新部分==="+pos);
} else {
// 否则全量刷新
mMsgAdapter.notifyItemChanged(pos);
Logger.d("更新整条==="+pos);
}
}
mChatBinding.executePendingBindings();
}
//是否到列表底部(真实的底部,最后显示的item是列表最后一条,并且该item内容全部显示了)
private void updateScrollState() {
int lastVisibleItemPosition = layoutManger.findLastVisibleItemPosition();
int totalItemCount = layoutManger.getItemCount();
// 关键修复1:使用 findLastVisibleItemPosition() 而不是 findLastCompletelyVisibleItemPosition()(用这个item太长则值为-1)
// 因为用户可能只看到最后一个item的一小部分,也应该视为“在底部”
// 当最后一个可见item是最后一个item时,认为在底部
isScrollToFoot = (lastVisibleItemPosition == totalItemCount - 1);
if (isScrollToFoot)
isScrollToFoot = isItemBottomAtRecyclerViewBottom(chatList,lastVisibleItemPosition);
Logger.d("是否到列表底部了==="+isScrollToFoot+"、"+lastVisibleItemPosition+"、"+(totalItemCount - 1));
}
//判断显示的最后item是否是列表最后position数据
private boolean updateScrollState2() {
int lastVisibleItemPosition = layoutManger.findLastVisibleItemPosition();
int totalItemCount = layoutManger.getItemCount();
return (lastVisibleItemPosition == totalItemCount - 1);
}
/**
* 判断指定 position 的 item 是否贴在 RecyclerView 的底部
* @param recyclerView RecyclerView 实例
* @param position 要检查的 item position
* @return true 表示该 item 的底部与 RecyclerView 的底部对齐(贴底)
*/
public boolean isItemBottomAtRecyclerViewBottom(RecyclerView recyclerView, int position) {
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (!(layoutManager instanceof LinearLayoutManager)) {
throw new IllegalArgumentException("仅支持 LinearLayoutManager");
}
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
// 1. 检查 item 是否可见
View itemView = linearLayoutManager.findViewByPosition(position);
if (itemView == null) {
return false; // item 不可见
}
// 2. 获取 RecyclerView 的可见区域底部(不包括 padding)
int recyclerViewBottom = recyclerView.getHeight()
- recyclerView.getPaddingBottom();
// 3. 获取 item 底部在 RecyclerView 坐标系中的 Y 坐标
int itemBottom = itemView.getBottom();
int height = itemView.getHeight();
Logger.d("底部比较==="+itemBottom+"、"+recyclerViewBottom);
// 4. 比较 item 底部是否与 RecyclerView 可见区域底部对齐
// 允许 1px 误差(防止浮点或滚动未完全停止导致的误差)
return Math.abs(itemBottom - recyclerViewBottom) <= 1;
}
//获取列表指定position内容高度
private int getItemHeight(RecyclerView recyclerView, int position){
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (!(layoutManager instanceof LinearLayoutManager)) {
throw new IllegalArgumentException("仅支持 LinearLayoutManager");
}
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
// 1. 检查 item 是否可见
View itemView = linearLayoutManager.findViewByPosition(position);
if (itemView == null) {
return 0; // item 不可见
}
int height = itemView.getHeight();
return height;
}
//获取RecyclerView控件高度
private int getRecyclerViewHeight(RecyclerView recyclerView){
int height = recyclerView.getHeight();
return height;
}
}
结语:只是把主体功能代码贴出来了,布局搭建和其他拓展功能(内容合成播放、复制、重新获取答案、点赞等等)根据需要自己添加即可,代码量太大就不一一贴了,有什么问题或想法可以相互交流,大佬多多指点!!!