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?
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:
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_] :>
idim[[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.
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
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:
TypeSystem`PackageScope`$ElisionEnabled = False;
Map[
Button[
"", Appearance -> #] &,
Association@apps
] // Dataset
(the jagged edges are rasterization artifacts before uploading)
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.
Another place where this is fun is in Panel. We'll make a panel that responds to hovering:
panelAppearances =
Map[
#[[1]] ->
ninePatchCreate[
(Framed[
"",
RoundingRadius -> 5,
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
]
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.
Rasterizemust be adding an extra pixel or two so the nine-patch gets interpreted wrong. I've changed theOffset[..., -5]toOffset[..., -7]and that should fix it. – b3m2a1 Dec 10 '17 at 20:49