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
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,966评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,170评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,909评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,959评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,851评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,583评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,956评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,590评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,878评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,892评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,719评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,501评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,957评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,124评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,440评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,003评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,211评论 2 339

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,966评论 4 60
  • “有志者事竟成” ,就算是两座大山,下定决心除掉它,只要坚持不懈,那么总有实现的一天。 这个故事...
    73c761794ff0阅读 297评论 0 0
  • 懂事一旦成为标签,连正常诉求的需要也变成了无理取闹,于是你拼命往后退,拔掉刺,在尘埃里活出卑微的样子 熟睡的深夜...
    流光影迹阅读 828评论 0 5
  • 今日上完课,下午有完整的时间画画了。临摹心蓝老师的一幅作品,尝试水溶彩铅的正确打开方式。 附一张:美术生送给我的教...
    唱妈阅读 996评论 2 1