从零尝试IM聊天界面

作者: 奈文摩尔定律 | 来源:发表于2017-06-11 12:04 被阅读134次

    移动端最没尝试的就属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)
        }
    }
    

    相关文章

      网友评论

      本文标题:从零尝试IM聊天界面

      本文链接:https://www.haomeiwen.com/subject/spdhqxtx.html