Skip to content

Commit 9a362a8

Browse files
committed
[1.7.0-feature] 仿chatgpt交互
1 parent 60736f4 commit 9a362a8

25 files changed

+828
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.ninetripods.mq.study.activity
2+
3+
import android.os.Bundle
4+
import androidx.appcompat.app.AppCompatActivity
5+
import androidx.recyclerview.widget.LinearLayoutManager
6+
import androidx.recyclerview.widget.RecyclerView
7+
import org.ninetripods.mq.study.R
8+
import org.ninetripods.mq.study.base.adapter.BaseAdapter
9+
import org.ninetripods.mq.study.chatgpt.ChatVHolderFactory
10+
import org.ninetripods.mq.study.chatgpt.MessageModel
11+
import org.ninetripods.mq.study.kotlin.ktx.id
12+
13+
/**
14+
* 仿ChatGPT交互页面
15+
*/
16+
class ChatGptActivity : AppCompatActivity() {
17+
18+
private val mRv: RecyclerView by id(R.id.rv_view)
19+
private val chatAdapter by lazy { BaseAdapter<MessageModel>(ChatVHolderFactory()) }
20+
21+
override fun onCreate(savedInstanceState: Bundle?) {
22+
super.onCreate(savedInstanceState)
23+
setContentView(R.layout.activity_layout_rv)
24+
setRvInfo()
25+
}
26+
27+
private fun setRvInfo() {
28+
val list = mutableListOf<MessageModel>()
29+
list.add(MessageModel("1", "今天天气如何", type = ChatVHolderFactory.TYPE_ASK_TXT))
30+
list.add(MessageModel("1", "今天天气很棒", type = ChatVHolderFactory.TYPE_REPLY_TXT))
31+
list.add(MessageModel("1", "今天天气如何", type = ChatVHolderFactory.TYPE_REPLY_SPAN))
32+
for (i in 0..50) {
33+
list.add(MessageModel("1", "今天天气如何", type = ChatVHolderFactory.TYPE_ASK_TXT))
34+
}
35+
chatAdapter.submitList(list)
36+
mRv.layoutManager = LinearLayoutManager(this)
37+
mRv.adapter = chatAdapter
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.ninetripods.mq.study.base.adapter
2+
3+
import android.view.ViewGroup
4+
import androidx.recyclerview.widget.DiffUtil
5+
import androidx.recyclerview.widget.ListAdapter
6+
import androidx.recyclerview.widget.RecyclerView
7+
import org.ninetripods.mq.study.chatgpt.ChatDiffUtil
8+
9+
/**
10+
* Created by mq on 2023/7/20
11+
* RecyclerView.Adapter基类
12+
*/
13+
open class BaseAdapter<T : Any>(private val vhFactory: IVHFactory) :
14+
RecyclerView.Adapter<BaseVHolder<T>>() {
15+
private val models = mutableListOf<T>()
16+
17+
override fun getItemViewType(position: Int): Int {
18+
val model = models[position]
19+
if (model is IMultiType) return model.getItemViewType()
20+
return super.getItemViewType(position)
21+
}
22+
23+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseVHolder<T> {
24+
return vhFactory.getVH(parent.context, parent, viewType) as BaseVHolder<T>
25+
}
26+
27+
override fun getItemCount(): Int = models.size
28+
29+
override fun onBindViewHolder(holder: BaseVHolder<T>, position: Int) {
30+
holder.onBindViewHolder(models[position], position)
31+
}
32+
33+
fun submit(item: T) {
34+
35+
}
36+
37+
fun submitList(newList: List<T>) {
38+
//传入新旧数据进行比对
39+
val diffUtil = ChatDiffUtil(models, newList)
40+
//经过比对得到差异结果
41+
val diffResult = DiffUtil.calculateDiff(diffUtil)
42+
//NOTE:注意这里要重新设置Adapter中的数据
43+
models.clear()
44+
models.addAll(newList)
45+
//将数据传给adapter,最终通过adapter.notifyItemXXX更新数据
46+
diffResult.dispatchUpdatesTo(this)
47+
}
48+
}
49+
50+
/**
51+
* 使用DiffUtil的ListAdapter,其父类还是RecyclerView.Adapter
52+
* @param diff DiffUtil.Callback主要用于比较新旧列表差异,并处理插入、删除和移动等操作;
53+
* 而 DiffUtil.ItemCallBack只关注当前位置处的对象是否发生改变。我们应该根据具体情况选择合适的接口来实现数据更新检查并且以最高效率达成目标 。
54+
*/
55+
class BaseListAdapter<T>(diff: DiffUtil.ItemCallback<T>, private val vhFactory: IVHFactory) :
56+
ListAdapter<T, BaseVHolder<T>>(diff) {
57+
58+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseVHolder<T> {
59+
return vhFactory.getVH(parent.context, parent, viewType) as BaseVHolder<T>
60+
}
61+
62+
override fun onBindViewHolder(holder: BaseVHolder<T>, position: Int) {
63+
holder.onBindViewHolder(currentList[position], position)
64+
}
65+
66+
fun submit(models: List<T>) {
67+
submitList(models)
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.ninetripods.mq.study.base.adapter
2+
3+
import android.content.Context
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import androidx.recyclerview.widget.RecyclerView
8+
9+
/**
10+
* Created by mq on 2023/7/19
11+
*/
12+
abstract class BaseVHolder<T>(context: Context, parent: ViewGroup, resource: Int) :
13+
RecyclerView.ViewHolder(LayoutInflater.from(context).inflate(resource, parent, false)) {
14+
15+
fun onBindViewHolder(item: T, position: Int) {
16+
onBindView(item, position)
17+
}
18+
19+
abstract fun onBindView(item: T, position: Int)
20+
21+
protected fun <V : View> bind(id: Int): V {
22+
return itemView.findViewById(id)
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.ninetripods.mq.study.base.adapter
2+
3+
import android.content.Context
4+
import android.view.ViewGroup
5+
6+
/**
7+
* Created by mq on 2023/7/20
8+
*/
9+
interface IVHFactory {
10+
fun getVH(context: Context, parent: ViewGroup, viewType: Int): BaseVHolder<*>
11+
}
12+
13+
interface IMultiType {
14+
fun getItemViewType(): Int
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.ninetripods.mq.study.chatgpt
2+
3+
import androidx.recyclerview.widget.DiffUtil
4+
5+
class ChatDiffUtil(private val oldModels: List<Any>, private val newModels: List<Any>) :
6+
DiffUtil.Callback() {
7+
8+
/**
9+
* 旧数据
10+
*/
11+
override fun getOldListSize(): Int = oldModels.size
12+
13+
/**
14+
* 新数据
15+
*/
16+
override fun getNewListSize(): Int = newModels.size
17+
18+
/**
19+
* DiffUtil调用来决定两个对象是否代表相同的Item。true表示两个Item相同(表示View可以复用),false表示不相同(View不可以复用)
20+
* 例如,如果你的项目有唯一的id,这个方法应该检查它们的id是否相等。
21+
*/
22+
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
23+
return oldModels[oldItemPosition]::class.java == newModels[newItemPosition]::class.java
24+
}
25+
26+
/**
27+
* 比较两个Item是否有相同的内容(用于判断Item的内容是否发生了改变),
28+
* 该方法只有当areItemsTheSame (int, int)返回true时才会被调用。
29+
*/
30+
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
31+
return oldModels[oldItemPosition] == newModels[newItemPosition]
32+
}
33+
34+
/**
35+
* 该方法执行时机:areItemsTheSame(int, int)返回true 并且 areContentsTheSame(int, int)返回false
36+
* 该方法返回Item中的变化数据,用于只更新Item中变化数据对应的UI
37+
*/
38+
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
39+
return super.getChangePayload(oldItemPosition, newItemPosition)
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.ninetripods.mq.study.chatgpt
2+
3+
import android.content.Context
4+
import android.view.ViewGroup
5+
import org.ninetripods.mq.study.base.adapter.BaseVHolder
6+
import org.ninetripods.mq.study.base.adapter.IVHFactory
7+
import org.ninetripods.mq.study.chatgpt.viewholder.ChatAskHolder
8+
import org.ninetripods.mq.study.chatgpt.viewholder.ChatReplyImgTextHolder
9+
import org.ninetripods.mq.study.chatgpt.viewholder.ChatReplyTxHolder
10+
11+
/**
12+
* Created by mq on 2023/7/23
13+
*/
14+
class ChatVHolderFactory : IVHFactory {
15+
companion object {
16+
const val TYPE_ASK_TXT = 1
17+
const val TYPE_REPLY_TXT = 2
18+
const val TYPE_REPLY_SPAN = 3
19+
}
20+
21+
override fun getVH(context: Context, parent: ViewGroup, viewType: Int): BaseVHolder<*> {
22+
return when (viewType) {
23+
TYPE_ASK_TXT -> ChatAskHolder(context, parent)
24+
TYPE_REPLY_TXT -> ChatReplyTxHolder(context, parent)
25+
TYPE_REPLY_SPAN -> ChatReplyImgTextHolder(context, parent)
26+
else -> throw IllegalStateException("unSupport type")
27+
}
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.ninetripods.mq.study.chatgpt
2+
3+
import org.ninetripods.mq.study.base.adapter.IMultiType
4+
5+
/**
6+
* Created by mq on 2023/7/20
7+
*/
8+
data class MessageModel(
9+
var msgId: String,
10+
var content: String,
11+
//封禁状态 正常 = 1 全屏蔽 = 2 部分屏蔽 = 3 无需审核 = 4 用户被封禁 = 5
12+
var banStatus: Int = 1,
13+
//传输中 = 0, 传输成功 = 1, 传输失败 = 2
14+
var status: Int = 0,
15+
var type: Int = ChatVHolderFactory.TYPE_ASK_TXT,
16+
) : IMultiType {
17+
override fun getItemViewType(): Int = type
18+
}
19+
20+
data class CardItemModel(
21+
var action: Int = 0,
22+
var sceneId: Int = 0,
23+
var sceneName: String = "",
24+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.ninetripods.mq.study.chatgpt.decoration
2+
3+
import android.graphics.Canvas
4+
import android.graphics.Paint
5+
import android.graphics.drawable.Drawable
6+
import android.text.style.ImageSpan
7+
8+
/**
9+
* Created by mq on 2023/6/15
10+
*/
11+
class CenterAlignImgSpan(d: Drawable, align: Int = ALIGN_BASELINE) : ImageSpan(d, align) {
12+
13+
override fun draw(
14+
canvas: Canvas,
15+
text: CharSequence?,
16+
start: Int,
17+
end: Int,
18+
x: Float,
19+
top: Int,
20+
y: Int,
21+
bottom: Int,
22+
paint: Paint,
23+
) {
24+
// super.draw(canvas, text, start, end, x, top, y, bottom, paint)
25+
val d = drawable
26+
val fm = paint.fontMetricsInt
27+
val tranY = (y + fm.descent + y + fm.ascent) / 2 - d.bounds.bottom / 2
28+
canvas.save()
29+
canvas.translate(x, tranY.toFloat())
30+
d.draw(canvas)
31+
canvas.restore()
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.ninetripods.mq.study.chatgpt.decoration
2+
3+
import android.graphics.Rect
4+
import android.view.View
5+
import androidx.recyclerview.widget.GridLayoutManager
6+
import androidx.recyclerview.widget.RecyclerView
7+
8+
class GirdItemDecoration(val rowSpace: Int, val columnSpace: Int) : RecyclerView.ItemDecoration() {
9+
10+
override fun getItemOffsets(
11+
outRect: Rect,
12+
view: View,
13+
parent: RecyclerView,
14+
state: RecyclerView.State,
15+
) {
16+
var layoutManager = parent.layoutManager
17+
if (layoutManager is GridLayoutManager) {
18+
val position = parent.getChildLayoutPosition(view)
19+
val spanCount = layoutManager.spanCount
20+
if (position < spanCount) {
21+
outRect.top = 0
22+
} else {
23+
outRect.top = rowSpace
24+
}
25+
26+
val last = spanCount - 1
27+
when (position % spanCount) {
28+
0 -> {
29+
outRect.left = 0
30+
outRect.right = columnSpace / 2
31+
}
32+
last -> {
33+
outRect.left = columnSpace / 2
34+
outRect.right = 0
35+
}
36+
else -> {
37+
outRect.left = 0
38+
outRect.right = columnSpace / 2
39+
}
40+
}
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.ninetripods.mq.study.chatgpt.decoration
2+
3+
import android.graphics.Rect
4+
import android.view.View
5+
import androidx.recyclerview.widget.LinearLayoutManager
6+
import androidx.recyclerview.widget.RecyclerView
7+
import androidx.recyclerview.widget.RecyclerView.VERTICAL
8+
9+
class LinearItemDecoration(val space: Int) : RecyclerView.ItemDecoration() {
10+
11+
override fun getItemOffsets(
12+
outRect: Rect,
13+
view: View,
14+
parent: RecyclerView,
15+
state: RecyclerView.State,
16+
) {
17+
var layoutManager = parent.layoutManager
18+
if (layoutManager is LinearLayoutManager) {
19+
val position = parent.getChildLayoutPosition(view)
20+
parent.adapter?.apply {
21+
val lastPosition = itemCount.minus(1)
22+
if (layoutManager.orientation == VERTICAL) {
23+
verticalSpace(outRect, position, lastPosition)
24+
} else {
25+
horizontalSpace(outRect, position, lastPosition)
26+
}
27+
}
28+
}
29+
}
30+
31+
private fun verticalSpace(outRect: Rect, position: Int, lastPosition: Int) {
32+
when (position) {
33+
0 -> {
34+
outRect.top = 0
35+
}
36+
else -> {
37+
outRect.top = space
38+
}
39+
}
40+
}
41+
42+
private fun horizontalSpace(outRect: Rect, position: Int, lastPosition: Int) {
43+
when (position) {
44+
0 -> {
45+
outRect.left = 0
46+
}
47+
else -> {
48+
outRect.left = space
49+
}
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)