Swift 3.1聊天界面键盘效果的实现详解

前言

最近写的 Swift 项目里要实现一个聊天界面,在处理键盘弹出的时候遇到了一点麻烦。

麻烦就在于键盘弹出后如何处理屏幕和键盘的关系

经过一番死磕,终于做出了想要的效果,效果如下:

注:原本项目是 Swift 2.3 写的,为了写这篇博客,用 Swift 3.1 重新实现了一遍。

感受:方法名真的缩短了不少,😁

分析

现在开始,就让我来分析一下这次死磕历程。

一开始想到了两种处理方法,一种是 键盘弹出消失的同时,输入栏随着键盘移动,一种是 键盘弹出消失时,整个屏幕随着键盘移动,这两种方法都有弊端,让我们分类讨论下:

1. 输入栏随着键盘移动

  • 当消息条数较少时,键盘不会遮挡住消息
  • 消息条数多了以后,键盘会遮挡住屏幕中处在键盘位置的消息
  • 每次发送了新的消息,用户无法及时看到(因为被键盘遮住了)

结论:体验不好

2. 屏幕随着键盘移动

  • 消息多了以后,能在屏幕上及时看到最新的消息
  • 但消息少的时候,由于键盘把整个 view 顶出屏幕,用户看不到这头几条消息
  • 当消息没有占满整个屏幕的时候,键盘把 view 顶上去,view 底部会留下一段空白

结论:还是体验不好

上述两种情况的图片我就不发了,大家自己脑补一下

那么作为强迫症,怎么能容忍这种不好的体验?于是开始死磕,首先参考了下日常使用最多的微信、qq,分情况总结了一下微信、qq里键盘弹出的效果

  • 情况一:消息较少时(当键盘弹出不会遮挡住消息)聊天界面不动,键盘弹出时只有输入栏上滑,这样保证了最开始的几条消息能完整显示
  • 情况二:消息较多但还未占满屏幕时(当键盘弹出会遮挡住部分消息),键盘弹出时输入栏上滑,同时聊天界面也上滑。注意:此时输入栏上滑的距离为键盘高度,聊天界面上滑距离为键盘可能遮挡住消息的高度
  • 情况三:消息占满或超出屏幕时,键盘弹出时整个 view 上滑
  • 这其中还包括了发送消息时,聊天界面上滑,保证最后一条消息显示在键盘上方的处理。

如果大家不方便脑补,直接掏出手机,用微信或qq和女神聊个天吧

下面,我们放出代码分析:

布局

首先导入 SnapKit 布局框架,对聊天界面和输入栏进行约束

由于我懒,怎么使用 Snapkit 就不赘述 😁

toolBarView.snp.makeConstraints { (make) in
 make.left.equalTo(view.snp.left)
 make.right.equalTo(view.snp.right)
 make.height.equalTo(toolBarHeight)
 make.bottom.equalTo(view.snp.bottom)
}

chatTableView.snp.makeConstraints { (make) in
 make.left.equalTo(view.snp.left)
 make.right.equalTo(view.snp.right)
 make.bottom.equalTo(toolBarView.snp.top)
 make.top.equalTo(view.snp.top).offset(64)
}

这里让聊天界面的底部和输入栏的上方贴合

监听

监听键盘的弹出和消失

NotificationCenter.default.addObserver(self,
selector: #selector(keyBoardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow,
object: nil)

NotificationCenter.default.addObserver(self,
selector: #selector(keyBoardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide,
object: nil)

当键盘弹出时,会触发 keyBoardWillShow(notification:) 方法,键盘消失时,会触发 keyBoardWillHide(notification:) 方法,我们很多复杂的逻辑,都要在这两个方法中实现。另外,Swift 3.1 的版本中,把很多方法的 NS 前缀去除了,所以还在用 Swift 2.3 的童鞋,在NotificationCenter 前面加上 NS 前缀就可以了。

下面重头戏来了,实现上述三种情况的效果

效果

弹出动画

想要 view 随着键盘弹出上滑,需要得到键盘的高度和键盘弹出动画的时间,这里我们通过如下代码得到:

func keyBoardWillShow(notification: Notification) {
 let userInfo = notification.userInfo! as Dictionary
 let value = userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue
 let keyBoardRect = value.cgRectValue
 // 得到键盘高度
 let keyBoardHeight = keyBoardRect.size.height
 mKeyBoardHeight = keyBoardHeight

 // 得到键盘弹出所需时间
 let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber
 mKeyBoardAnimateDuration = duration.doubleValue

 ...
}

然后实现动画

之前在实现输入栏随着键盘弹出的时候,尝试过两种写法:

1、更新 frame

var animate: (()->Void) = {
 let newFrame = CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: SCREEN_HEIGHT - mKeyBoardHeight)
 self.toolBarView.frame = newFrame
}

UIView.animate(withDuration: mKeyBoardAnimateDuration,
 delay: 0, options: options, animations: animate)

2、更新约束

var animate: (()->Void) = {
 self.toolBarView.snp.updateConstraints(closure: { (make) in
 make.bottom.equalTo(self.view.snp_bottom).offset(-mKeyBoardHeight)
 }
}

UIView.animate(withDuration: mKeyBoardAnimateDuration,
 delay: 0, options: options, animations: animate)

但最后发现,由于滑动的速度不一样,会造成键盘弹出和输入栏上滑时出现缝隙。一句话,体验不好。

于是去网上找了一种方法(必须要感谢下那位大哥),利用一个动画的 options,和 view 的 transform 方法完美解决问题。让 view 和键盘滑动时无缝贴合、如丝般顺滑。

方法如下:

处理所需的动画

var animate: (()->Void) = {
 self.toolBarView.transform = CGAffineTransform(translationX: 0, y: -keyBoardHeight)
}

创建动画 options

let options = UIViewAnimationOptions(rawValue:
UInt((userInfo[UIKeyboardAnimationCurveUserInfoKey]
as! NSNumber).intValue << 16))

实现动画

UIView.animate(withDuration: mKeyBoardAnimateDuration,
 delay: 0, options: options, animations: animate)

如此这般,大功告成!亲个嘴儿 😙

现在有了丝滑的滑动效果,我们来处理上述分析的三种情况

定义情况

首先定义效果枚举类型,枚举的好处就不赘述了

enum AnimateType {
 case animate1 // 键盘弹出的话不会遮挡消息
 case animate2 // 键盘弹出的话会遮挡消息,但最后一条消息距离输入框有一段距离
 case animate3 // 最后一条消息距离输入框在小范围内,这里设为 30
}

枚举类型对应了上述分析的三种效果

让我们回顾一下三种情况

  • 情况一:消息较少时(当键盘弹出不会遮挡住消息)聊天界面不动,键盘弹出时只有输入栏上滑,这样保证了最开始的几条消息能完整显示
  • 情况二:消息较多但还未占满屏幕时(当键盘弹出会遮挡住部分消息),键盘弹出时输入栏上滑,同时聊天界面也上滑。注意:此时输入栏上滑的距离为键盘高度,聊天界面上滑距离为键盘可能遮挡住消息的高度
  • 情况三:消息占满或超出屏幕时,键盘弹出时整个 view 上滑

实现

当消息数量为 0 时,默认动画为输入框滑动

var animate: (()->Void) = {
 self.toolBarView.transform = CGAffineTransform(translationX: 0, y: -keyBoardHeight)
}

当消息数量不为 0 时,需要进行计算判断情况

首先得到最后一条消息在屏幕的位置,其中 cellDistance 就是最后一条消息相对于当前屏幕的 y 值

let lastIndex = IndexPath(row: msgList.count - 1, section: 0)
let rectCellView = chatTableView.rectForRow(at: lastIndex)
let rect = chatTableView.convert(rectCellView, to: chatTableView.superview)
let cellDistance = rect.origin.y + rect.height

限定两个位置 distance1 和 distance2

distance1 代表弹出键盘后键盘顶部的位置相对于当前屏幕的 y 值,对应第一和第二种情况的判断,distance2 代表未弹出键盘时输入框顶部的位置当对于当前屏幕的 y 值。

let distance1 = SCREEN_HEIGHT - toolBarHeight - keyBoardHeight
let distance2 = SCREEN_HEIGHT - toolBarHeight - 2 * fitBlank

计算出最后一条消息的位置和限定 distance1 的差值

这样,当处于第二种情况时,输入框上滑距离为键盘高度,聊天界面上滑距离为计算出的差值,完美实现对应效果

对应代码如下:

let difY = cellDistance - distance1

if cellDistance <= distance1 {
 animate = {
  self.toolBarView.transform = CGAffineTransform(translationX: 0, y: -keyBoardHeight)
 }
 animateType = .animate1
} else if distance1 < cellDistance && cellDistance <= distance2 {
 animate = {
  self.toolBarView.transform = CGAffineTransform(translationX: 0, y: -keyBoardHeight)
  self.chatTableView.transform = CGAffineTransform(translationX: 0, y: -difY)
  self.lastDifY = difY //这里记录下最后一次滑动的dif值,以后有用
 }
 animateType = .animate2
} else {
 animate = {
  self.view.transform = CGAffineTransform(translationX: 0, y: -keyBoardHeight)
 }
 animateType = .animate3
}

以上代码都发生在 keyBoardWillShow(notification: Notification) 中,每次判断完动画的情况后,记录下动画情况,然后当键盘消失时,在 keyBoardWillHide(notification: Notification) 中还原

代码如下:

// 返回 view 或 toolBarView 或 chatTableView 到原有状态
switch animateType {
case .animate1:
 animate = {
 self.toolBarView.transform = CGAffineTransform.identity
 self.chatTableView.transform = CGAffineTransform.identity
 }
case .animate2:
 animate = {
 self.toolBarView.transform = CGAffineTransform.identity
 self.chatTableView.transform = CGAffineTransform.identity
 }
case .animate3:
 animate = {
 self.view.transform = CGAffineTransform.identity
 }
}

如此这般,就实现了三种滑动的效果。但是别急,问题又来了。在情况一和情况二中,聊天界面上滑,怎么保证最后一条消息显示在键盘上方呢?

这就需要我们在发送完消息后,刷新列表的方法中进行处理,这里贴出整个刷新列表方法

实现思路为:

  • 处于情况三时,由于之前约束了聊天界面在输入栏上方,并且整个界面一起上滑,约束依旧成立,只需把聊天界面最后一条消息滚动到聊天界面底部
  • 处于情况一和情况二时,如果聊天界面上滑的总距离(lastDifY + difY)小于键盘高度,则可以继续上滑,上滑距离为新增消息的高度
  • 一旦聊天界面上滑的总距离将要超过键盘高度,则上滑总距离设为键盘高度,如果聊天界面上滑的总距离超过键盘高度,界面上会出现多余的空白
  • 一旦聊天界面上滑的总距离为键盘高度,则按照情况三处理

费尽唇舌,可能还是说不清楚,所以上代码吧😭:

// 刷新列表
 func reloadTableView() {
 chatTableView.reloadData()
 chatTableView.layoutIfNeeded()

 // 得到最后一条消息在view中的位置
 let lastIndex = IndexPath(row: msgList.count - 1, section: 0)
 let rectCellView = chatTableView.rectForRow(at: lastIndex)
 let rect = chatTableView.convert(rectCellView, to: chatTableView.superview)
 let cellDistance = rect.origin.y + rect.height
 let distance1 = SCREEN_HEIGHT - toolBarHeight - mKeyBoardHeight

 // 计算键盘可能遮住的消息的长度
 let difY = cellDistance - distance1

 if animateType == .animate3 {
  // 处于情况三时,由于之前的约束(聊天界面在输入栏上方),并且
  // 是整个界面一起上滑,所以约束依旧成立,只需把聊天界面最后
  // 一条消息滚动到聊天界面底部即可
  scrollToBottom()
 } else if (animateType == .animate1 || animateType == .animate2) && difY > 0{
  // 在情况一和情况二中,如果聊天界面上滑的总距离小于键盘高度,则可以继续上滑
  // 一旦聊天界面上滑的总距离 lastDifY + difY 将要超过键盘高度,则上滑总距离设为键盘高度
  // 此时执行 trans 动画
  // 一旦聊天界面上滑总距离为键盘高度,则变为情况三的情况,把聊天界面最后
  // 一条消息滚动到聊天界面底部即可
  if lastDifY + difY < mKeyBoardHeight {
  lastDifY += difY
  let animate: (()->Void) = {
   self.chatTableView.transform = CGAffineTransform(translationX: 0, y: -self.lastDifY)
  }
  UIView.animate(withDuration: mKeyBoardAnimateDuration, delay: 0, options: animateOption, animations: animate)

  } else if lastDifY + difY > mKeyBoardHeight {
  if lastDifY != mKeyBoardHeight {
   let animate: (()->Void) = {
   self.chatTableView.transform = CGAffineTransform(translationX: 0, y: -self.mKeyBoardHeight)
   }
   UIView.animate(withDuration: mKeyBoardAnimateDuration, delay: 0, options: animateOption, animations: animate)
   lastDifY = mKeyBoardHeight
  }
  scrollToBottom()
  }
 }

 }

再贴一下滚动最后一条消息到聊天界面底部的代码:

func scrollToBottom() {
 if msgList.count > 0 {
 chatTableView.scrollToRow(at: IndexPath(row: msgList.count - 1, section: 0), at: .bottom, animated: true)
 }
}

至此,就真的大功告成了

最后,附上源码地址:

github地址:https://github.com/Newbeeee/NbChatView-Swift

本地地址:http://xiazai.jb51.net/201704/yuanma/NbChatView-Swift-master(jb51.net).rar

总结

开局只是想简单实现聊天效果,没想到因为强迫症和实现优秀的体验,在键盘效果上死磕了许久。前后共花了一天半时间,当真是茶饭不思,夜不能寐。中间尝试了无数滑动方法,在笔记本上画图模拟各种情况,最终做出来后,就像那啥之后,整个人瞬间疲软了,迫不及待地睡了一觉,但内心却是无比激动。

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • swift3.0键盘弹起遮挡输入框问题的解决方案

    下面一段代码是小编给大家介绍的swift3.0键盘弹起遮挡输入框问题的解决方案代码解析,具体代码如下所示: extension LoginViewController:UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() //键盘收回,view放下 UIView.animate(withDuration: 0.4,

  • Swift让输入框跟随键盘弹起避免输入输入法挡住输入框问题

    第一步: 新建Controller 在Xcode选择File → New → File → Cocoa Touch Class 新建LoginViewController继承自UIViewController 第二步:创建两个UITextField passwordInput: UITextField // 密码输入框 accountInput: UITextField // 帐号输入框 第三步:添加键盘KVO 在viewDidLoad方法添加下面两行代码 //当键盘弹起的时候会向系统发出一个通

  • Swift 3.1聊天界面键盘效果的实现详解

    前言 最近写的 Swift 项目里要实现一个聊天界面,在处理键盘弹出的时候遇到了一点麻烦. 麻烦就在于键盘弹出后如何处理屏幕和键盘的关系 经过一番死磕,终于做出了想要的效果,效果如下: 注:原本项目是 Swift 2.3 写的,为了写这篇博客,用 Swift 3.1 重新实现了一遍. 感受:方法名真的缩短了不少,

  • 在自动化中用python实现键盘操作的方法详解

    原来在robotframework中使用press key方法进行键盘的操作,但是该方法需要写被操作对象的locator,不是很方便,现在找到了一种win32api库写键盘操作的一个方法(注意:此方法被操作界面必须在顶层),首先,需要安装win32api的python库,使用命令: pip install pywin32 具体实现代码如下: import win32api import win32con class MyLibrary(object): def keybd_event(self,

  • Android WebView软键盘遮挡输入框方案详解

    目录 背景 纪实 方案 实现 总结 背景 笔者在使用 WebView 加载含有输入框的 H5 页面时,点击输入框后,输入框会被软键盘遮挡住,无法看到输入的内容,这很影响用户体验. 笔者想着这种业务场景比较常见,遂上网搜索一番,果不其然,有不少同志遇到这个问题,想来这个问题很好解决了.笔者一一尝试了同志们提供的解决方案,结果要不是没有作用,要不是效果不太满意,只好自己另辟蹊径了. 注:在笔者的业务场景中,App是全屏的,即没有顶部的系统栏,也没有底部的导航栏,所以笔者的解决方案,可能不适用于其他场

  • iOS界面布局简化UIStackView使用详解

    目录 前言 UIStackView布局思想 distribution: alignment: UIStackView用法 初始化 添加.删除子视图 排列方向 布局方式 对齐方式 间距 前言 在过去iOS页面布局较为传统,大多数人使用Frame或者AutoLayout来布局,在iOS9以后,引入了UIStackView.UIStackView是用于线性布局的控件,可以自动管理子视图布局,自动填充.它借鉴了前端的布局算法Flexbox,可以简便地实现各种页面布局. UIStackView虽然已经不是

  • jQuery动态添加.active 实现导航效果代码思路详解

     代码思路: 页面4: 页面5: 代码思路: 通过jq获取你打开页面的链接  window.location.pathname: 在HTML中给自己的li加入一个ID id的命名与网址链接中的href相同 通过jq包含方法找到相对应的li给他加入active类名 然后..就没有然后了... jq代码: $(function () { var li = $(".title_ul").children("li"); for (var i = 0; i < li.l

  • Android 监听软键盘状态的实例详解

    Android 监听软键盘状态的实例详解 近日遇到要检测软键盘是否显示或隐藏的问题,搜了一下网上,最后找到一个很简单的,记录一下. activityRoot是activity的根view,就是xml里面的第一个view,给它设置一个id. final View activityRootView = findViewById(R.id.activityRoot); activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(ne

  • Android自定义指示器时间轴效果实例代码详解

    指示器时间轴在外卖.购物类的APP里会经常用到,效果大概就像下面这样,看了网上很多文章,大都是自己绘制,太麻烦,其实通过ListView就可以实现. 在Activity关联的布局文件activity_main.xml中放置一个ListView,代码如下.由于这个列表只是用于展示信息,并不需要用户去点击,所以将其clickable属性置为false:为了消除ListView点击产生的波纹效果,我们设置其listSelector属性的值为透明:我们不需要列表项之间的分割线,所以设置其divider属

  • Spinner在Dialog中的使用效果实例代码详解

    背景: 记得很久以前,碰到一个需求场景,需要在Android Dialog中显示Spinner,用来进行选择操作.那个时候还很困惑,不知道是否可以这么搞.抱着试试看的心态,做起了实验,看起来效果还可行,不过最终还是选用了一个开源项目,效果看起来更棒. 代码演示: Spinner在Dialog中的使用,Dialog中关于view的xml布局. <?xml version="1.0" encoding="utf-8"?> <LinearLayout x

  • vue+Element-ui实现分页效果实例代码详解

    当我们向后台请求大量数据的时候,并要在页面展示出来,请求的数据可能上百条数据或者更多的时候,并不想在一个页面展示,这就需要使用分页功能来去完成了. 1.本次所使用的是vue2.0+element-ui实现一个分页功能,element-ui这个组件特别丰富,分页中给我提供了一个Pagination 分页,使用Pagination 快速完成分页功能 最终效果展示 <div class="deit"> <div class="crumbs"> &l

  • js实现弹出框的拖拽效果实例代码详解

    具体代码如下所示: //HTML部分 <div class="wrap"></div> <div class="popUpBox"> <div class="layer-head"><div class="layer-head-text">弹出框</div><div class="layer-close"></div&

随机推荐