Nine patch appearances

I'm an Android developer (not really) and I want to use my knowledge of nine-patch images to design interfaces in Mathematica (really)

How can I do this?

We'll start by reviewing our nine-patch images from here.

Basically there are four things we need:

1. A vertical stretch zone
2. A horizontal stretch zone
3. A vertical content zone
4. A horizontal content zone

We mark these on the image by a one-pixel black border with the vertical stretch on the left, the horizontal stretch on the top, the vertical content on the right, and the horizontal content on the left.

In fact, we can have multiple markers for each zone, so we'll need to implement this to take lists of specs.

By default, we will specs to be centered, unless passed in an Offset

Finally, our implementation will take an image, a list of stretch specs (width and height), and a list of content-zone specs (list and height). If only one is passed, the two will mirror each other.

With that pre-amble out of the way, here's the code:

Clear[ninePatchParamClean, ninePatchStretchZones, ninePatchImagePad,
ninePatchCreate];
ninePatchMarkerPatternSingle =
_Integer | Scaled[i_?NumericQ] |
Offset[
Scaled[i_?NumericQ] | _Integer,
Scaled[i_?NumericQ] | _Integer
];
ninePatchMarkerPattern =
ninePatchMarkerPatternSingle | {ninePatchMarkerPatternSingle ..};
ninePatchParamClean[p_, ind_, dim_, doNeg_: True] :=

ReplaceRepeated[p,
{
Scaled[i_] :>
i*dim[[Mod[ind, 2, 1]]],
i_Integer?(Negative[#] && doNeg &) :>
i + dim[[Mod[ind, 2, 1]]],
i : Except[_Integer, _?NumericQ] :>
Floor[i]
}
];
ninePatchStretchZones[stretch_, contents_, dim_] :=
Module[
{
stretchesX, stretchesY,
contentsX, contentsY,
stretchOffsetsX, stretchOffsetsY,
contentOffsetsX, contentOffsetsY
},
{stretchesX, stretchesY} =
Flatten@*List /@

Replace[stretch, Automatic -> {Scaled[.5], Scaled[.25]}];
{contentsX, contentsY} =
Flatten@*List /@
Replace[contents,
Automatic -> {stretchesX, stretchesY}
];
stretchOffsetsX =
ConstantArray[0, Length@stretchesX];
stretchOffsetsY =
ConstantArray[0, Length@stretchesY];
contentOffsetsX =
ConstantArray[0, Length@contentsX];
contentOffsetsY =
ConstantArray[0, Length@contentsY];
{stretchesX, stretchesY, contentsX, contentsY} =
MapIndexed[
With[{ind = #2[[1]]},
MapIndexed[
Replace[
#,
{
Offset[w_, s_] :>
With[{
v =
ninePatchParamClean[s, ind, dim, False]
},
Switch[ind,
1, stretchOffsetsX[[#2[[1]]]] = v,
2, stretchOffsetsY[[#2[[1]]]] = v,
3, contentOffsetsX[[#2[[1]]]] = v,
4, contentOffsetsY[[#2[[1]]]] = v
];
ninePatchParamClean[w, ind, dim]
],
e_ :>
ninePatchParamClean[e, ind, dim]
}
] &,
#
]
] &,
{stretchesX, stretchesY, contentsX, contentsY}
];
If[AnyTrue[Flatten@{
stretchesX, stretchesY,
contentsX, contentsY,
stretchOffsetsX, stretchOffsetsY,
contentOffsetsX, contentOffsetsY
}, Not@*IntegerQ],
Throw[$Failed], { stretchesX, stretchesY, contentsX, contentsY, stretchOffsetsX, stretchOffsetsY, contentOffsetsX, contentOffsetsY } ] ]; ninePatchImagePad[img_, { stretchesX_, stretchesY_, contentsX_, contentsY_, stretchOffsetsX_, stretchOffsetsY_, contentOffsetsX_, contentOffsetsY_ }] := With[{dim = ImageDimensions[img] + 2}, ReplacePixelValue[ ImagePad[img, 1, White], Flatten[ Map[ With[{vals = #[[1]], offsets = #[[2]], f = #[[3]]}, MapThread[ With[{v = #, o = #2}, Array[f[#, v, o] &, v] ] &, {vals, offsets} ] ] &, { { stretchesY, stretchOffsetsY, {1, Floor[(dim[[2]] - #2)/2] + # + #3} & }, { stretchesX, stretchOffsetsX, {Floor[(dim[[1]] - #2)/2] + # + #3, dim[[2]]} & }, { contentsY, contentOffsetsY, {dim[[1]], Floor[(dim[[2]] - #2)/2] + # + #3} & }, { contentsX, contentOffsetsX, {Floor[(dim[[1]] - #2)/2] + # + #3, 1} & } } ], 2 ] -> Black ] ]; ninePatchCreate[ img_?ImageQ, stretch : {ninePatchMarkerPattern, ninePatchMarkerPattern} | Automatic : Automatic, content : {ninePatchMarkerPattern, ninePatchMarkerPattern} | Automatic : Automatic ] := Catch@ Image[ ColorConvert[ ninePatchImagePad[img, ninePatchStretchZones[stretch, content, ImageDimensions[img]] ], RGBColor ], "Byte", Interleaving -> True ]; ninePatchCreate[e_, stretch : {ninePatchMarkerPattern, ninePatchMarkerPattern} | Automatic : Automatic, content : {ninePatchMarkerPattern, ninePatchMarkerPattern} | Automatic : Automatic ] := ninePatchCreate[Rasterize[e], stretch, content]  Then we can use this with the fact that some things can take a nine-patch list for Apperance to make fun Panel and Button displays. Examples: Pill-Buttons: We can use these to add attractive gradient pill-buttons like one sees in many interfaces. My approach here was to define a PillImage function that wraps a construct in a Framed with RoundingRadius set, rasterize that, and pick off the border. Then applying that to a LinearGradientImage we get the fun nine-patch appearance we'd like. Button[ Style[ "asdasdas dasdasdasd dasdasdasdas asdasdasdas", White], Appearance -> { "Default" -> NinePatchCreate[PillGradientImage[]], "Hover" -> NinePatchCreate[ PillGradientImage[{Bottom, Top} -> {GrayLevel[.6], GrayLevel[1]}]], "Pressed" -> NinePatchCreate[ PillGradientImage[{Bottom, Top} -> {GrayLevel[.8], GrayLevel[.7]}]] } ]  Note that I'm using package-level versions of these functions which can be pulled from here Also note that we could do something like: Button[ Style[ "asdasdas dasdasdasd dasdasdasdas asdasdasdas", White], Appearance -> { "Default" -> NinePatchCreate[ PillGradientImage[{Bottom, Top} -> Map[ColorData["AlpineColors"], {0, 1}]]] } ]  To get fun color palettes Arrow Button: Here's a somewhat ugly example for an Arrow button: arrow = Graphics[ { edgeColor, Thickness[.24], Arrowheads[{{.6, 1}}], Arrow[{{-2.07, 0}, {2.25, 0}}], arrowColor, Thickness[.2], Arrowheads[{{.5, 1}}], Arrow[{{-2, 0}, {2, 0}}], Text[Style["Next", textColor, 8], {-1, 0} ] }, Background -> bg, ImageSize -> {50, 25}, PlotRange -> {{-2.1, 2.1}, {-1, 1}} ]; apps = Map[ #[[1]] -> ninePatchCreate[ Rasterize[arrow /. #[[2]], RasterSize -> {100, 50}], {Offset[30, -25], Offset[-30, -1]} ] &, { "Default" -> { textColor -> GrayLevel[.95], edgeColor -> GrayLevel[.6], arrowColor -> GrayLevel[.95], bg -> None }, "Hover" -> { textColor -> GrayLevel[.6], edgeColor -> GrayLevel[.8], arrowColor -> GrayLevel[.95], bg -> None }, "Pressed" -> { textColor -> GrayLevel[.6], edgeColor -> GrayLevel[.6], arrowColor -> GrayLevel[.8], bg -> None } } ];  Here's what the button will look like at each stage in the default-hover-press process: TypeSystemPackageScope$ElisionEnabled = False;
Map[
Button[
"", Appearance -> #] &,
Association@apps
] // Dataset


Where this is better could be better than a plain Graphics is in the resizing:

We were able to specify that only the tail should grow in size.

Hover-active Panel:

Another place where this is fun is in Panel. We'll make a panel that responds to hovering:

panelAppearances =
Map[
#[[1]] ->
ninePatchCreate[
(Framed[
"",
ImageSize -> {1, 15},
Background -> bg,
FrameStyle -> fs
] /. #[[2]]),
{1, 2},
{2, 5}
] &,
{
"Default" ->
{
fs -> Black,
bg -> GrayLevel[.95]
},
"Hover" ->
{
bg -> White,
fs -> Gray
}
}
];


We can then pass this appearance to a Panel to make an input field that responds to hovering:

Panel[
InputField["", String, Appearance -> "Frameless"],
Appearance -> panelAppearances
]


Name-Tag Panel

One final place we can make use of this is in a name-tag-like Panel:

nameTag =
ninePatchCreate[
Column[
{
Framed["", RoundingRadius -> 10, FrameStyle -> Gray,
Background -> GrayLevel[.9],
ImageSize -> {2, 35}
],
Framed["", RoundingRadius -> 2, FrameStyle -> Gray,
ImageSize -> {2, 26}
],
Framed["", RoundingRadius -> 2, FrameStyle -> Gray,
Background -> GrayLevel[.9],
ImageSize -> {2, 20}]
},
Spacings -> -1.5
],
{1, Offset[1, -7]}
];

Panel["Hi!", Appearance -> nameTag, ImageSize -> {125, 75},
Alignment -> Center]


This extends interface options by a good margin.

• Your examples use different capitalization convention than the source. (not everywhere though)
– Kuba
Dec 10 '17 at 13:29
• This is what I see for the last one: i.stack.imgur.com/zjU20.png
– Kuba
Dec 10 '17 at 13:31
• @Kuba the caps one is the one I've stuck into a package. I'll make that more clear in the note below it. For some reason your Rasterize must be adding an extra pixel or two so the nine-patch gets interpreted wrong. I've changed the Offset[..., -5] to Offset[..., -7] and that should fix it. Dec 10 '17 at 20:49