从零尝试IM聊天界面

移动端最没尝试的就属IM了,这次想拆出自己尝试的聊天界面记录下
还是基于kotlin开发

WechatIMG2.jpeg

我觉得聊天有很多种,当然今天只说一对一的

屏幕快照 2017-06-11 11.32.34.png

对于消息数据的存储跟检索需要定义一些枚举来方便自己
比如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如下图

WechatIMG3.jpeg

静态加入

//拍照 相册  语音输入 文件 位置
    val funcviewdata:List<FucViewItem>? by lazy {
        listOf<FucViewItem>(
                FucViewItem("拍照","\ue640","#00d1a0"),
                FucViewItem("相册","\ue64c","#368bfc"),
                FucViewItem("文件","\ue648","#00b7fa"),
                FucViewItem("位置","\ue806","#ffa243")
        )
    }
val datasource: ArrayList<ChatItem>? by lazy {
        arrayListOf<ChatItem>(
                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<BubbleTextVew>(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<BubbleTextVew>(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<OneToOneChatFragment.ChatItem>) : RecyclerView.Adapter<ChatPageAdapter.MsgViewHolder>() {
    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<BubbleTextVew>(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<BubbleTextVew>(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)
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容