iOS swift 关于自定义表情键盘
demo图片:
输入框
- 为了让输入框能够随着用户输入内容变化自动变化高度,这里的输入框使用UITextView来实现,监听textView的代理,当输入内容发生改变的时候计算当前输入的宽高,给予textView一个最小高度一个最大高度,当高度超过最大高度时,让textView滚动起来
//验证文字高度
func textHeight() -> CGFloat{
let rect = textView.attributedText.boundingRect(with: CGSize(width: textView.bounds.size.width - textView.textContainer.lineFragmentPadding*2, height: CGFloat.greatestFiniteMagnitude) , options: .usesLineFragmentOrigin, context: nil)
return rect.height + textView.textContainerInset.top*2
}
//监听输入
func textViewDidChange(_ textView: UITextView) {
//内容改变 计算文字高度 同时更新键盘的高度
let height = textHeight()
if textHeight() <= keyBoardMaxheight {
textView.isScrollEnabled = false
}else{
textView.isScrollEnabled = true
}
print(height)
if height != last {
last = height
textView.setNeedsUpdateConstraints()
if textView.isScrollEnabled{
textView.scrollRangeToVisible(NSRange(location: textView.attributedText.length, length: 1))
}
}
}
键盘监听
NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(_:)), name: UIResponder.keyboardWillShowNotification , object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillHide(_:)), name: UIResponder.keyboardWillHideNotification , object: nil)
//MARK:- 键盘弹起
@objc func keyBoardWillShow(_ noti: Notification){
let info = noti.userInfo
let rect = (info?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
//键盘偏移量
let changeY = rect.size.height
//键盘弹出的时间
let duration = info?[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double
UIView.animate(withDuration: duration) {
self.transform = CGAffineTransform(translationX: 0, y: -changeY)
}
if !emojiBtn.isSelected{
//键盘升起来的时候让emojiView弹下去
UIView.animate(withDuration: duration) {
self.emojiView.transform = CGAffineTransform(translationX: 0, y: changeY)
}
}
}
//MARK:- 键盘落下
@objc func keyBoardWillHide(_ noti: Notification){
let info = noti.userInfo
//键盘弹出的时间
let duration = info?[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double
UIView.animate(withDuration: duration) {
self.transform = CGAffineTransform.identity
}
if emojiBtn.isSelected{
UIView.animate(withDuration: duration) {
self.transform = CGAffineTransform(translationX: 0, y: -self.emojiViewHeight)
self.emojiView.transform = CGAffineTransform.identity
}
}
}
键盘切换
- 通过表情键盘按钮切换表情键盘,需要注意的切换键盘之前先把键盘resignFirstResponder,当处于表情键盘时,如果用户点击了输入框,也需要把键盘切换到默认键盘模式
@objc func keyboardExchange(_ btn: UIButton){
btn.isSelected = !btn.isSelected
//键盘切换
if textView.isFirstResponder{
textView.resignFirstResponder()
}
if btn.isSelected { //表情键盘
}else{ //自定义键盘
textView.becomeFirstResponder()
}
}
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
if emojiBtn.isSelected {
emojiBtn.isSelected = true
keyboardExchange(emojiBtn)
}
return true
}
表情装载
- 加载bundle中的资源需要Bundle.main.path(forResource: "emojiPackage.plist", ofType: nil, inDirectory: "EmojiKeyBoard.bundle")这种方式加载
- 对于每页的最后一个需要用delete图片填充
- 当前表情每页无法填充的部分空白的部分 用空白的内容填充保证同类表情能够撑满一整页
- 如果表情是自定义图片,需要拿到图片的绝对路径,加载时使用UIImage(contentsOfFile:),而不能使用UIImage(named: )
- 如果表情是emoji则需要通过扫描器将emoji表情扫描出来,emoji表情不是图片,在iOS中emoji表情当做普通文字来处理,大小通过font来控制
- swift4.0以后如果需要使用 setValuesForKeys,需要使用@objcMembers修饰class 或者 需要在每个属性的前面加上@objc,否则通过setValuesForKeys无法给对应属性赋值

class EmojiManager: NSObject {
static let shared: EmojiManager = EmojiManager()
var emojiPackages = [EmojiPackage]()
var deletePath: String?
override init() {
super.init()
//加载Emoji
guard let path = Bundle.main.path(forResource: "emojiPackage.plist", ofType: nil, inDirectory: "EmojiKeyBoard.bundle") else{
return
}
guard let dict = NSDictionary(contentsOfFile: path) as? [String:Any] else{
return
}
guard let array = dict["packages"] as? [[String:String]] else{
return
}
for dict in array {
emojiPackages.append(EmojiPackage(dict: dict))
}
//deletePath
guard let deletePath = Bundle.main.path(forResource: "delete@3x.png", ofType: nil, inDirectory: "EmojiKeyBoard.bundle") else{
return
}
self.deletePath = deletePath
}
}
class EmojiPackage: NSObject {
@objc var id: String?
@objc var name: String?
var emojis = [EmojiModel]()
init(dict: [String: String]) {
super.init()
//一次性赋值
setValuesForKeys(dict)
guard let path = Bundle.main.path(forResource: "\(id!)/info.plist", ofType: nil, inDirectory: "EmojiKeyBoard.bundle")else {
return
}
guard let dt = NSDictionary(contentsOfFile: path) as? [String:Any] else {
return
}
guard let array = dt["emojis"] as? [[String:String]] else {
return
}
for var (i,dx) in array.enumerated() {
if let png = dx["png"] {
dx["png"] = id! + "/" + png
}
if i%31 == 0 && i != 0{
emojis.append(EmojiModel(isDelete: true))
}
emojis.append(EmojiModel(dict: dx))
}
let r = emojis.count%32
//填充空格 8*4
if r != 0{
for _ in r..<31{
emojis.append(EmojiModel(isSpace: true))
}
emojis.append(EmojiModel(isDelete: true))
}
}
override func setValue(_ value: Any?, forUndefinedKey key: String) {
}
}
@objcMembers
class EmojiModel: NSObject {
var chs: String?
var png: String?{
didSet{
if let png = png {
if let path = Bundle.main.path(forResource: "EmojiKeyBoard.bundle", ofType: nil){
pngPath = path + "/" + png
}
}
}
}
var code: String?{
didSet{
if let code = code{
//创建扫描器
let scanner = Scanner(string: code)
var result: UInt32 = 0
//利用扫描器扫出结果
scanner.scanHexInt32(&result)
//将结果转换成字符
let c = Character(UnicodeScalar(result)!)
//将字符转换成字符串
emojiCode = String(c)
}
}
}
/// emoji表情解析后的code码
var emojiCode: String?
/// 图片的绝对路径
var pngPath: String?
/// 是否是移除键
var isDelete: Bool = false
/// 是否是空格
var isSpace: Bool = false
init(dict: [String: String]) {
super.init()
setValuesForKeys(dict)
}
init(isDelete: Bool) {
super.init()
self.isDelete = true
}
init(isSpace: Bool) {
super.init()
self.isSpace = true
}
override func setValue(_ value: Any?, forUndefinedKey key: String) {
}
}
表情加载
- 使用collectonView 横向布局,表情cell用UIButton来加载即可显示文字又可显示图片
class EmojiCollectionView: UICollectionView {
lazy var emojiManager: EmojiManager = {
let manager = EmojiManager.shared
return manager
}()
var emojiBlock: ((EmojiModel)->Void)?
init(frame: CGRect,selectedEmoji:((EmojiModel)->Void)?) {
super.init(frame: frame, collectionViewLayout: EmojiFlowLayout())
delegate = self
dataSource = self
register(EmojiCell.self, forCellWithReuseIdentifier: "EmojiCell")
emojiBlock = selectedEmoji
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
extension EmojiCollectionView: UICollectionViewDelegate,UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return emojiManager.emojiPackages.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let emojiPackage = emojiManager.emojiPackages[section]
return emojiPackage.emojis.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "EmojiCell", for: indexPath) as! EmojiCell
let emojiPackage = emojiManager.emojiPackages[indexPath.section]
let model = emojiPackage.emojis[indexPath.row]
cell.model = model
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let emojiPackage = emojiManager.emojiPackages[indexPath.section]
let model = emojiPackage.emojis[indexPath.row]
emojiBlock?(model)
}
}
class EmojiFlowLayout: UICollectionViewFlowLayout {
let row: Int = 4 //行
let col: Int = 8 //列
override func prepare() {
super.prepare()
//设置宽高
minimumLineSpacing = 0
minimumInteritemSpacing = 0
scrollDirection = .horizontal
let width = UIScreen.main.bounds.size.width/CGFloat(col)
itemSize = CGSize(width: width, height: width)
let bmHeight = collectionView!.bounds.size.height - width*CGFloat(row)
sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: bmHeight, right: 0)
collectionView?.isPagingEnabled = true
collectionView?.showsVerticalScrollIndicator = false
collectionView?.showsHorizontalScrollIndicator = false
}
}
class EmojiCell: UICollectionViewCell {
var model: EmojiModel? {
didSet{
emojiBtn.setImage(UIImage(contentsOfFile: model?.pngPath ?? ""), for: .normal)
emojiBtn.setTitle(model?.emojiCode, for: .normal)
if let model = model {
if model.isDelete{
emojiBtn.setImage(UIImage(contentsOfFile: EmojiManager.shared.deletePath ?? ""), for: .normal)
}
if model.isSpace{
}
}
}
}
lazy var emojiBtn: UIButton = {
let button = UIButton()
button.titleLabel?.font = UIFont.systemFont(ofSize: 30)
button.isUserInteractionEnabled = false
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setupUI(){
contentView.addSubview(emojiBtn)
emojiBtn.snp.makeConstraints { (maek) in
maek.edges.equalToSuperview()
}
}
}
表情输入
- 删除键直接调用textView.deleteBackward()会自动删除一个单元
- 空格键直接return
- emoji表情,找到光标所在位置,通过textView.replace直接替换指定位置的内容为emoji码
- 如果是自定义图片则需要通过富文本NSTextAttachment加载图片,通过可变NSMutableAttributedString将NSTextAttachment加载出来,注意图片资源需要使用绝对路径,设置bounds图片好像不能居中,需要在y轴方向设置有一定偏移,富文本内容设置ok之后通过NSMutableAttributedString replaceCharacters方法将内容填充到指定光标的位置
- 使用富文本需要重新设置字体大小
- 让光标加载完表情之后让光标的位置后移一位
- 主动调用textViewDidChange让键盘监听方法能检测到输入
private func showEmojiText(emojModel: EmojiModel){
//删除键
if emojModel.isDelete{
self.textView.deleteBackward()
return
}
//空格键
if emojModel.isSpace{
return
}
//获取emoji并显示UITextView上
if emojModel.emojiCode != nil {
//找到光标的位置
let textRange = textView.selectedTextRange
textView.replace(textRange!, withText: emojModel.emojiCode!)
return
}
// 本地图片
let font = textView.font!
let range = textView.selectedRange
if emojModel.pngPath != nil {
let attr = NSMutableAttributedString(attributedString: textView.attributedText)
let attach = EmojiAttachment()
attach.absolutePath = emojModel.pngPath
attach.chs = emojModel.chs
attach.image = UIImage(contentsOfFile: emojModel.pngPath!)
attach.bounds = CGRect(x: 0, y: -4, width: font.lineHeight, height: font.lineHeight)
//在光标所在位置插入表情
attr.replaceCharacters(in: range, with: NSAttributedString(attachment: attach))
textView.attributedText = attr
}
//重新设置字体大小
textView.font = font
//让选中的rang+1
textView.selectedRange = NSRange(location: range.location+1, length: 0)
//主动调用textDidChange方法
textViewDidChange(textView)
}
表情输出
- 我们知道当表情发送的时候,并不是把图片,或者把图片地址发给服务端,而是将图片的别名发送给服务端eg:表情别名 [哈哈]
- 监听textView的代理方法shouldChangeTextIn当用户点击发送按钮时开始检索图片并把图片替换成对应的别名
- NSMutableAttributedString的enumerateAttributes可以快速便利所有的富文本内容
- 通过dict[NSAttributedString.Key.init(rawValue: "NSAttachment")] 是否有值可以检索出所有的图片表情
- 为了能够将别名和NSAttachment绑定,这里创建了一个EmojiAttachment对象继承NSTextAttachment,前面创建EmojiAttachment的时候把别名chs传进来,这里拿到EmojiAttachment
- 通过NSMutableAttributedString replaceCharacters将chs替换到range对应位置并将表情发送出去
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == "\n"{ //点击了发送按钮
let attr = NSMutableAttributedString(attributedString: textView.attributedText)
attr.enumerateAttributes(in: NSRange(location: 0, length: textView.attributedText.length), options: []) { (dict, range, _) in
//替换表情图片为对应的chs
if let attach = dict[NSAttributedString.Key.init(rawValue: "NSAttachment")] as? EmojiAttachment {
attr.replaceCharacters(in: range, with: attach.chs!)
}
}
emojiReturnBlock?(attr.string)
return false
}
return true
}
class EmojiAttachment: NSTextAttachment {
/// ‘[哈哈]’
var chs: String?
}
表情显示
- 当我们收到服务端返回的content的时候,要考虑里面是否包含有表情,如果有表情,则需要将表情筛选出来替换成对应的图片
- 服务端返回内容eg: 1234[哈哈][害羞],如果要将[]的内容筛查出来这里要使用正则表达式 "\[.*?\]"
- 由于筛选出来的list是个数组,发现替换的时候只有第一组能够正常替换成功,第二种表情替换失败,分析发现当使用图片对应的表情时,图片只占用一个位置,而[哈哈]占用了4个位置,这样替换第二个表情的时候range就不对了,所以后面的表情替换都可能失败,解决这个问题的方法就是从后向前替换表情,使用array.reversed()方法,将NSTextCheckingResult从后向前开始替换
- 替换过程中根据NSTextCheckingResult的range通过substring的方式将[哈哈]截取出来,遍历之前创建的所有表情,找到[哈哈]对应的图片,并使用该图片替换[哈哈]字符串,这样我们在显示的时候看到就是对应图片表情,而不是[哈哈]了
- 正则表达式功能强大,这里只用到了一点皮毛
class EmojiPrase: NSObject {
/// lineHeight 文字单行高度
static func findEmojiAttr(emojiText: String, font: UIFont) -> NSMutableAttributedString?{
//将emojiText转换成富文本
// 1234[哈哈] ,将哈哈解析出来
let pattern = "\\[.*?\\]"
guard let regular = try? NSRegularExpression(pattern: pattern, options: []) else{
return nil
}
let attr = NSMutableAttributedString(string: emojiText)
let results = regular.matches(in: emojiText, options: [], range: NSRange(location: 0, length: attr.length))
//从后往前遍历
for result in results.reversed(){
//将字符串截取出来
let chs = (emojiText as NSString).substring(with: result.range)
//将字符串截出来然后匹配
if let pngPath = findChsPngPath(chs: chs){
let attach = NSTextAttachment()
attach.image = UIImage(contentsOfFile: pngPath)
attach.bounds = CGRect(x: 0, y: -4, width: font.lineHeight, height: font.lineHeight)
attr.replaceCharacters(in: result.range, with: NSAttributedString(attachment: attach))
}
}
return attr
}
private static func findChsPngPath(chs: String) -> String?{
let manager = EmojiManager.shared
for package in manager.emojiPackages {
for emoji in package.emojis{
if chs == emoji.chs{
return emoji.pngPath
}
}
}
return nil
}
//拿到label并显示出来
let emojiInput = EmojiInputView(frame: .zero) { [weak self] (text) in
let attr = EmojiPrase.findEmojiAttr(emojiText: text, font: (self?.textLabel.font)!)
self?.textLabel.attributedText = attr
}
结束语
- 写这边博客的目的是将最近研究的表情键盘的一些知识点和注意点进行归纳和总结,便于以后再用到查验,可能文中还有很多错误和不足的地方,欢迎指正,谢谢
demo下载
iOS swift 关于自定义表情键盘的更多相关文章
- iOS开发之自定义表情键盘(组件封装与自动布局)
下面的东西是编写自定义的表情键盘,话不多说,开门见山吧!下面主要用到的知识有MVC, iOS开发中的自动布局,自定义组件的封装与使用,Block回调,CoreData的使用.有的小伙伴可能会问写一个自 ...
- ios开发之--仿(微信)自定义表情键盘
先附上demo:https://github.com/hgl753951/CusEmoji.git 效果图如下:
- [译] 用 Swift 创建自定义的键盘
本文翻译自 How to make a custom keyboard in iOS 8 using Swift 我将讲解一些关于键盘扩展的基本知识,然后使用iOS 8 提供的新应用扩展API来创建一 ...
- iOS_仿QQ表情键盘
当UITextFiled和UITextView这种文本输入类控件成为第一响应者时,弹出的键盘由他们的一个UIView类的inputView属性来控制,当inputView为nil时会弹出系统的键盘,想 ...
- iOS 自定义emoji表情键盘
之前走了很多弯路,包括自己定以emoji表情,自己创建view类去处理图文混排 ,当把这些焦头烂额的东西处理完了才发现 ,其实系统自带键盘是如此的方便,iOS 系统自带的表情在view,textfie ...
- 根据iOS 10 的新特性,创建iMessage App,可用于自定义表情
第一. 介绍(原文作者 澳大利亚19岁少年--Davis Allie ----原文地址) 随着iOS10的发布,苹果对开发者开放了Messages应用程序,开发人员现在可以创建他们自己的各种类型 并且 ...
- iOS Swift WisdomKeyboardKing 键盘智能管家SDK
iOS Swift WisdomKeyboardKing 键盘智能管家SDK [1]前言: 今天给大家推荐个好用的开源框架:WisdomKeyboardKing,方面iOS日常开发,优点和功能请 ...
- 【转】swift实现ios类似微信输入框跟随键盘弹出的效果
swift实现ios类似微信输入框跟随键盘弹出的效果 为什么要做这个效果 在聊天app,例如微信中,你会注意到一个效果,就是在你点击输入框时输入框会跟随键盘一起向上弹出,当你点击其他地方时,输入框又会 ...
- IOS UITextView支持输入、复制、粘贴、剪切自定义表情
UITextView是ios的富文本编辑控件,除了文字还可以插入图片等.今天主要介绍一下UITextView对自定义表情的处理. 1.首先识别出文本中的表情文本,然后在对应的位置插入NSTextAtt ...
随机推荐
- iOS:quartz2D绘图(给图形绘制阴影)
quartz2D既可以绘制原始图形,也可以给原始图形绘制阴影. 绘制阴影时,需要的一些参数:上下文.阴影偏移量.阴影模糊系数 注意:在drawRect:方法中同时调用绘制同一个图形时,在对绘制的图形做 ...
- VMware Workstation 重启服务脚本 解决连不上ssh问题
解决虚拟机,每次启动连不上ssh问题,需要关闭虚拟机,再执行脚本.执行完后,再启动虚拟机就可以连上ssh啦! 脚本名称:vmware_server_restart.bat (请以管理员身份运行,否则可 ...
- MVC与MVT
MVC 大部分开发语言中都有MVC框架 MVC框架的核心思想是:解耦 降低各功能模块之间的耦合性,方便变更,更容易重构代码,最大程度上实现代码的重用 m表示model,主要用于对数据库层的封装 v表示 ...
- Spark SQL 代码简要阅读(基于Spark 1.1.0)
Spark SQL允许相关的查询如SQL,HiveQL或Scala运行在spark上.其核心组件是一个新的RDD:SchemaRDD,SchemaRDDs由行对象组成,并包含一个描述此行对象的每一列的 ...
- Spring框架学习(7)spring mvc入门
内容源自:spring mvc入门 一.spring mvc和spring的关系 spring mvc是spring框架提供的七层体系架构中的一个层,是spring框架的一部分,是spring用于处理 ...
- 转: codereview工具之 review board 选型与实践
转:ReviewBoard代码评审实践总结 http://my.oschina.net/donhui/blog/350074 svn与review board 结合实践 http://my.oschi ...
- [Algorithm] Find Max Items and Max Height of a Completely Balanced Binary Tree
A balanced binary tree is something that is used very commonly in analysis of computer science algor ...
- activemq集群搭建Demo
activemq5.14.5单节点安装Demo 第一步:创建集群目录 [root@node001 ~]# mkdir -p /usr/local/activemqCluster 复制单点至集群目录 [ ...
- 正则表达式学习(PCRE)
正则表达式是一个从左到右匹配目标字符串的模式.大多数字符自身就代表一个匹配 它们自身的模式. 1.分隔符:当使用 PCRE 函数的时候,模式需要由分隔符闭合包裹.分隔符可以使任意非字母数字.非反斜线. ...
- CDN新应用和客户
目前的CDN配置服务主要应用于证券.金融保险.ISP.ICP.网上交易.门户网站.大中型公司.网络教学等领域.另外在行业专网.互联网中都可以用到,甚至可以对局域网进行网络优化.利用CDN,这些网站无需 ...