圖文混排
實現效果
表情圖文混排.png.jpeg
表情按鈕點擊事件
- 在
HMEmoticonPageCell
中監聽表情按鈕點擊 -- 在添加按鈕的時候添加
/// 添加表情按鈕
private func addEmoticonButtons(){
for _ in 0..<HMEmoticonPageNum {
let button = UIButton()
// 添加點擊事件
button.addTarget(self, action: "emoticonButtonClick:", forControlEvents: UIControlEvents.TouchUpInside)
// 設置字體大小
button.titleLabel?.font = UIFont.systemFontOfSize(36)
contentView.addSubview(button)
emoticonButtons.append(button)
}
}
- 實現點擊方法
/// 表情按鈕點擊
///
/// - parameter button: <#button description#>
@objc private func emoticonButtonClick(button: UIButton) {
printLog("表情按鈕點擊了")
}
-
接下來需要做哪些事情?
- 取到按鈕對應的表情模型
- 自定義 button,添加一個屬性記住當前顯示的表情模型
- 將表情模型發送給發微博控制器
- 利用通知的形式
- 控制器中添加表情到
textView
中- 使用 NSAttributedString
- 取到按鈕對應的表情模型
自定義表情按鈕
HMEmoticonButton
class HMEmoticonButton: UIButton {
var emoticon: HMEmoticon?
}
- 更改
HMEmoticonPageCell
中emoticonButtons
數據類型
// 裝有所有表情按鈕的集合
private lazy var emoticonButtons: [HMEmoticonButton] = [HMEmoticonButton]()
- 在給
HMEmoticonPageCell
設置數據的時候給每一個表情按鈕設置數據
// 遍歷當前設置的表情數據
for (index,value) in emoticons!.enumerate() {
let button = emoticonButtons[index]
// 設置表情屬性
button.emoticon = value
// 顯示當前遍歷到的表情按鈕
button.hidden = false
if !value.isEmoji {
let image = UIImage(named: "\(value.path!)/\(value.png!)")
button.setImage(image, forState: UIControlState.Normal)
button.setTitle(nil, forState: UIControlState.Normal)
}else{
button.setImage(nil, forState: UIControlState.Normal)
button.setTitle((value.code! as NSString).emoji(), forState: UIControlState.Normal)
}
}
- 提取顯示表情的邏輯到
HMEmoticonButton
中的emoticon
的didSet
方法中
var emoticon: HMEmoticon? {
didSet{
// 顯示表情數據
if !emoticon!.isEmoji {
let image = UIImage(named: "\(emoticon!.path!)/\(emoticon!.png!)")
self.setImage(image, forState: UIControlState.Normal)
self.setTitle(nil, forState: UIControlState.Normal)
}else{
self.setImage(nil, forState: UIControlState.Normal)
self.setTitle((emoticon!.code! as NSString).emoji(), forState: UIControlState.Normal)
}
}
}
- 更改
HMEmoticonPageCell
中emoticons
的didSet
方法
/// 當前頁顯示的表情數據
var emoticons: [HMEmoticon]? {
didSet{
// 先隱藏所有的表情按鈕
for value in emoticonButtons {
value.hidden = true
}
// 遍歷當前設置的表情數據
for (index,value) in emoticons!.enumerate() {
let button = emoticonButtons[index]
// 設置表情屬性
button.emoticon = value
// 顯示當前遍歷到的表情按鈕
button.hidden = false
}
}
}
- 在
CommonTools
中添加表情按鈕點擊通知
// 表情按鈕點擊通知
let HMEmoticonDidSelectedNotification = "HMEmoticonDidSelectedNotification"
- 監聽表情按鈕點擊,發送通知
/// 表情按鈕點擊
@objc private func emoticonButtonClick(button: HMEmoticonButton) {
//發送表情按下的通知
NSNotificationCenter.defaultCenter().postNotificationName(HMEmoticonDidSelectedNotification, object: self, userInfo: ["emoticon": button.emoticon!])
}
-
HMComposeViewController
注冊通知
// 監聽表情按鈕點擊的通知
NSNotificationCenter.defaultCenter().addObserver(self, selector: "emoticonDidSelected:", name: HMEmoticonDidSelectedNotification, object: nil)
- 添加通知調用的方法
/// 表情按鈕點擊發送通知監聽的方法
@objc private func emoticonDidSelected(noti: NSNotification){
// 需要重寫 `HMEmoticon` 的 description 屬性
printLog(noti.userInfo!["emoticon"])
}
運行測試
- 圖文混排邏輯
- 通過現有的
attributedText
初始化一個NSMutableAttributedString
- 通過表情圖片初始化一個
NSTextAttachment
對象 - 通過第 2 步的
attachment
對象初始化一個NSAttributedString
- 將第 3 步的
attributedString
添加到第 1 步的可變的NSMutableAttributedString
- 將第 4 步的結果賦值給
textView
的attributedText
- 通過現有的
注意區分
emoji
表情與圖片表情
- 以下代碼都是在
emoticonDidSelected
方法中測試
/// 表情按鈕點擊發送通知監聽的方法
@objc private func emoticonDidSelected(noti: NSNotification){
printLog(noti.userInfo!["emoticon"])
// 判斷 emoticon 是否為空
guard let emoticon = noti.userInfo!["emoticon"] as? HMEmoticon else {
return
}
if !emoticon.isEmoji {
// 通過原有的文字初始化一個可變的富文本
let originalAttributedString = NSMutableAttributedString(attributedString: textView.attributedText)
// 通過表情模型初始化一個圖片
let image = UIImage(named: "\(emoticon.path!)/\(emoticon.png!)")
// 初始化文字附件,設置圖片
let attatchment = NSTextAttachment()
attatchment.image = image
// 通過文字附件初始化一個富文本
let attributedString = NSAttributedString(attachment: attatchment)
// 添加到原有的富文本中
originalAttributedString.appendAttributedString(attributedString)
// 設置 textView 的 attributedText
textView.attributedText = originalAttributedString
}else{
// emoji 表情
}
}
運行測試:圖片太大
- 調整圖片大小
// 圖片寬高與文字的高度一樣
let imageWH = textView.font!.lineHeight
// 調整圖片大小
attatchment.bounds = CGRectMake(0, 0, imageWH, imageWH)
運行測試:當輸入第二個表情的時候圖片大小變小了,沒有指定
attributedString
的字體大小
- 指定表情的
attributedString
的字體大小
// 通過文字附件初始化一個富文本
let attributedString = NSMutableAttributedString(attributedString: NSAttributedString(attachment: attatchment))
// 設置添加進去富文本的字體大小
attributedString.addAttribute(NSFontAttributeName, value: textView.font!, range: NSMakeRange(0, 1))
運行測試:發現表情圖片偏上,調整
attachment
的bounds
// 調整圖片大小 --> 解決圖片大小以及偏移問題
attatchment.bounds = CGRectMake(0, -4, imageWH, imageWH)
運行測試:發現當光標不在最后一位的時候,表情圖片依然拼在最后面,解決辦法就是調用
NSMutableAttributedString
的insertAttributedString
的方法,傳入index
就是當前 textView 的選中范圍的location
- 解決當光標不在最后一位的時候表情圖片拼接問題
// 添加到原有的富文本中
// originalAttributedString.appendAttributedString(attributedString)
// 解決當光標不在最后一位的時候添加圖片表情的問題
let selectedRange = textView.selectedRange
originalAttributedString.insertAttributedString(attributedString, atIndex: selectedRange.location)
運行測試:當添加圖片到光標位置的時候,光標移動到最后一個去了,解決方法:在設置完 textView 的富文本之后調用
selectedRange
- 設置完富文本之后更新
selectedRange
var selectedRange = textView.selectedRange
originalAttributedString.insertAttributedString(attributedString, atIndex: selectedRange.location)
// 設置 textView 的 attributedText
textView.attributedText = originalAttributedString
// 更新光標所在位置
selectedRange.location += 1
textView.selectedRange = selectedRange
運行測試:如果選中某一段字符,然后再次輸入表情的話,需要用表情把選中的字符替換掉
- 在輸入表情的時候,使用表情替換當前選中的文字
// 添加到原有的富文本中
// originalAttributedString.appendAttributedString(attributedString)
var selectedRange = textView.selectedRange
// 解決當光標不在最后一位的時候添加圖片表情的問題
// originalAttributedString.insertAttributedString(attributedString, atIndex: selectedRange.location)
// 解決 textView 選中文字之后輸入表情產生的 bug
originalAttributedString.replaceCharactersInRange(selectedRange, withAttributedString: attributedString)
// 設置 textView 的 attributedText
textView.attributedText = originalAttributedString
// 更新光標所在位置,以及選中長度
selectedRange.location += 1
selectedRange.length = 0
textView.selectedRange = selectedRange
運行測試
- 顯示 Emoji 表情
if !emoticon.isEmoji {
...
}else{
// emoji 表情
textView.insertText((emoticon.code! as NSString).emoji())
}
運行測試
-
監聽鍵盤里面刪除按鈕點擊
- 發送刪除按鈕點擊的通知
- 在
HMComposeViewController
中監聽通知 - 在通知的方法中調用
textView
的deleteBackward
方法
在
HMEmoticonPageCell
中給刪除按鈕添加點擊事件
deleteButton.addTarget(self, action: "deleteButtonClick:", forControlEvents: UIControlEvents.TouchUpInside)
- 在
CommonTools
中添加常量HMEmoticonDeleteButtonDidSelectedNotification
// 刪除按鈕點擊通知
let HMEmoticonDeleteButtonDidSelectedNotification = "HMEmoticonDeleteButtonDidSelectedNotification"
- 點擊事件執行的方法
@objc private func deleteButtonClick(button: UIButton){
//發送表情按下的通知
NSNotificationCenter.defaultCenter().postNotificationName(HMEmoticonDeleteButtonDidSelectedNotification, object: self)
}
- 在
HMComposeViewController
中監聽通知
// 監聽刪除按鈕的通知
NSNotificationCenter.defaultCenter().addObserver(self, selector: "deletedButtonSelected:", name: HMEmoticonDeleteButtonDidSelectedNotification, object: nil)
- 添加通知調用的方法
// 刪除按鈕點擊的通知
@objc private func deletedButtonSelected(noti: NSNotification){
textView.deleteBackward()
}
運行測試
- 抽取代碼,自定義
HMEmoticonTextView
繼承于HMTextView
,在內部提供insertEmoticon
的方法
class HMEmoticonTextView: HMTextView {
/// 向當前 textView 添加表情
///
/// - parameter emoticon: 表情模型
func insertEmoticon(emoticon: HMEmoticon) {
}
}
- 更改
HMComposeViewController
中textView
的類型
/// 輸入框
private lazy var textView: HMEmoticonTextView = {
let textView = HMEmoticonTextView()
textView.placeholder = "聽說下雨天音樂和辣條更配喲~"
textView.font = UIFont.systemFontOfSize(16)
textView.alwaysBounceVertical = true
textView.delegate = self
return textView
}()
- 將
HMComposeViewController
中的 添加表情的代碼移植到以HMEmoticonTextView
中的insertEmoticon
方法中
// 向當前 textView 添加表情
///
/// - parameter emoticon: 表情模型
func insertEmoticon(emoticon: HMEmoticon) {
if !emoticon.isEmoji {
// 通過原有的文字初始化一個可變的富文本
let originalAttributedString = NSMutableAttributedString(attributedString: attributedText)
// 通過表情模型初始化一個圖片
let image = UIImage(named: "\(emoticon.path!)/\(emoticon.png!)")
// 初始化文字附件,設置圖片
let attatchment = NSTextAttachment()
attatchment.image = image
// 圖片寬高與文字的高度一樣
let imageWH = font!.lineHeight
// 調整圖片大小 --> 解決圖片大小以及偏移問題
attatchment.bounds = CGRectMake(0, -4, imageWH, imageWH)
// 通過文字附件初始化一個富文本
let attributedString = NSMutableAttributedString(attributedString: NSAttributedString(attachment: attatchment))
// 設置添加進去富文本的字體大小
attributedString.addAttribute(NSFontAttributeName, value: font!, range: NSMakeRange(0, 1))
// 添加到原有的富文本中
// originalAttributedString.appendAttributedString(attributedString)
var selectedRange = self.selectedRange
// 解決當光標不在最后一位的時候添加圖片表情的問題
// originalAttributedString.insertAttributedString(attributedString, atIndex: selectedRange.location)
// 解決 textView 選中文字之后輸入表情產生的 bug
originalAttributedString.replaceCharactersInRange(selectedRange, withAttributedString: attributedString)
// 設置 textView 的 attributedText
attributedText = originalAttributedString
// 更新光標所在位置,以及選中長度
selectedRange.location += 1
selectedRange.length = 0
self.selectedRange = selectedRange
}else{
// emoji 表情
insertText((emoticon.code! as NSString).emoji())
}
}
- 在
HMComposeViewController
中表情點擊的方法
/// 表情按鈕點擊發送通知監聽的方法
@objc private func emoticonDidSelected(noti: NSNotification){
// 判斷 emoticon 是否為空
guard let emoticon = noti.userInfo!["emoticon"] as? HMEmoticon else {
return
}
textView.insertEmoticon(emoticon)
}
運行測試:當輸入圖片表情的時候,占位文字并沒有隱藏,解決方法,在
insertEmoticon
方法最后調用代理,發送通知
- 添加完表情之后,調用代理,發送通知
// 調用代理
// OC 寫法
// if let del = self.delegate where del.respondsToSelector("textViewDidChange:"){
// del.textViewDidChange!(self)
// }
// Swift 寫法
self.delegate?.textViewDidChange?(self)
// 發送通知
NSNotificationCenter.defaultCenter().postNotificationName(UITextViewTextDidChangeNotification, object: self)
運行測試
表情點擊氣泡
- 功能1:在點擊表情按鈕的時候彈出一個氣泡
- 功能2:在長按滑動的時候氣泡隨著手指移動
實現效果
表情點擊氣泡.png.jpeg
點擊表情按鈕彈出一個氣泡
實現思路
- 氣泡可以使用 xib 實現
- 點擊表情按鈕的時候取到對應表情按鈕的位置
- 將位置轉化成在 window 上的位置
- 根據將氣泡添加到最上層的 Window 上
- 0.1 秒之后氣泡從 window 上移除
代碼實現
- 使用 xib 實現彈出的視圖
HMEmoticonPopView
popviewxib.png.jpeg
將此 View 的背景設置成透明色,并將 button 的類型設置成
HMEmoticonButton
- 連線到
HMEmoticonPopView.swift
,并提供從 xib 加載的方法
class HMEmoticonPopView: UIView {
@IBOutlet weak var emoticonButton: HMEmoticonButton!
class func popView() -> HMEmoticonPopView {
let result = NSBundle.mainBundle().loadNibNamed("HMEmoticonPopView", owner: nil, options: nil).last! as! HMEmoticonPopView
return result
}
}
- 監聽表情按鈕點擊,初始化控件,將控件添加到 window 上
// MARK: - 監聽事件
@objc private func emoticonButtonClick(button: HMEmoticonButton){
printLog("表情按鈕點擊")
if let emoticon = button.emoticon {
...
// 初始化 popView
let popView = HMEmoticonPopView.popView()
// 將 popView 添加到 window 上
let window = UIApplication.sharedApplication().windows.last!
window.addSubview(popView)
// 0.1 秒消失
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { () -> Void in
popView.removeFromSuperview()
}
}
}
- 顯示的位置不對:取到 button 在屏幕上的位置,并設置 popView 的位置
let rect = button.convertRect(button.bounds, toView: nil)
popView.centerX = CGRectGetMidX(rect)
popView.y = CGRectGetMaxY(rect) - popView.height
運行測試
- 顯示數據: 給
popView
添加emoticon
屬性
var emoticon: HMEmoticon? {
didSet{
emoticonButton.emoticon = emoticon
}
}
- 提取顯示
popView
代碼到HMEmoticonButton
中
/// 將傳入的 PopView 顯示在當前按鈕之上
///
/// - parameter popView: popView
func showPopView(popView: HMEmoticonPopView){
// 獲取到 button 按鈕在屏幕上的位置
let rect = convertRect(bounds, toView: nil)
// 設置位置
popView.centerX = CGRectGetMidX(rect)
popView.y = CGRectGetMaxY(rect) - popView.height
// 設置表情數據
popView.emoticon = emoticon
// 添加到 window 上
let window = UIApplication.sharedApplication().windows.last!
window.addSubview(popView)
}
- 外界調用
@objc private func emoticonButtonClick(button: HMEmoticonButton){
printLog("表情按鈕點擊")
if let emoticon = button.emoticon {
...
// 初始化 popView
let popView = HMEmoticonPopView.popView()
// 顯示 popView
button.showPopView(popView)
// 0.25 秒消失
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.25 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { () -> Void in
popView.removeFromSuperview()
}
}
}
長按滑動的時候氣泡隨著手指移動
實現思路
- 懶加載一個
popView
供長按拖動的時候顯示 - 監聽 cell 的長按 -> 添加長按手勢
- 在手勢監聽方法里面取到手指的位置
- 判斷手指的位置在哪一個按鈕之上
- 調用對應按鈕的
showPopView
方法 - 在手勢結束的時候隱藏
popView
代表實現
- 懶加載一個
popView
供長按拖動的時候顯示
/// 長按顯示的 popView
private lazy var popView = HMEmoticonPopView.popView()
- 給當前 cell 的 contentView 添加長按手勢
// 添加長按手勢事件
let longGes = UILongPressGestureRecognizer(target: self, action: "longPress:")
contentView.addGestureRecognizer(longGes)
- 監聽手勢事件,取到手指的位置
/// 長按手勢監聽
///
/// - parameter ges: 手勢
@objc private func longPress(ges: UILongPressGestureRecognizer) {
// 獲取當前手勢在指定 view 上的位置
let location = ges.locationInView(contentView)
printLog(location)
}
- 在
longPress
方法內部提供通過位置查找按鈕的方法
/// 長按手勢監聽
///
/// - parameter ges: 手勢
@objc private func longPress(ges: UILongPressGestureRecognizer) {
/// 根據位置查找到對應位置的按鈕
///
/// - parameter location: 位置
func findButtonWithLocation(location: CGPoint) -> HMEmoticonButton? {
for value in emoticonButtons {
if CGRectContainsPoint(value.frame, location) {
return value
}
}
return nil
}
// 獲取當前手勢在指定 view 上的位置
let location = ges.locationInView(contentView)
}
- 監聽手勢狀態
switch ges.state {
case .Began,.Changed:
// 通過手勢的位置查找到對應的按鈕
guard let button = findButtonWithLocation(location) where button.hidden == false else {
return
}
popView.hidden = false
button.showPopView(popView)
case .Ended:
popView.hidden = true
// 通過手勢的位置查找到對應的按鈕
guard let button = findButtonWithLocation(location) where button.hidden == false else {
return
}
emoticonButtonClick(button)
default:
// 將 popView 隱藏
popView.hidden = true
break
}