在iOS中使用Android中的.9图片

最近遇到一个需求,就是聊天的气泡需要个性化定制,类似于qq中的各式各样的聊天气泡。



之前也有聊天气泡,但是只有一种,所以直接用本地图片,使用iOS提供的API:

image = [image resizableImageWithCapInsets:UIEdgeInsetsMake(1, 1, 1, 1)];

或者使用assets中的slice工具:


那么现在要支持个性化的聊天气泡,有几个问题:
1、图片得从服务器下载,不能用本地的。
2、图片的capInsets参数根据不同的图片不一样,就像上图中的绿色气泡使用的是{34, 80, 21, 43},换一个气泡有可能就变成了{30, 20, 10, 50}。
3、iOS和Android最好能用一套方案。
3、气泡得有一个padding,这个padding是用来布局的,比如下面的气泡,蓝色框是气泡的大小,红色框是content的大小,padding就是两者的差值。有了这个padding,我们就知道content相对于气泡的位置了。


如果没有这种个性化需求,对于padding和capInsets,可以根据设计给的气泡图片直接在代码中写死,之前的版本就是这么干的。但是现在不能这么做了,padding和capInsets得一张气泡配置一个,那么把它放在服务器的接口中返回吗?当然可以这么做,但是这么做显得比较呆板。先看看Android的.9图是怎么处理的?

Android.9图

参考 https://blog.csdn.net/qq_26585943/article/details/68070043

1号黑色条位置向下覆盖的区域表示图片横向拉伸时,只拉伸该区域;
2号黑色条位置向右覆盖的区域表示图片纵向拉伸时,只拉伸该区域;
3号黑色条位置向左覆盖的区域表示图片纵向显示内容的区域(在手机上主要是文字区域);
4号黑色条位置向上覆盖的区域表示图片横向显示内容的区域(在手机上主要是文字区域);

从上面的描述中可以看出黑线1和2可以组成capInsets,而3和4可以组成padding。对比前面的Xcode中的slice工具,slice工具只指定了capInsets,并没有padding。因此Android中的.9工具满足我们的要求。

另外Android在项目中制作.9.png图之后,在打包到手机上面的时候,是不会有.9.png的,而是一张特殊的png图片,注意看上面的.9图片,有四条黑线,这个黑线只是在项目中便于你查看具体的capInsets和padding的位置而已,打包到手机上的时候,会根据.9图片生成另外一张png图片放到本地,而这张图片是不带4条黑线的。但是这张图片会携带capInsets和padding信息在png的chunkdata信息中,真正用到的时候Andorid会从png的chunkdata中读取这些信息应用到具体的布局和渲染上。当然这些Android都有专门的API帮你做好了,你不需要操心。然而iOS没有这一套的API,如果不想让运营的小姐姐在配置气泡的时候额外给iOS配置她不明所以的capInsets和padding的话,那么就要手动实现从png图片中读取这些信息了。

实现从png中读取capInsets和padding

参考这篇文章,感谢这位作者 https://blog.csdn.net/u013365670/article/details/25415393

上图是png格式的图片的chunkdata的格式,一个png可能有多个chunkdata,他们的chunktype各不相同,对于.9图片chunktype == 0x6E705463,所以我们要在png的字节流中不断遍历,直到找到chunktype == 0x6E705463的chunkdata为止,然后在chunkdata中读出对应的capInsets和padding信息。下面贴一段查找chunkdata的代码:

+ (NSData *)p_chunkDataWithImageData:(NSData *)data
{
    if (data == nil) {
        return nil;
    }
    
#define SNKNinePatchTestDataByte(a) if (a <= 0) { return nil; }
    
    uint32_t startPos = 0;
    uint32_t ahead1 = [self p_getByte4FromData:data startPos:&startPos];
    SNKNinePatchTestDataByte(ahead1)
    uint32_t ahead2 = [self p_getByte4FromData:data startPos:&startPos];
    SNKNinePatchTestDataByte(ahead2)
    if (ahead1 != 0x89504e47 || ahead2 != 0x0D0A1A0A) {
        return nil;
    }
    
    while (YES) {
        uint32_t length = [self p_getByte4FromData:data startPos:&startPos];
        SNKNinePatchTestDataByte(length)
        uint32_t type = [self p_getByte4FromData:data startPos:&startPos];
        SNKNinePatchTestDataByte(type)
        if (type != 0x6E705463) {
            startPos += (length + 4);
            continue;
        }
        return [data subdataWithRange:NSMakeRange(startPos, length)];
    }
    
    return nil;
    
#undef SNKNinePatchTestDataByte
}

要注意的是这个读取数据的过程需要判断大端小端模式,iOS应该都是小端模式,要转换成大端数据才可以,具体的实现可以看文末的链接。

图片下载,处理以及缓存

对于SDWebImage和YYWebImage这些第三方库,提供了对于普通Image的下载,处理以及缓存,但是气泡图片下载之后需要经过额外的处理,这些第三方库下载的Image不能直接使用;其次padding信息在布局的时候需要应用到具体的content上面。
为了方便项目中使用,封装了一下ImageView和Button类,下面是主要的代码:

- (void)addConstraintsWithPaddingView:(UIView *)paddingView
{
    NSAssert(self.superview && paddingView.superview && self.superview == paddingView.superview, @"paddingView 和 SNKNinePatchImageView 应该公有一个父 view");
    
    self.translatesAutoresizingMaskIntoConstraints = NO;
    
    [self setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
    [self setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisVertical];
    [self setContentHuggingPriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
    [self setContentHuggingPriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisVertical];
    
    NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:paddingView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0];
    self.topConstraint = topConstraint;
    NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:paddingView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0];
    self.leftConstraint = leftConstraint;
    NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:paddingView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0];
    self.bottomConstraint = bottomConstraint;
    NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:paddingView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:0];
    self.rightConstraint = rightConstraint;
    
    [self.superview addConstraints:@[topConstraint, leftConstraint, bottomConstraint, rightConstraint]];
    
    if (self.ninePatchImage) {
        [self p_updateConstraints];
    }
}

- (void)p_updateConstraints
{
    UIEdgeInsets padding = self.ninePatchImage.paddingCap.padding;
    self.topConstraint.constant = padding.top;
    self.leftConstraint.constant = padding.left;
    self.bottomConstraint.constant = -padding.bottom;
    self.rightConstraint.constant = -padding.right;
}

在使用的时候可以传一个paddingView利用addConstraintsWithPaddingView设置好上下左右约束,然后在后续设置ninePatchImage的时候根据padding信息来更新约束。上面将ImageView自身的各个ContentPriority都设为Low是为了让paddingView的内容驱动气泡大小变化,而不是反过来。

- (void)setImageWithUrlStr:(NSString *)urlStr
{
    SNKNinePatchImage *ninePatchImage = [SNKNinePatchImageCache ninePatchImageNamed:urlStr];
    if (ninePatchImage) {
        self.ninePatchImage = ninePatchImage;
        return;
    }
    
    __weak SNKNinePatchImageView *weakSelf = self;
    [self yy_setImageWithURL:[NSURL URLWithString:urlStr]
                 placeholder:nil
                     options:kNilOptions
                    progress:nil
                   transform:^UIImage * _Nullable(UIImage * _Nonnull image, NSURL * _Nonnull url) {
                       [weakSelf p_checkSetImageForUrl:url urlStr:urlStr];
                       return image;
                   }
                  completion:^(UIImage * _Nullable image, NSURL * _Nonnull url, YYWebImageFromType from, YYWebImageStage stage, NSError * _Nullable error) {
                      [weakSelf p_checkSetImageForUrl:url urlStr:urlStr];
                   }];
}

- (void)setNinePatchImage:(SNKNinePatchImage *)ninePatchImage
{
    _ninePatchImage = ninePatchImage;
    
    self.image = ninePatchImage.image;
    if (ninePatchImage) {
        [self p_updateConstraints];
    }
}

- (void)p_checkSetImageForUrl:(NSURL *)url urlStr:(NSString *)urlStr
{
    YYWebImageManager *manager = [YYWebImageManager sharedManager];
    NSString *key = [manager cacheKeyForURL:url];
    NSData *data = (NSData *)[[YYWebImageManager sharedManager].cache getImageDataForKey:key];
    if (data) {
        [self p_checkSetImageWithData:data urlStr:urlStr];
    } else {
        __weak SNKNinePatchImageView *weakSelf = self;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSData *data = (NSData *)[[YYWebImageManager sharedManager].cache getImageDataForKey:key];
            [weakSelf p_checkSetImageWithData:data urlStr:urlStr];
        });
    }
}

- (void)p_checkSetImageWithData:(NSData *)data urlStr:(NSString *)urlStr
{
    SNKNinePatchImage *ninePatchImage = [SNKNinePatchImage ninePatchImageWithImageData:data scale:self.imageScale];
    [SNKNinePatchImageCache setNinePatchImage:ninePatchImage forName:urlStr];
    self.ninePatchImage = ninePatchImage;
}

在获取网络图片的时候使用了YYWebImage,由于YYWebImage缓存的是没有resible的image,因此我自己做了个简单的内存缓存,用来缓存resible之后的SNKNinePatchImage实例,以避免在滑动的时候卡顿。

这种实现也有不好的地方:1、需要Android同学每次将设计的气泡图片导成带有capInsets和padding信息的png,然后再上传到后台;2、如果使用本地的png图片,不要将它放在assets中,因为assets中的图片在打包的时候会压缩到assets.car中,目前还没有找到从assets.car中读取NSData的方法

用法:

[self.imageView2 addConstraintsWithPaddingView:self.label2];
self.imageView2.ninePatchImage = [SNKNinePatchImage ninePatchImageWithName:@"xxxx"];
[self.imageView2 setImageWithUrlStr:@"xxxx"];

demo在这里 https://github.com/tujinqiu/KTNinePatchImage

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 142,680评论 1 300
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,177评论 1 256
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 94,249评论 0 212
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,981评论 0 175
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,746评论 1 255
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,763评论 1 176
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,362评论 2 268
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,151评论 0 165
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,964评论 6 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,501评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,285评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,614评论 1 229
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,232评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,117评论 2 213
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,498评论 3 204
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,615评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,012评论 0 167
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,512评论 2 230
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,594评论 2 231

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,374评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,390评论 2 59
  • 今天分享一部根据英国日裔小说作家石黑一雄的同名原作改编的电影《别让我走》。电影画面优美,人物青春靓丽,每个镜头犹如...
    微影流年阅读 270评论 1 0
  • 2016.9.7 秋季服务商大会结束了,会议上公司一再强调了每天4+1+1的重要性,会后开始每天践行4+1+1的行...
    宁昕阅读 332评论 0 0
  • Do you love to travel and what kind of travel do you want...
    Jenna_King阅读 174评论 0 0