iOS点九图NinePatch解析

1. 背景

项目有个web页面卡片类型UI,卡片有不同宽高大小。现在想在卡片上增加一个封面边框,设计给出的切图

,在不同卡片宽高时候,需要展示示意图如

,要求原切图右上角区域维持不变,其他可以适应宽高拉伸。

2. 方案

首先得选择,自然是点九图(NinePatch)来实现需求。点九图是android系统中特有的图片格式,包含有定义可拉伸区域的信息,用于做局部拉伸。iOS在处理这种图片,也是非常方便的,有相关的系统函数可以做处理,

- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets resizingMode:(UIImageResizingMode)resizingMode;

该函数返回一张被拉伸(压缩)之后的image图片,在UIImageView上展示即是拉伸(压缩)之后的效果。
函数需要传入两个参数,
capInsetsresizingMode

capInsets: UIEdgeInsets{CGFloat top, left, bottom, right}, 定义了受保护区域,除去受保护区域,剩下则是可拉伸区域;
resizingMode: 图片拉伸模式,两种取值,UIImageResizingModeTile平铺和UIImageResizingModeStretch拉伸;

两个参数更具体的说明和影响效果,可以参考文章,文章针对不同取值有详细的demo和说明,

iOS图片拉伸(resizableImage)

https://www.jianshu.com/p/84848c1b2d47

我们更关注的是capInsets,定义了图片的受保护区域,用一张图来示例,如下图,top、left、bottom、right指定的四个绿色边角是受保护区域,不可拉伸;而中间的蓝色区域则是可以拉伸的。

由于不同的切图,其受保护区域(可拉伸区域)不同,调用函数resizableImageWithCapInsets就需要视觉或者开发同学给出不同的capInsets值,对于硬编码来说是很不方便的。那么,有没有一种自动确定capInsets的方法呢?

有的,我们从点九图制作生成说起。

2.1 点九图制作生成

官方文档

Create resizable bitmaps (9-patch files)

https://developer.android.com/studio/write/draw9patch

在android studio里面制作一张点九图(.9.png)。该点九图有上下左右四个边有一条1像素的黑线,用于标注拉伸区域和显示内容区域,例如

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


然而,包含4个黑边的.9.png图片,并不会用于真正的图片展示,真正用于手机展示的图片,需要使用工具来对.9.png做处理之后生成新的点九图,具体的说步骤为:

  1. 设计师或者产品给出原始切图top1.png;
  2. 使用android studio制作包含4个黑边点九图top1.9.png;
  3. 使用android sdk 目录下的 aapt 工具将点九图转化为png图片 top1_out.png;

  4. aapt工具是android sdk目录下,可以在Android Studio Preferences | Languages & Frameworks | Android SDK找到sdk location,如果没有sdk,则需要手动安装android sdk,然后找到location,aapt在我机器参考目录为~/Library/Android/sdk/build-tools/34.0.0,执行命令如下:
  5. ./aapt s -i top1.9.png -o top1_out.png
  6. 本地使用该 top1_out.png图片,或者将图片上传至网络cdn,拿到图片url;

这里第3步,aapt会把4个黑边的点九图信息,写入到结果png图片中的chunkdata数据中,并且去掉4个1像素的黑边,这样得到一张可用于手机展示的点九图片。其关键信息都在写在png的点九chunkdata里面,那么我们怎么获取图片的点九图信息呢?

我们从PNG文件格式着手。

2.2 PNG文件格式

PNG文件格式是有标准规范的,

PNG Specification

http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html


PNG格式文件由一个8字节的PNG文件标识(file signature or file header)域和3个以上的后续数据块(chunk)如IHDR、IDAT、IEND等组成。

PNG文件标识

数据块

……

数据块

0x 89 50 4E 47 0D 0A 1A 0A







Length

4 bytes

指定本数据块中Chunk Data的长度

Chunk Type

4 bytes

数据块类型码,由ASCII字母组成的”数据块符号”

Chunk Data

Length bytes

数据

CRC

4 bytes

循环冗余码

长度/数字都是网络字节序, All integers that require more than one byte must be in network byte order

如下图是一个实际png文件hex格式展示:

2.3 PNG点九图数据解析

我们知道aapt会把点九NinePatch信息写入到PNG的chunk中,那么怎么知道其chunk的类型以及数据结构呢?
aapt处理点九图相关代码在tools/aapt/Images.cpp,以及从android源码中,对应点九图NinePatch解析代码以及头文件定义,

ResourceTypes.h

https://android.googlesource.com/platform/frameworks/base/+/56a2301/include/androidfw/ResourceTypes.h


*
 * The PNG chunk type is "npTc".
 */
struct Res_png_9patch
{
    Res_png_9patch() : wasDeserialized(false), xDivs(NULL),
                       yDivs(NULL), colors(NULL) { }
    int8_t wasDeserialized;
    int8_t numXDivs;
    int8_t numYDivs;
    int8_t numColors;
    // These tell where the next section of a patch starts.
    // For example, the first patch includes the pixels from
    // 0 to xDivs[0]-1 and the second patch includes the pixels
    // from xDivs[0] to xDivs[1]-1.
    // Note: allocation/free of these pointers is left to the caller.
    int32_t* xDivs;
    int32_t* yDivs;
    int32_t paddingLeft, paddingRight;
    int32_t paddingTop, paddingBottom;

可以还原出数据块类型码是npTc,对应的数据结构,网上已经有文章总结,我就直接引用了NinePatch数据结构:

变量

长度:byte

说明

wasDeserialized

1

无意义,非0即可

numXDivs

1

上方黑点标记的数量,即可以多段标记,xDivs数组的数量

numYDivs

1

左方黑点标记的数量,即可以多段标记,yDivs数组的数量

numColors

1

颜色数量

xDivsOffset

4

xDivs 内存起始偏移,方便直接定位到 xDivs

yDivsOffset

4

yDivs 内存起始偏移,方便直接定位到 yDivs

paddingLeft

4

右方和下方的黑线标记,padding

paddingRight

4

右方和下方的黑线标记,padding

paddingTop

4

右方和下方的黑线标记,padding

paddingBottom

4

右方和下方的黑线标记,padding

colorOffset

4

Colors 内存起始偏移,方便直接定位到 Colors

xDivs

numXDivs*4

上方黑点标记数组,表示横向拉伸区域

yDivs

numYDivs*4

左方黑点标记数组,表示纵向拉伸区域

Colors

numColors*4

Sample

这里,包含可拉伸区域的数组xDivs和yDivs,用于指定如何将图像分割成多个部分进行拉伸缩放,

xDivs描述了拉伸区域水平方向的起始位置和结束位置
yDivs描述了拉伸区域垂直方向的起始位置和结束位置

更具体和详细的字段定义和理解,仍然参考文章

NinePatch数据结构

https://zhuanlan.zhihu.com/p/595445856

到此,我们就可以实现解析点九图PNG的编码;

//
//  PNGNinePatch.h
//  podDemo
//
//

#ifndef PNGNinePatch_h
#define PNGNinePatch_h

NS_ASSUME_NONNULL_BEGIN

@interface PNGNinePatch : NSObject

@property (nonatomic, assign) int32_t width;
@property (nonatomic, assign) int32_t height;

@property (nonatomic, assign) int8_t numXDivs;
@property (nonatomic, assign) int8_t numYDivs;
@property (nonatomic, assign) int8_t numColors;

@property (nonatomic, assign) int32_t paddingLeft;
@property (nonatomic, assign) int32_t paddingRight;
@property (nonatomic, assign) int32_t paddingTop;
@property (nonatomic, assign) int32_t paddingBottom;

@property (nonatomic, strong) NSArray *xDivsArray;
@property (nonatomic, strong) NSArray *yDivsArray;

+ (nullable instancetype)ninePatchWithPNGFileData:(NSData *)data;

/// 获取点九图bitmap中的可拉伸区域,如果返回UIEdgeInsetsZero,则表示没有可以拉伸的区域
/// 点九图可能包含多个不连续的可拉伸区域,本函数只取第一个
- (UIEdgeInsets)resizableCapInsets;

@end

NS_ASSUME_NONNULL_END

#endif /* PNGNinePatch_h */
//  PNGNinePatch.m
//  podDemo_Example
//
//  Created by asterpang on 2023/7/20.
//  Copyright   2023 asterpang. All rights reserved.
//

#import 
#import "PNGNinePatch.h"

static char bytes[8] = {0};

@implementation PNGNinePatch
// https://dev.exiv2.org/projects/exiv2/wiki/The_Metadata_in_PNG_files
// https://android.googlesource.com/platform/frameworks/base/+/56a2301c7a1169a0692cadaeb48b9a6385d700f5/include/androidfw/ResourceTypes.h

+ (instancetype)ninePatchWithPNGFileData:(NSData *)data
{
    if(data.length < 32) {
        return nil;
    }
    int index = 0;

    // 先判断是否png图片
    if ([[self class] readInt32:data fromIndex:&index] != 0x89504e47 || [[self class] readInt32:data fromIndex:&index] != 0x0D0A1A0A) {
        // 不是png图片,不再处理
        return nil;
    }

    // NinePatch的chunk type标记
    // The PNG chunk type is "npTc"
    const char npTc[4] = {'n', 'p', 'T', 'c'};

    BOOL hasNinePatchChunk = NO;

    int32_t chunk_length = 0;

    while (YES) {
        if(index >= data.length - 8) {
            break;
        }
        // 获取chunk长度
        chunk_length = [[self class] readInt32:data fromIndex:&index];

        // 获取chunk type标记
        [data getBytes:bytes range:NSMakeRange(index, 4)];
        index += 4;

        if (memcmp(bytes, npTc, 4) == 0) {
            // 表示读取到了NinePatch信息,index之后的数据是chunk data
            hasNinePatchChunk = YES;
            break;
        }

        // 跳过本chunk(数据长度 chunk_length + CRC 4bytes)
        index += chunk_length + 4;
    }

    PNGNinePatch *ninePatch = nil;

    if(hasNinePatchChunk && chunk_length > 0 && data.length > index + chunk_length) {

        ninePatch = PNGNinePatch.new;

        int8_t wasDeserialized  = [[self class] readInt8:data fromIndex:&index];
        if(wasDeserialized == 0) {
            // nothing to do
        }

        ninePatch.numXDivs = [[self class] readInt8:data fromIndex:&index];
        ninePatch.numYDivs = [[self class] readInt8:data fromIndex:&index];
        ninePatch.numColors = [[self class] readInt8:data fromIndex:&index];

        // skip xDivsOffset/yDivsOffset
        index += 4 + 4;

        ninePatch.paddingLeft = [[self class] readInt32:data fromIndex:&index];
        ninePatch.paddingRight = [[self class] readInt32:data fromIndex:&index];
        ninePatch.paddingTop = [[self class] readInt32:data fromIndex:&index];
        ninePatch.paddingBottom = [[self class] readInt32:data fromIndex:&index];

        // skip colorOffset
        index += 4;

        // now xDivs,即点九图上方黑点标记数组,横向可拉伸区域
        NSMutableArray *xDivsArray = NSMutableArray.new;
        for(int count = 0; count < ninePatch.numXDivs; count++) {
            [data getBytes:bytes range:NSMakeRange(index, 4)];
            index += 4;
            int32_t x = ntohl( *(int32_t *)bytes);
            [xDivsArray addObject:@(x)];
        }

        // now yDivs,即点九图左边黑点标记数组,纵向可拉伸区域
        NSMutableArray *yDivsArray = NSMutableArray.new;
        for(int count = 0; count < ninePatch.numYDivs; count++) {
            [data getBytes:bytes range:NSMakeRange(index, 4)];
            index += 4;
            int32_t y = ntohl(*(int32_t *)bytes);
            [yDivsArray addObject:@(y)];
        }
        ninePatch.xDivsArray = xDivsArray;
        ninePatch.yDivsArray = yDivsArray;
    }

    return ninePatch;
}

- (UIEdgeInsets)resizableCapInsetsWithImageSize:(CGSize)imageSize
{
    if(self.xDivsArray.count < 2 || self.yDivsArray.count < 2) {
        return UIEdgeInsetsZero;
    }
    // 可以是多段分割,指定拉伸/压缩,不过我们约定需求没那么复杂,只需要拉伸第一段区域
    // 如需多段处理,则更该代码
    int32_t xStart = self.xDivsArray[0].intValue;
    int32_t xEnd = self.xDivsArray[1].intValue;
    int32_t yStart = self.yDivsArray[0].intValue;
    int32_t yEnd = self.yDivsArray[1].intValue;

    if(xEnd < xStart || yEnd < yStart) {
        return UIEdgeInsetsZero;
    }

    UIEdgeInsets insets;
    insets.top = yStart;
    insets.left = xStart;
    insets.bottom = imageSize.height - yEnd;
    insets.right = imageSize.width - xEnd;

    if(insets.bottom < 0 || insets.right < 0) {
        return UIEdgeInsetsZero;
    }

    return insets;
}

+ (int8_t)readInt8:(NSData *)data fromIndex:(int *)index
{
    [data getBytes:bytes range:NSMakeRange(*index, 1)];
    *index += 1;
    return (int8_t)bytes[0];
}

+ (int32_t)readInt32:(NSData *)data fromIndex:(int *)index
{
    [data getBytes:bytes range:NSMakeRange(*index, 4)];
    *index += 4;
    return ntohl(*(int32_t *)bytes);
}

@end

使用上也比较简单,

PNGNinePatch *ninePatch = [PNGNinePatch ninePatchWithPNGFileData:imageFileData];
UIEdgeInsets insets = [ninePatch resizableCapInsets];
image = [image resizableImageWithCapInsets:insets resizingMode:UIImageResizingModeStretch];

3. 附录

PNG (Portable Network Graphics) Specification

http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html

PNG

https://zh.wikipedia.org/zh-hk/PNG

The Metadata in PNG files

https://dev.exiv2.org/projects/exiv2/wiki/The_Metadata_in_PNG_files

这才是从网络加载点9图的正确姿势!

https://zhuanlan.zhihu.com/p/595445856

NinePatchPeeker.cpp

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/libs/hwui/jni/NinePatchPeeker.cpp

NinePatchChunk

https://github.com/Anatolii/NinePatchChunk

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

https://www.jianshu.com/p/b54cbb02abad

作者:asterpang

来源:微信公众号:腾讯音乐技术团队

出处:https://mp.weixin.qq.com/s/angyJag7AZntt2FLNCOuXw

展开阅读全文

页面更新:2024-03-07

标签:黑线   数据结构   黑点   数组   纵向   标记   区域   位置   数据   图片

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top