13-1图文混排

图文混排

实现效果

13-1图文混排_第1张图片
表情图文混排.png.jpeg

表情按钮点击事件

  • HMEmoticonPageCell 中监听表情按钮点击 -- 在添加按钮的时候添加
/// 添加表情按钮
private func addEmoticonButtons(){
    for _ in 0..
  • 实现点击方法
/// 表情按钮点击
///
/// - 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:在长按滑动的时候气泡随着手指移动

实现效果

13-1图文混排_第2张图片
表情点击气泡.png.jpeg

点击表情按钮弹出一个气泡

实现思路

  1. 气泡可以使用 xib 实现
  2. 点击表情按钮的时候取到对应表情按钮的位置
  3. 将位置转化成在 window 上的位置
  4. 根据将气泡添加到最上层的 Window 上
  5. 0.1 秒之后气泡从 window 上移除

代码实现

  • 使用 xib 实现弹出的视图 HMEmoticonPopView
13-1图文混排_第3张图片
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
}

你可能感兴趣的:(13-1图文混排)