13-1圖文混排

圖文混排

實現效果

表情圖文混排.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?
}
  • 更改 HMEmoticonPageCellemoticonButtons 數據類型
// 裝有所有表情按鈕的集合
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 中的 emoticondidSet 方法中
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)
        }
    }
}
  • 更改 HMEmoticonPageCellemoticonsdidSet 方法
/// 當前頁顯示的表情數據
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"])
}

運行測試

  • 圖文混排邏輯
    1. 通過現有的 attributedText 初始化一個 NSMutableAttributedString
    2. 通過表情圖片初始化一個 NSTextAttachment 對象
    3. 通過第 2 步的 attachment 對象初始化一個 NSAttributedString
    4. 將第 3 步的 attributedString 添加到第 1 步的可變的 NSMutableAttributedString
    5. 將第 4 步的結果賦值給 textViewattributedText

注意區分 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))

運行測試:發現表情圖片偏上,調整 attachmentbounds

// 調整圖片大小 --> 解決圖片大小以及偏移問題
attatchment.bounds = CGRectMake(0, -4, imageWH, imageWH)

運行測試:發現當光標不在最后一位的時候,表情圖片依然拼在最后面,解決辦法就是調用 NSMutableAttributedStringinsertAttributedString 的方法,傳入 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 中監聽通知
    • 在通知的方法中調用 textViewdeleteBackward 方法
  • 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) {

    }
}
  • 更改 HMComposeViewControllertextView 的類型
/// 輸入框
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

點擊表情按鈕彈出一個氣泡

實現思路

  1. 氣泡可以使用 xib 實現
  2. 點擊表情按鈕的時候取到對應表情按鈕的位置
  3. 將位置轉化成在 window 上的位置
  4. 根據將氣泡添加到最上層的 Window 上
  5. 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()
        }
    }
}

長按滑動的時候氣泡隨著手指移動

實現思路

  1. 懶加載一個 popView 供長按拖動的時候顯示
  2. 監聽 cell 的長按 -> 添加長按手勢
  3. 在手勢監聽方法里面取到手指的位置
  4. 判斷手指的位置在哪一個按鈕之上
  5. 調用對應按鈕的 showPopView 方法
  6. 在手勢結束的時候隱藏 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
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,241評論 4 61
  • “有志者事竟成” ,就算是兩座大山,下定決心除掉它,只要堅持不懈,那么總有實現的一天。 這個故事...
    73c761794ff0閱讀 315評論 0 0
  • 懂事一旦成為標簽,連正常訴求的需要也變成了無理取鬧,于是你拼命往后退,拔掉刺,在塵埃里活出卑微的樣子 熟睡的深夜...
    流光影跡閱讀 863評論 0 5
  • 今日上完課,下午有完整的時間畫畫了。臨摹心藍老師的一幅作品,嘗試水溶彩鉛的正確打開方式。 附一張:美術生送給我的教...
    唱媽閱讀 1,045評論 2 1