Nine-Patch

Nine Patch is an image format that adds extra information into a normal image file to define which parts of the image should be stretched when the image is scaled. The technique used by this format is also implemented in the core of the Android OS.

More information about this format can be found at: http://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch

It is very simple and flexible to make a stencil based on an existing bitmap image by defining the areas that should stretch using the Nine-patch format. Starting from version 2.0, Pencil also provides built-in behaviors and tools to support this technique for developers to use when creating their collections. There are already many collections using this technique in the Pencil repository such as the iOS UI Stencils.

Creating a simple stencil using Nine-patch

Suppose we have the rounded rectangle image shown below and we want to create a shape based on this image that can be scaled to any size while maintaining the corner radius.

../../_images/tutorial_rounded_rectangle.png

1. Defining Nine-patch

The first step is to create a Nine-patch from this image by adding 4 black lines to its border to define the scaling and padding areas:

../../_images/tutorial_rectangle_with_lines.png

In this nine-patch, the top and left lines are used to divide the rectangle into nine pieces while the bottom and right lines are used to define the bounds.

Note

The thickness of these lines must be 1 pixel. For the purpose of illustration, lines are enlarged in this tutorial.

2. Generating JS code

The next step is to use Pencil-provided tools to load this nine-patch image and generate JavaScript code containing the nine-patch data in a structure that is compatible with Pencil’s built-in implementation of nine-patch images.

Go to Tools » Developer Tools » N-Path Script Generator... to launch the tool and load the image created above. Code generated by this tool should be copied and used in the stencil. After parsing you can see that the images are sliced into 9 pieces:

../../_images/tutorial_rectangle_nine_cut.png

These pieces are sliced based on the lines on the top and left side of the images:

  • The 1, 3, 7, 9 piece will be the same when scaling box.
  • The width of 2, 5, 8 piece will be scale based on the ratio between old width and new width.
  • The height of 4, 5, 6 piece will be scale based on the ratio between old height and new height.

3. Use the generated code in your stencil

The easiest way to use the generated code is to define a set of nine-patches at the collection level by using the <Script> tag. The generated code in the above step is used as the value of the “sample” property in the following example.

<Script comments="N Patches">
    collection.nPatches = {
        sample: {
            "w": 35,
            "h": 35,
            "p1": {
                "x": 15,
                "y": 16
            },
            "p2": {
                "x": 20,
                "y": 22
            },
            "patches": [
                [
                    {
                        "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAAr0lEQVQ4jaXTMQ6DMAwF0L+ycQPOwMbJOEA5CHuWKEYZEQsqdpE69ATdUJeuHemAxAIkgVjK+J8sOwbOlhlzNPcbiC2MPEEyhQXrLoUeKhC/QTJvnrOUSqClBPF3N+xFbJuhebycYSdCfQHDnyBgF6G+APEvGNggts1OdbBBlEqCZ3CIaCkvAStSd6l3jV5ED9VlYEWOfmIwYsY8CiCZsRxTLEJs45HlnGM7kSkW+QMkMjoMrMdPRgAAAABJRU5ErkJggg==",
                        "w": 17,
                        "h": 17,
                        "scaleX": false,
                        "scaleY": false
                    },
                    {
                        "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAARCAYAAAAcw8YSAAAAFUlEQVQImWNg2HjyJwPDppP/qUgAAOGdKhRyz8aoAAAAAElFTkSuQmCC",
                        "w": 1,
                        "h": 17,
                        "scaleX": true,
                        "scaleY": false
                    },
                    {
                        "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAApUlEQVQ4ja3TsQ3CQAwF0N/SsQEz0DFZBkgGoU9zOp+uRDQosUFKwQTpIpq0lKZIAKHQXHyW3D75WzZAMsBLB+KI0JTwtz2Si0SXzT1cW+F43hqQDzbCSYG63hiQucP1jnja2RAShecH6HKwIVO8538oCZknWkRLRd47+ln2GoRE4aSwI8Tj945WI6JwbWVHiPsMiOj0a1YkNKUdIY4Z4kiXYRIZXuZVOgx3G7yrAAAAAElFTkSuQmCC",
                        "w": 17,
                        "h": 17,
                        "scaleX": false,
                        "scaleY": false
                    }
                ],
                [
                    {
                        "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAABCAYAAAA4u0VhAAAAE0lEQVQImWNg2HjyJ8Omk/8pwQCRHioUjQN2IAAAAABJRU5ErkJggg==",
                        "w": 17,
                        "h": 1,
                        "scaleX": false,
                        "scaleY": true
                    },
                    {
                        "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNg2HTyPwAErAJ72rrK9QAAAABJRU5ErkJggg==",
                        "w": 1,
                        "h": 1,
                        "scaleX": true,
                        "scaleY": true
                    },
                    {
                        "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAABCAYAAAA4u0VhAAAAEklEQVQImWNg2HTyP0V448mfAJLeKhTUgefAAAAAAElFTkSuQmCC",
                        "w": 17,
                        "h": 1,
                        "scaleX": false,
                        "scaleY": true
                    }
                ],
                [
                    {
                        "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAAnUlEQVQ4ja3UMQ6CQBCF4b+l4wacgc6TeQA9CP02ZIdsSWgMzkpi4QnsiI0tpRarJQIZXzLtl92dmQWJIxJfpsLHqx0RDXak6Q9/uM5Q2hEA0bsdqc9HO1J1OaJPGwJQx70dcS6judxsCEBoC7w+bAiAnHaITjbkC2050WxCW6x+o59xLktdW2j/qlRdngZyZrI3xw9lWloNn29kfAN5zToMs/CBPQAAAABJRU5ErkJggg==",
                        "w": 17,
                        "h": 17,
                        "scaleX": false,
                        "scaleY": false
                    },
                    {
                        "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAARCAYAAAAcw8YSAAAAFElEQVQImWNg2HTyPwM1iY0nfwIA480qFPtI62wAAAAASUVORK5CYII=",
                        "w": 1,
                        "h": 17,
                        "scaleX": true,
                        "scaleY": false
                    },
                    {
                        "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAAn0lEQVQ4ja3TPQqDQBAF4Nemyw1yhnSezAPoQextlp1lS0kTdEbBwhPYSRrblEkRMIiB7Do+2PZj5w8geSnfBDVipdcjxF6PuDo7oJzuqkR4BAAdYppcifCM4nbWIUZSLNk1kXZAWZ72I5Yf8NUFq8T14Qm6J9gk5gc/gVDEtcO2hGCEZxhJ100MRniEafLvHvwLyQQrPYg9XJ19biEubzv/OgxaZ8VBAAAAAElFTkSuQmCC",
                        "w": 17,
                        "h": 17,
                        "scaleX": false,
                        "scaleY": false
                    }
                ]
            ],
            "lastScaleX": 1,
            "lastScaleY": 1
        }
    }
</Script>

Then in the code for the stencil that uses this nine-patch, you can use Pencil’s built-in functions to simplify the code.

<Shape id="sample" displayName="NPathSampe" icon="Icons/sample.png">
    <Properties>
        <PropertyGroup>
            <Property name="box" type="Dimension">320,44</Property>
        </PropertyGroup>

        <PropertyGroup name="Text">
            <Property name="text" displayName="Text" type="PlainText">Content</Property>
            <Property name="textFont" displayName="Default Font" type="Font">Helvetica|bold|normal|20px</Property>
        </PropertyGroup>
    </Properties>
    <Behaviors>
        <For ref="bg">
            <NPatchDomContent>
                <Arg>collection.nPatches.sample</Arg>
                <Arg>$box</Arg>
            </NPatchDomContent>
        </For>
        <For ref="text">
            <TextContent>$text</TextContent>
            <Font>$textFont</Font>
            <Fill>Color.fromString('#ffffffff')</Fill>
            <BoxFit>
                <Arg>getNPatchBound(collection.nPatches.sample, $box)</Arg>
                <Arg>new Alignment(1, 1)</Arg>
            </BoxFit>
        </For>
    </Behaviors>
    <p:Content xmlns="http://www.w3.org/2000/svg">
        <g id="bg"></g>
        <text id="text" />
    </p:Content>
</Shape>

The NPatchDomContent behavior uses the provided nine-patch and dimension to perform scaling calculations and fill the bg element with images generated from the nine-patch.

The getNPatchBound() utility function is used here to obtain the bounds defined by the bottom and right markers in the nine-patch to place the text in the correct position.

../../_images/tutorial_rectangle_bounding_box.png

4. More complex nine-patch

Despite the name of the technique, nine-patch images can be defined so that they are sliced into an unlimited number of pieces. Suppose that we have the following bitmap and we would like to have it scale in a way that in the vertical direction, only the blue and red parts are scaled while the cyan areas remain unscaled. In the horizontal direction the whole length of the image should be scaled.

../../_images/tutorial_complex_nine_patch.png

To do this, we can add the scaling markers to the image as shown in the following nine-patch:

../../_images/tutorial_complex_scaling_nine_patch.png

If we do not add right and bottom lines, getNPatchBound will return the bound that contains the whole image.