Android中使用RecyclerView+DeepSeek实现聊天对话

发布于:2025-09-08 ⋅ 阅读:(17) ⋅ 点赞:(0)

说明:本文只把思想代码贴出来,有些东西需要根据跟人要求(布局要求、拓展功能等)去完善

功能说明:

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;
    }


}

结语:只是把主体功能代码贴出来了,布局搭建和其他拓展功能(内容合成播放、复制、重新获取答案、点赞等等)根据需要自己添加即可,代码量太大就不一一贴了,有什么问题或想法可以相互交流,大佬多多指点!!!


网站公告

今日签到

点亮在社区的每一天
去签到