前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS点九图NinePatch解析

iOS点九图NinePatch解析

作者头像
QQ音乐技术团队
发布2023-09-17 08:26:20
5980
发布2023-09-17 08:26:20
举报

1. 背景

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

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

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

2. 方案

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

代码语言:javascript
复制
- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets resizingMode:(UIImageResizingMode)resizingMode;

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

代码语言:javascript
复制
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做处理之后生成新的点九图,具体的说步骤为:

设计师或者产品给出原始切图top1.png;

使用android studio制作包含4个黑边点九图top1.9.png;

使用android sdk 目录下的 aapt 工具将点九图转化为png图片 top1_out.png;

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,执行命令如下:

代码语言:javascript
复制
./aapt s -i top1.9.png -o top1_out.png

本地使用该 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

  • 文件标识(文件头) 8字节的signature标识,固定为十六进制89 50 4E 47 0D 0A 1A 0A
  • 数据块chunk 每个数据块的结构固定,4个部分组成:

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

代码语言:javascript
复制
*
 * 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的编码;

代码语言:javascript
复制
//
//  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<NSNumber *> *xDivsArray;
@property (nonatomic, strong) NSArray<NSNumber *> *yDivsArray;

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

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

@end

NS_ASSUME_NONNULL_END

#endif /* PNGNinePatch_h */
代码语言:javascript
复制
//  PNGNinePatch.m
//  podDemo_Example
//
//  Created by asterpang on 2023/7/20.
//  Copyright © 2023 asterpang. All rights reserved.
//

#import <Foundation/Foundation.h>
#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<NSNumber *> *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<NSNumber *> *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

使用上也比较简单,

代码语言:javascript
复制
PNGNinePatch *ninePatch = [PNGNinePatch ninePatchWithPNGFileData:imageFileData];
UIEdgeInsets insets = [ninePatch resizableCapInsets];
image = [image resizableImageWithCapInsets:insets resizingMode:UIImageResizingModeStretch];

3. 附录

  1. PNG (Portable Network Graphics) Specification http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html
  2. PNG https://zh.wikipedia.org/zh-hk/PNG
  3. The Metadata in PNG files https://dev.exiv2.org/projects/exiv2/wiki/The_Metadata_in_PNG_files
  4. 这才是从网络加载点9图的正确姿势! https://zhuanlan.zhihu.com/p/595445856
  5. NinePatchPeeker.cpp https://cs.android.com/android/platform/superproject/+/master:frameworks/base/libs/hwui/jni/NinePatchPeeker.cpp
  6. NinePatchChunk https://github.com/Anatolii/NinePatchChunk
  7. 在iOS中使用Android中的.9图片 https://www.jianshu.com/p/b54cbb02abad
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-09-15 12:00,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯音乐技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 背景
  • 2. 方案
    • 2.1 点九图制作生成
      • 2.2 PNG文件格式
        • 2.3 PNG点九图数据解析
        • 3. 附录
        相关产品与服务
        图数据库 KonisGraph
        图数据库 KonisGraph(TencentDB for KonisGraph)是一种云端图数据库服务,基于腾讯在海量图数据上的实践经验,提供一站式海量图数据存储、管理、实时查询、计算、可视化分析能力;KonisGraph 支持属性图模型和 TinkerPop Gremlin 查询语言,能够帮助用户快速完成对图数据的建模、查询和可视化分析。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档