在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

 


作者:KevinTing
鏈接:https://www.jianshu.com/p/b54cbb02abad
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM