美文网首页iOSiOS之框架架构IOS文章收集
TextKit入门? 表情键盘? 图文混排? --看我就够了

TextKit入门? 表情键盘? 图文混排? --看我就够了

作者: 呆子是只喵 | 来源:发表于2016-09-17 18:39 被阅读3815次

    前言

    现在市场上大部分信息展示类应用都有用到图文混排,我之前对这块也是一知半解,最近放假正好深入了解一下这块,在这里对此做一个总结,废话不多说,先上 demo 地址
    TextKit入门 demo地址
    精仿手工课 demo地址
    精仿手工课博客地址

    背景知识

    在正式开始学习之前,我们先来了解一下图文混排在 iOS 上面的发展,抛弃 coretext (涉及到一些底层,这里不讨论)不说,苹果在 iOS6给出了*** NSMutableAttributedString这个类,简单的说就是带属性的字符串,用它可以实现图文混排,在 iOS7苹果则给出了更加强大的 API--textKit,用 textKit***可以实现更加复杂的界面,接下来让我们开始浪起来吧

    1.gif

    富文本(NSMutableAttributedString)

    NSMutableAttributedString和数组一样分为可变字符串和不可变字符串, NSAttributedString就是不可变字符串
    NSMutableAttributedString简单的说就是一个带属性的字符串,因此它的使用非常简单,

    • 1.初始化字符串
    • 2.初始化字符串所需要的属性
    • 3.将属性赋值给字符串
      初始化方法
      - (instancetype)initWithString:(NSString *)str;
      - (instancetype)initWithString:(NSString *)str attributes:(nullable NSDictionary<NSString *, id> *)attrs;
      - (instancetype)initWithAttributedString:(NSAttributedString *)attrStr;
      初始化属性
    • NSFontAttributeName 字体
    • NSForegroundColorAttributeName 字体颜色
    • NSBackgroundColorAttributeName 背景颜色
    • NSLigatureAttributeName 连字符
      该属性所对应的值是一个 NSNumber 对象(整数)。连体字符是指某些连在一起的字符,它们采用单个的图元符号。
      0 表示没有连体字符。
      1 表示使用默认的连体字符。
      2 表示使用所有连体符号。默认值为 1(注意,iOS 不支持值为 2)。
    • NSParagraphStyleAttributeName 段落
      该属性所对应的值是一个 NSParagraphStyle 对象。该属性在一段文本上应用多个属性。如果不指定该属性,则默认为 NSParagraphStyle 的defaultParagraphStyle 方法返回的默认段落属性
    • NSKernAttributeName 字间距
      该属性所对应的值是一个 NSNumber 对象(整数)。字母紧排指定了用于调整字距的像素点数。字母紧排的效果依赖于字体。值为 0 表示不使用字母紧排。默认值为0
    • NSStrikethroughStyleAttributeName 删除线
    • NSUnderlineStyleAttributeName 下划线
    • NSStrokeColorAttributeName 边线颜色
      该属性所对应的值是一个 UIColor 对象。如果该属性不指定(默认),则等同于 NSForegroundColorAttributeName。否则,指定为删除线或下划线颜色。更多细节见“Drawing attributedstrings that are both filled and stroked”
    • NSStrokeWidthAttributeName 边线宽度
    • NSShadowAttributeName 阴影
    • NSVerticalGlyphFormAttributeName 横竖排版
      实例
     // 设置颜色等
        NSMutableDictionary *arrDic = [NSMutableDictionary dictionary];
        arrDic[NSForegroundColorAttributeName] = [UIColor purpleColor];
        arrDic[NSBackgroundColorAttributeName] = [UIColor greenColor];
        arrDic[NSKernAttributeName] = @10;
        arrDic[NSUnderlineStyleAttributeName] = @1;
        
        NSMutableAttributedString *attriOneStr = [[NSMutableAttributedString alloc]initWithString:@"来呀,快活呀,反正有大把时光" attributes:arrDic];
        self.oneLabel.attributedText = attriOneStr;
        
        // 简单的图文混排
        NSMutableAttributedString *arrTwoStr = [[NSMutableAttributedString alloc]init];
        NSMutableAttributedString *TwoChildStr = [[NSMutableAttributedString alloc]initWithString:@"你好啊"];
        [arrTwoStr appendAttributedString:TwoChildStr];
        
        NSTextAttachment *attachMent = [[NSTextAttachment alloc]init];
        attachMent.image = [UIImage imageNamed:@"2"];
        attachMent.bounds = CGRectMake(0, -5, 20, 20);
        NSAttributedString *picStr = [NSAttributedString attributedStringWithAttachment:attachMent];
        [arrTwoStr appendAttributedString:picStr];
        
        NSAttributedString *TwooStr = [[NSAttributedString alloc]initWithString:@"我是小菜鸟"];
        [arrTwoStr appendAttributedString:TwooStr];
        self.twoLabel.attributedText = arrTwoStr;
    

    效果

    简单.png

    表情键盘,富文本实现图文混排

    表情键盘

    Snip20160917_5.png
    平常大家用的 QQ,微信等APP中随处可见表情键盘,在做表情键盘前,先来了解一下什么是表情.
    日常生活中,所用到的表情一般为两种--图片表情,emoji表情
    emoji表情的本质就是字符串,比如 0x1f601:,在显示的时候我们需要将字符串转成表情
    图片表情就是一张图片,比如这是一个表情信息,我们根据png来加载缓存在本地的图片,并显示.
    图片表情.png

    实现思路

    考虑到表情的两种表现形式,决定用 Button 来实现,这样可以方便的显示字体或者图片

    • 初始化一个 ListView, 并添加一个 UIScrollview 子控件
    • UIScrollview添加n个(需要几个表情种类添加几个) gridView
    • gridView上面添加 Button来显示表情

    表情键盘代码###

    由于代码量较大,这里上一些核心代码,具体代码可以下载,下来再看TextKit入门 demo地址
    表情工具类加载所需表情

    + (NSArray *)defaultEmotions
    {
        if (!_defaultEmotions) {
            NSString *plist = [[NSBundle mainBundle] pathForResource:@"EmotionIcons/default/info.plist" ofType:nil];
            _defaultEmotions = [GPEmotion mj_objectArrayWithFile:plist];
            [_defaultEmotions makeObjectsPerformSelector:@selector(setDirectory:) withObject:@"EmotionIcons/default"];
        }
        return _defaultEmotions;
    }
    
    + (NSArray *)emojiEmotions
    {
        if (!_emojiEmotions) {
            NSString *plist = [[NSBundle mainBundle] pathForResource:@"EmotionIcons/emoji/info.plist" ofType:nil];
            _emojiEmotions = [GPEmotion mj_objectArrayWithFile:plist];
            [_emojiEmotions makeObjectsPerformSelector:@selector(setDirectory:) withObject:@"EmotionIcons/emoji"];
        }
        return _emojiEmotions;
    }
    
    + (NSArray *)lxhEmotions
    {
        if (!_lxhEmotions) {
            NSString *plist = [[NSBundle mainBundle] pathForResource:@"EmotionIcons/lxh/info.plist" ofType:nil];
            _lxhEmotions = [GPEmotion mj_objectArrayWithFile:plist];
            [_lxhEmotions makeObjectsPerformSelector:@selector(setDirectory:) withObject:@"EmotionIcons/lxh"];
        }
        return _lxhEmotions;
    }
    
    + (NSArray *)recentEmotions
    {
        if (!_recentEmotions) {
            _recentEmotions = [NSKeyedUnarchiver unarchiveObjectWithFile:GPRecentFilepath];
            if (!_recentEmotions) {
                _recentEmotions = [NSMutableArray array];
            }
        }
        return _recentEmotions;
    }
    

    表情数据传递

    • 点击所需表情传递给 ListView
      - (void)setEmtiontype:(GPEmtionType)type
    {
        switch (type) {
            case GPEmotionTypeRecent: {
                self.listView.emotions = [GPEmtionTool recentEmotions];
                break;
            }
            case GPEmotionTypeDefault: {
                self.listView.emotions = [GPEmtionTool defaultEmotions];
                
                break;
            }
            case GPEmotionTypeEmoji: {
                self.listView.emotions = [GPEmtionTool emojiEmotions];
                break;
            }
            case GPEmotionTypeLxh: {
                self.listView.emotions = [GPEmtionTool lxhEmotions];
                break;
            }
        }
    }
    
    • LIstView 传递给GridView
      - (void)setEmotions:(NSArray *)emotions
    {
        _emotions = emotions;
        
        NSInteger totlas = (emotions.count + GPEmotionMaxCountPerPage - 1) / GPEmotionMaxCountPerPage;
        NSInteger currrentGridViewCount = self.scrollView.subviews.count;
        
        self.pageControl.numberOfPages = totlas;
        self.pageControl.currentPage = 0;
        GPEmottionGridView *gridView = nil;
        for (NSInteger i = 0; i < totlas; i ++) 
            if (i >= currrentGridViewCount) {
                gridView = [[GPEmottionGridView alloc]init];
                [self.scrollView addSubview:gridView];
            } else {
                gridView = self.scrollView.subviews[i]
            }
            NSInteger loc = i * GPEmotionMaxCountPerPage;
            NSInteger len = GPEmotionMaxCountPerPage;
            if (loc + len > emotions.count) {
                len = emotions.count - loc;
            }
            NSRange range = NSMakeRange(loc, len);
            NSArray *gridViewemotionS = [emotions subarrayWithRange:range];
            gridView.emotions = gridViewemotionS;
            gridView.hidden = NO
        
        for (NSInteger i = totlas; i<currrentGridViewCount; i++) {
            GPEmottionGridView *gridView = self.scrollView.subviews[i];
            gridView.hidden = YES;
        
           [self setNeedsLayout];
        self.scrollView.contentOffset = CGPointZero
    
    • gridView 传递给 btn
      - (void)setEmotions:(NSArray *)emotions
    {
        _emotions = emotions;
        
        NSInteger count = emotions.count;
        NSInteger currentEmotionViewCount = self.emotionViews.count;
        for (int i = 0; i<count; i++) {
            GPEmotionView *emotionView = nil;
            
            if (i >= currentEmotionViewCount) {
                emotionView = [[GPEmotionView alloc] init];
                [emotionView addTarget:self action:@selector(emotionClick:)           forControlEvents:UIControlEventTouchUpInside];
                [self addSubview:emotionView];
                [self.emotionViews addObject:emotionView];
            } else {
                emotionView = self.emotionViews[i];
            }
            // 传递模型数据
            emotionView.emotion = emotions[i];
           
          for NSInteger i = count; i<currentEmotionViewCount; i++) 
            UIButton *emotionView = self.emotionViews[i];
            emotionView.hidden = YES;
        ```
    * Btn 展示表情
    
    • (void)setEmotion:(GPEmotion *)emotion
      {
      _emotion = emotion;

      if (emotion.code) {
      self.titleLabel.font = [UIFont systemFontOfSize:32];
      [self setTitle:emotion.emoji forState:UIControlStateNormal];
      [self setImage:nil forState:UIControlStateNormal];
      } else { // 图片表情
      NSString *icon = [NSString stringWithFormat:@"%@/%@", emotion.directory, emotion.png];
      UIImage *image = [UIImage imageNamed:icon];
      [self setImage: [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]forState:UIControlStateNormal];
      [self setTitle:nil forState:UIControlStateNormal];
      }
      }

    ###图文混排实现
    
    
    ![Uploading Snip20160917_5_983709.png . . .]](http:https://img.haomeiwen.com/i694552/193ab88b3b973702.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    **实现思路**
    * 获得后台的字符串,然后用正则表达式将字符串分割为表情和非表情两部分,然后将其转换为**富文本字符串**,并在匹配到超链接时候给其绑定一个 key, 随后将分割后的结果按顺序排好,
    
    * 将之前转换后的富文本字符串赋值为 `UITextView` 的`attributedText`属性
    
    * 点击超链接,判断当前点是否在链接范围之内,若在就打开链接
    
    **图文混排核心代码**
    
    * 普通字符串转换为富文本字符串
             - (NSAttributedString *)creatArrtext:(NSString *)text
          { NSArray *regexResults = [GPEmtionTool regexResultsWithText:text];
          NSMutableAttributedString *attributedString =   [[NSMutableAttributedString alloc] init];
           [regexResults enumerateObjectsUsingBlock:^(GPRegexResult *result,   NSUInteger idx, BOOL *stop) {
            GPEmotion *emotion = nil;
            if (result.isEmotion) { // 表情
                emotion = [GPEmtionTool emotionWithDesc:result.string];
            }
            
            if (emotion) { // 如果有表情
                // 创建附件对象
                GPEmotionAttachment *attach = [[GPEmotionAttachment alloc] init];
                
                // 传递表情
                attach.emotion = emotion;
                attach.bounds = CGRectMake(0, -3, GPStatusOrginalTextFont.lineHeight, GPStatusOrginalTextFont.lineHeight);
                
                // 将附件包装成富文本
                NSAttributedString *attachString = [NSAttributedString attributedStringWithAttachment:attach];
                [attributedString appendAttributedString:attachString];
            } else { // 非表情(直接拼接普通文本)
                NSMutableAttributedString *substr = [[NSMutableAttributedString alloc] initWithString:result.string];
            
                // 匹配超链接
                NSString *httpRegex = @"http(s)?://([a-zA-Z|\\\\d]+\\\\.)+[a-zA-Z|\\\\d]+(/[a-zA-Z|\\\\d|\\\\-|\\\\+|_./?%&=]*)?";
                [result.string enumerateStringsMatchedByRegex:httpRegex usingBlock:^(NSInteger captureCount, NSString *const __unsafe_unretained *capturedStrings, const NSRange *capturedRanges, volatile BOOL *const stop) {
                    [substr addAttribute:NSForegroundColorAttributeName value:[UIColor greenColor] range:*capturedRanges];
                    
                    // 绑定一个key
                    [substr addAttribute:GPLinkText value:*capturedStrings range:*capturedRanges];
                }];
                
                [attributedString appendAttributedString:substr];
            }
          }];
        
          // 设置字体
          [attributedString addAttribute:NSFontAttributeName   value:GPStatusOrginalTextFont range:NSMakeRange(0,   attributedString.length)];
        
          return attributedString;
          }
    
    * 点击链接跳转
    
    • (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
      {
      UITouch *touch = [touches anyObject];
      CGPoint point = [touch locationInView:touch.view];
      CGPoint pp = [self convertPoint:point toView:self.textView];
      GPLink *touchingLink = [self touchingLinkWithPoint:pp];
    if (touchingLink) {
        [[NSNotificationCenter defaultCenter] postNotificationName:GPLinkDidSelectedNotification object:nil userInfo:@{GPLinkText : touchingLink.text}];
    }
    
    [self touchesCancelled:touches withEvent:event];
    

    }

    • (GPLink *)touchingLinkWithPoint:(CGPoint)point
      {
      __block GPLink *touchingLink = nil;
      [self.links enumerateObjectsUsingBlock:^(GPLink *link, NSUInteger idx, BOOL *stop) {
      for (UITextSelectionRect *selectionRect in link.rects) {
      if (CGRectContainsPoint(selectionRect.rect, point)) {
      NSLog(@"选中%@",NSStringFromCGRect(selectionRect.rect));
      touchingLink = link;
      break;
      }
      }
      }];
      return touchingLink;
      }
    # TextKit
    ![TextKit.png](http:https://img.haomeiwen.com/i694552/21b677318e171048.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    接下来,我们来看今天的重头戏` TextKit`,首先来看看`textKit` 的类
    * `NSTextStorage`: 平时用到的字符串 **string**,这个类里面包含着 string的属性,比如颜色,大小等都是这个类来管理
    * `NSLayoutManager`: 这个就是管理中心,负责布局渲染
    * `NSTextContainer` : 就是可以渲染的范围
    * `UITextView` :就是我们平时用的控件,用来给用户展示数据
    那么,我么可以用这些类来做哪些事情呢,看一些实例
    ### 阅读排版
    
    ![阅读排版.gif](http:https://img.haomeiwen.com/i694552/9463e4514a639b67.gif?imageMogr2/auto-orient/strip)
    
    
    **阅读排版实现思路**
    * 创建n 个 `textView`,共用一套`NSLayoutManager`
    
    **阅读排版核心代码**
    
    • (void)setupTextKit
      {
      NSURL *contentUrl = [[NSBundle mainBundle]URLForResource:@"content" withExtension:@"txt"];

      self.textStorage = [[NSTextStorage alloc]initWithFileURL:contentUrl options:[NSDictionary dictionary] documentAttributes:nil error:nil];
      self.layoutManager = [[NSLayoutManager alloc]init];

      [self.textStorage addLayoutManager:self.layoutManager];
      }

    • (void)layoutconter
      {
      NSInteger totlaGlyph = 0;
      CGFloat X = 0;
      while (totlaGlyph < self.layoutManager.numberOfGlyphs) {

        NSTextContainer *textContainer = [[NSTextContainer alloc]initWithSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT)];
        
        [self.layoutManager addTextContainer:textContainer];
        
        UITextView *textView = [[UITextView alloc]initWithFrame:CGRectMake(X, 0, SCREEN_WIDTH, self.pageScrollView.height) textContainer:textContainer];
        textView.scrollEnabled = NO;
        textView.font = [UIFont systemFontOfSize:20];
        [self.pageScrollView addSubview:textView];
        
        X += SCREEN_WIDTH;
        
        totlaGlyph = NSMaxRange([self.layoutManager glyphRangeForTextContainer:textContainer]);
      

      }

      CGSize contentSize = CGSizeMake(X, self.pageScrollView.height);
      self.pageScrollView.pagingEnabled = YES;
      self.pageScrollView.contentSize = contentSize;
      self.pageScrollView.showsHorizontalScrollIndicator = NO;

    }

    ### 高亮文字,URL, 保持 URL 在一行是一个整体
    
    **高亮文字,URL实现思路**
    * 可以自定义`NSTextStorage`,这里注意`NSTextStorage`继承自`NSMutableAttributedString`,所以必须实现以下方法:
    `  - (NSString *)string;`
     ` - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range;`
     ` - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str;`
     ` - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range;`
    
    * 在`- (void)processEditing`中匹配高亮字符,匹配 URL, 并高亮
    * 在 `NSLayoutManagerDelegate`实现 url 保持整体不换行
    **高亮文字,URL核心代码**
    
    
    • (void)processEditing
      {

      NSRegularExpression *expression = [[NSRegularExpression alloc]initWithPattern:@"a[\\b{Alphabetic}&&\\b{Uppercase}][\\br{Alphabetic}]+" options:NSRegularExpressionCaseInsensitive error:nil];

      NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange];
      [self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];

      [expression enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
      [self addAttribute:NSForegroundColorAttributeName value:[UIColor greenColor] range:result.range];
      }];
      [super processEditing];
      }

    • (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
      {
      [_imp replaceCharactersInRange:range withString:str];
      [self edited:NSTextStorageEditedCharacters range:range changeInLength:(NSInteger)str.length - (NSInteger)range.length];

      NSDataDetector *datector = [[NSDataDetector alloc]initWithTypes:NSTextCheckingTypeLink error:nil];

      NSRange paragaphRange = [self.string paragraphRangeForRange: NSMakeRange(range.location, str.length)];

      [self removeAttribute:NSLinkAttributeName range:paragaphRange];
      [self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];

      [datector enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {

        [self addAttribute:NSLinkAttributeName value:result.URL range:result.range];
        [self addAttribute:NSForegroundColorAttributeName value:[UIColor greenColor] range:result.range];
      

      }];
      }

    
    
    • (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex
      {
      NSRange range;
      NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName atIndex:charIndex effectiveRange:&range];

      if (linkURL && charIndex > range.location && charIndex <= NSMaxRange(range))
      return NO;
      else
      return YES;
      }

    **机智的你一定发现,其实利用 TextKit也可以实现图文混排,机智的你自己试一试**
    ###环绕布局
    
    ![环绕布局.png](http:https://img.haomeiwen.com/i694552/93d9a2037da33412.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    如图所示;大家用 Word 的时候插入图片,使得文字环绕在图片周围,哈哈,好消息来了, iOS 也可以实现哦
    **环绕布局实现思路**
    * 记得`textContainer`,我们可以直接给其属性赋值一个` path`, 就相当于在一张纸上裁剪掉一部分,那么自然就不会在那部分渲染文字了
    **环绕布局核心代码**
    
    • (void)setupImage
      {

      UIImageView *imageView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"5"]];
      imageView.center = self.view.center;
      CGRect ofram = [self.textView convertRect:imageView.bounds fromView:imageView];
      ofram.origin.y = ofram.origin.y - 64;
      UIBezierPath *path = [UIBezierPath bezierPathWithRect:ofram];

      self.textView.textContainer.exclusionPaths = @[path];

      [self.view addSubview:imageView];
      }

    #总结
    这只是一些简单的使用,机智的你肯定有更多想法,互相学习吧
    
    >参考
    [obJc,初始 textKit](https://objccn.io/issue-5-1/)
    [MJ](https://github.com/CoderMJLee/MJExtension)

    相关文章

      网友评论

      本文标题:TextKit入门? 表情键盘? 图文混排? --看我就够了

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