美文网首页
13-1图文混排

13-1图文混排

作者: 月下独酌灬 | 来源:发表于2016-05-29 23:49 被阅读233次

    图文混排

    实现效果

    表情图文混排.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
    }
    

    相关文章

      网友评论

          本文标题:13-1图文混排

          本文链接:https://www.haomeiwen.com/subject/nhbqdttx.html