6
$\begingroup$

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?

$\endgroup$
10
$\begingroup$

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]}]]
   }
 ]

dddd

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}]]]
   }
 ]

asdasd

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:

TypeSystem`PackageScope`$ElisionEnabled = False;
Map[
  Button[
    "", Appearance -> #] &,
  Association@apps
  ] // Dataset

huh

(the jagged edges are rasterization artifacts before uploading)

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

asdasd

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[
         "",
         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
 ]

asdasd

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]

asdasd

This extends interface options by a good margin.

$\endgroup$
3
  • $\begingroup$ Your examples use different capitalization convention than the source. (not everywhere though) $\endgroup$
    – Kuba
    Dec 10 '17 at 13:29
  • $\begingroup$ This is what I see for the last one: i.stack.imgur.com/zjU20.png $\endgroup$
    – Kuba
    Dec 10 '17 at 13:31
  • $\begingroup$ @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. $\endgroup$
    – b3m2a1
    Dec 10 '17 at 20:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.