移动端最没尝试的就属IM了,这次想拆出自己尝试的聊天界面记录下
还是基于kotlin开发
我觉得聊天有很多种,当然今天只说一对一的
对于消息数据的存储跟检索需要定义一些枚举来方便自己
比如ItemType作为消息类型决定消息的是否发送或接收或时间line
enum class ItemType(var value:Int){
Time(0),
SendText(1),
SendImg(2),
ReceiveText(3),
ReceiveImg(4)
}
比如TimeType与ContentType
现在只区分昨天以前、昨天与今天
内容也只简单的区分文本与图片
当然等服务端IM正式联用会拓展功能的
enum class TimeType(var value: Int){
Faraway(0),Yesterday(1),Today(2)
}
enum class ContentType(var value: Int){
Text(0),Img(1)
}
俩个所需的数据元类
ChatItem直接作为realm本地数据存储与recycleview显示的模型类(聊天这块的realm还未加入)
/**
* Created by tanweiping on 16/12/29.
*/
data class ChatItem(var itemtype:ItemType, var sender:Contact?=null,
var itemcontent:String?=null, var timecontent:String?=null,
var timetype:TimeType?=TimeType.Today, var contentType:ContentType?=ContentType.Text)
data class FucViewItem(var label:String,var iconcode:String,var iconcolor:String? = "#8399a6")
FucViewItem作为自定义改装的keyboard的功能view如下图
静态加入
//拍照 相册 语音输入 文件 位置
val funcviewdata:List? by lazy {
listOf(
FucViewItem("拍照","\ue640","#00d1a0"),
FucViewItem("相册","\ue64c","#368bfc"),
FucViewItem("文件","\ue648","#00b7fa"),
FucViewItem("位置","\ue806","#ffa243")
)
}
val datasource: ArrayList? by lazy {
arrayListOf(
ChatItem(ItemType.Time,timecontent = "2016-12-22"
,timetype = TimeType.Faraway),
ChatItem(ItemType.ReceiveText,itemcontent = "hello",timecontent = "2016-12-22"
,timetype = TimeType.Faraway),
ChatItem(ItemType.ReceiveText,itemcontent = "你好啊",timecontent = "2016-12-22"
,timetype = TimeType.Faraway),
ChatItem(ItemType.Time,timecontent = "2016-12-30"
,timetype = TimeType.Today),
ChatItem(ItemType.SendText,itemcontent = "我好的啊",timecontent = "2016-12-30"
,timetype = TimeType.Today),
ChatItem(ItemType.SendText,itemcontent = "hehe",timecontent = "2016-12-30"
,timetype = TimeType.Today)
....
)
}
现在了解下childview的设置吧
titletv是自定义的头部view的标题view,接收上一个界面传来的需要chat的人或组织,righttv是使用iconfont的右部的功能button的Textview
titletv?.text = arguments.getString("title","")
righttv?.text = "\ue601"
下面加入keyboard的自定义的functionview部分
了解anko与kotlin的童鞋应该很容易就明白了吧
主要使用了anko的dsl与kotlin 函数式
funcviewdata数据源直接参与UI的构建
//拍照 相册 语音输入 文件 位置
ek_bar?.addFuncView(context.verticalLayout {
backgroundColor = Color.parseColor("#ecebf0")
linearLayout {
orientation = LinearLayout.HORIZONTAL
weightSum = 4f
padding = dip(10)
funcviewdata?.forEach {
verticalLayout {
isClickable = true
gravity = Gravity.CENTER_HORIZONTAL
textView {
gravity = Gravity.CENTER
text = it.iconcode
setPadding(dip(15),dip(10),dip(15),dip(5))
typeface = App.instance?.iconfont
textSize = 35f
textColor = Color.parseColor(it.iconcolor)
background = resources.getDrawable(R.drawable.func_border_radius)
}.lparams { width = wrapContent;height = wrapContent }
textView {
padding = dip(5)
gravity = Gravity.CENTER
text = it.label
textSize = 12f
textColor = resources.getColor(R.color.gray)
}
}.lparams { height = matchParent;width= matchParent;weight = 1f; }
}
}.lparams { width = matchParent;height = wrapContent;weight = 1f }
linearLayout {
orientation = LinearLayout.HORIZONTAL
weightSum = 4f
padding = dip(10)
}.lparams { width = matchParent;height = wrapContent;weight = 1f }
})
设置recycleview了
垂直显示
stackFromEnd是数据从栈底开始显示
ChatPageAdapter稍等讲
还有个是发送信息的事件
val lp = LinearLayoutManager(context)
lp.orientation = LinearLayoutManager.VERTICAL
lp.stackFromEnd = true
//lp.reverseLayout = true
//lp.scrollToPosition(0)
chat_content?.layoutManager = lp
chat_content?.adapter = ChatPageAdapter(context,datasource!!)
//chat_content?.adapter?.setHasStableIds(true)
ek_bar?.btnSend?.onClick {
datasource?.add(ChatItem(
ItemType.SendText,itemcontent = ek_bar?.etChat?.text.toString(),timecontent = "2016-12-30"
,timetype = TimeType.Today)
)
chat_content?.adapter?.notifyItemInserted(datasource?.size!! - 1)
chat_content?.smoothScrollToPosition(datasource?.size!!)
ek_bar?.etChat?.setText("")
}
俩个事件代理
head_frame是包裹着recycleview的容器,负责用户对于聊天记录的下拉获取往日的记录
doAsync是异步包裹
chat_content的touch事件是为了点击空白,将keyboard收回
head_frame?.setPtrHandler(object : PtrDefaultHandler() {
override fun onRefreshBegin(frame: PtrFrameLayout) {
doAsync {
sleep(1000)
uiThread {
frame.refreshComplete()
}
}
}
})
chat_content?.onTouch { _, motionEvent ->
if (motionEvent.action == MotionEvent.ACTION_DOWN){
ek_bar?.reset()
}
false
}
下面介绍今天比较重要的adapter吧
相信大家在此之前已经很了解recycleview的adapter了,
所以简单来说主要 实现从Holder来说
暂时没考虑消息撤回
我定义了MsgViewHolder局部父类holder
TimeViewHolder时间label
ReceiveTextViewHolder,ReceiveImgViewHolder 接收msg的holder
SendTextViewHolder,SendImgViewHolder发出去的holder
open inner class MsgViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView) {}
inner class TimeViewHolder(itemView: View?) : MsgViewHolder(itemView) {
var timetv:TextView? = itemView?.find(pageIds.ItemTextId)
}
inner class ReceiveTextViewHolder(itemView: View?) : MsgViewHolder(itemView) {
var userimg:ImageView? = itemView?.find(pageIds.ItemUserImgId)
var receivetext:TextView? = itemView?.find(pageIds.ItemTextId)
}
inner class ReceiveImgViewHolder(itemView: View?) : MsgViewHolder(itemView) {}
inner class SendTextViewHolder(itemView: View?) : MsgViewHolder(itemView) {
var userimg:ImageView? = itemView?.find(pageIds.ItemUserImgId)
var sendtext:TextView? = itemView?.find(pageIds.ItemTextId)
}
inner class SendImgViewHolder(itemView: View?) : MsgViewHolder(itemView) {}
覆写onBindViewHolder
我需要枚举item的Type来给予相应的数据绑定及事件
比如
val obj = datasource[position]
when(getItemViewType(position)){
OneToOneChatFragment.ItemType.Time.value->{
with(holder as TimeViewHolder){
timetv?.text = obj.timecontent
}
}
OneToOneChatFragment.ItemType.ReceiveText.value->{
with(holder as ReceiveTextViewHolder){
Glide.with(context).load(R.mipmap.sf).into(userimg)
receivetext?.text = obj.itemcontent
}
}
OneToOneChatFragment.ItemType.SendText.value->{
with(holder as SendTextViewHolder){
Glide.with(context).load(R.mipmap.sf).into(userimg)
sendtext?.text = obj.itemcontent
}
}
}
而onCreateViewHolder则是给予关于datasource的item的视图
比如
var layout:View? = null
when(viewType){
OneToOneChatFragment.ItemType.Time.value->{
layout = context.relativeLayout {
gravity = Gravity.CENTER
padding = dip(5)
textView {
id = pageIds.ItemTextId
background = resources.getDrawable(R.drawable.chat_item_border_radius)
textColor = resources.getColor(R.color.bgColor_overlay)
textSize = 14f
padding = dip(5)
}.lparams { width = wrapContent;height = wrapContent }
}
layout.layoutParams = LinearLayout.LayoutParams(matchParent, wrapContent)
//parent?.addView(layout)
return TimeViewHolder(layout)
}
OneToOneChatFragment.ItemType.ReceiveText.value->{
layout = context.linearLayout {
orientation = LinearLayout.HORIZONTAL
padding = dip(15)
gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
imageView {
scaleType = ImageView.ScaleType.CENTER_CROP
id = pageIds.ItemUserImgId
}.lparams { width = dip(30);height = dip(30)}
include(R.layout.chat_receive_msg_text).lparams { padding = dip(5) }
}
layout.layoutParams = LinearLayout.LayoutParams(matchParent, wrapContent)
//parent?.addView(layout)
return ReceiveTextViewHolder(layout)
}
OneToOneChatFragment.ItemType.SendText.value->{
layout = context.linearLayout {
orientation = LinearLayout.HORIZONTAL
padding = dip(15)
gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
include(R.layout.chat_send_msg_text).lparams { padding = dip(5) }
imageView {
scaleType = ImageView.ScaleType.CENTER_CROP
id = pageIds.ItemUserImgId
}.lparams{ width = dip(30);height = dip(30)}
}
layout.layoutParams = LinearLayout.LayoutParams(matchParent, wrapContent)
//parent?.addView(layout)
return SendTextViewHolder(layout)
}
}
return MsgViewHolder(layout)
}
下面是adapter的完整代码
/**
* Created by tanweiping on 16/12/30.
*/
class ChatPageAdapter(var context:Context,var datasource: ArrayList) : RecyclerView.Adapter() {
data class PageID(var ItemTextId:Int,var ItemImgId:Int,var ItemUserImgId:Int)
val pageIds:PageID by lazy { PageID(R.id.msgcontent,R.id.msgcontent,10103) }
override fun getItemViewType(position: Int): Int = datasource[position].itemtype.value
override fun getItemCount(): Int = datasource.size
override fun onBindViewHolder(holder: MsgViewHolder?, position: Int) {
val obj = datasource[position]
when(getItemViewType(position)){
OneToOneChatFragment.ItemType.Time.value->{
with(holder as TimeViewHolder){
timetv?.text = obj.timecontent
}
}
OneToOneChatFragment.ItemType.ReceiveText.value->{
with(holder as ReceiveTextViewHolder){
Glide.with(context).load(R.mipmap.sf).into(userimg)
receivetext?.text = obj.itemcontent
}
}
OneToOneChatFragment.ItemType.SendText.value->{
with(holder as SendTextViewHolder){
Glide.with(context).load(R.mipmap.sf).into(userimg)
sendtext?.text = obj.itemcontent
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): MsgViewHolder {
var layout:View? = null
when(viewType){
OneToOneChatFragment.ItemType.Time.value->{
layout = context.relativeLayout {
gravity = Gravity.CENTER
padding = dip(5)
textView {
id = pageIds.ItemTextId
background = resources.getDrawable(R.drawable.chat_item_border_radius)
textColor = resources.getColor(R.color.bgColor_overlay)
textSize = 14f
padding = dip(5)
}.lparams { width = wrapContent;height = wrapContent }
}
layout.layoutParams = LinearLayout.LayoutParams(matchParent, wrapContent)
//parent?.addView(layout)
return TimeViewHolder(layout)
}
OneToOneChatFragment.ItemType.ReceiveText.value->{
layout = context.linearLayout {
orientation = LinearLayout.HORIZONTAL
padding = dip(15)
gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
imageView {
scaleType = ImageView.ScaleType.CENTER_CROP
id = pageIds.ItemUserImgId
}.lparams { width = dip(30);height = dip(30)}
include(R.layout.chat_receive_msg_text).lparams { padding = dip(5) }
}
layout.layoutParams = LinearLayout.LayoutParams(matchParent, wrapContent)
//parent?.addView(layout)
return ReceiveTextViewHolder(layout)
}
OneToOneChatFragment.ItemType.SendText.value->{
layout = context.linearLayout {
orientation = LinearLayout.HORIZONTAL
padding = dip(15)
gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
include(R.layout.chat_send_msg_text).lparams { padding = dip(5) }
imageView {
scaleType = ImageView.ScaleType.CENTER_CROP
id = pageIds.ItemUserImgId
}.lparams{ width = dip(30);height = dip(30)}
}
layout.layoutParams = LinearLayout.LayoutParams(matchParent, wrapContent)
//parent?.addView(layout)
return SendTextViewHolder(layout)
}
}
return MsgViewHolder(layout)
}
}