Search Docs
Loading...
Skip to content

Variable Bindings

Drive block colors, sizes, images and typefaces from typed variables, so one change updates every bound property.

10 mins
estimated time
GitHub

Text variables fill {{key}} tokens in text, as the Text Variables guide shows. Color, number, image and typeface variables hold one typed value each. When you bind a block property to such a variable, the property takes the variable’s value and follows every change of it. One brand color can then drive every fill, border and text that uses it.

This guide covers the typed variables, binding and unbinding properties, the lock on bound properties, renaming variables, and the errors to expect.

Creating Typed Variables#

Each type has its own setter. setColor, setNumber, setImage and setTypeface create the variable when the key is new, and update it otherwise. set stays the setter for text variables.

// One variable per type. A setter creates the variable when the key is new.
val bundledAssetsBaseUri = "file:///android_asset/imgly-assets"
val brandTypeface = Typeface(
name = "Fira Sans",
fonts = listOf(
Font(
uri = Uri.parse("$bundledAssetsBaseUri/ly.img.typeface/fonts/FiraSans/FiraSans-Regular.ttf"),
subFamily = "Regular",
weight = FontWeight.NORMAL,
style = FontStyle.NORMAL,
),
),
)
engine.variable.setColor(key = "accent", value = Color.fromRGBA(r = 0.48F, g = 0.23F, b = 0.93F, a = 1F))
engine.variable.setNumber(key = "cornerRadius", value = 40F)
engine.variable.setImage(key = "heroImage", uri = "https://img.ly/static/ubq_samples/sample_1.jpg")
engine.variable.setTypeface(key = "headline", typeface = brandTypeface)
engine.variable.set(key = "title", value = "Summer Sale")

A number must be finite. A typeface needs a name and at least one font. One key holds one type, so setNumber(key = "accent", value = 1F) fails while accent is a color variable.

Reading Variables#

getColor, getNumber, getImage and getTypeface return the values. getType returns a VariableType: STRING, COLOR, NUMBER, IMAGE or TYPEFACE. findAllOfType lists the keys of one type.

val accentType = engine.variable.getType(key = "accent") // VariableType.COLOR
val cornerRadius = engine.variable.getNumber(key = "cornerRadius") // 40
val colorVariables = engine.variable.findAllOfType(type = VariableType.COLOR) // [accent]
// findAll() lists text variables only
val textVariables = engine.variable.findAll() // [title]
Log.i(TAG, "$accentType, $cornerRadius, $colorVariables, $textVariables")

findAll lists text variables only, as before. Use findAllOfType for the other types.

Binding Properties#

We create a card with a color fill and a rect shape, an image with an image fill, and a text that shows the title variable. A binding targets the block that owns the property: the fill owns fill/color/value, and the shape owns the corner radii.

// A card with a color fill and a rect shape
val card = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = card, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setFill(block = card, fill = engine.block.createFill(FillType.Color))
engine.block.setWidth(block = card, value = 720F)
engine.block.setHeight(block = card, value = 520F)
engine.block.setPositionX(block = card, value = 40F)
engine.block.setPositionY(block = card, value = 40F)
engine.block.appendChild(parent = page, child = card)
val cardFill = engine.block.getFill(block = card)
val cardShape = engine.block.getShape(block = card)
// An image with an image fill
val image = engine.block.create(DesignBlockType.Graphic)
engine.block.setShape(block = image, shape = engine.block.createShape(ShapeType.Rect))
val imageFill = engine.block.createFill(FillType.Image)
engine.block.setFill(block = image, fill = imageFill)
engine.block.setWidth(block = image, value = 640F)
engine.block.setHeight(block = image, value = 300F)
engine.block.setPositionX(block = image, value = 80F)
engine.block.setPositionY(block = image, value = 80F)
engine.block.appendChild(parent = page, child = image)
// A headline that shows the text variable
val text = engine.block.create(DesignBlockType.Text)
engine.block.replaceText(block = text, text = "{{title}}")
engine.block.setWidth(block = text, value = 640F)
engine.block.setHeightMode(block = text, mode = SizeMode.AUTO)
engine.block.setPositionX(block = text, value = 80F)
engine.block.setPositionY(block = text, value = 420F)
engine.block.appendChild(parent = page, child = text)

setVariableBinding binds a property to a variable. The property takes the variable’s current value at once.

// Each binding applies the current value of the variable at once
engine.block.setVariableBinding(block = cardFill, property = "fill/color/value", key = "accent")
for (corner in listOf("TL", "TR", "BL", "BR")) {
engine.block.setVariableBinding(
block = cardShape,
property = "shape/rect/cornerRadius$corner",
key = "cornerRadius",
)
}
engine.block.setVariableBinding(block = imageFill, property = "fill/image/imageFileURI", key = "heroImage")
engine.block.setVariableBinding(block = text, property = "text/typeface", key = "headline")

Each variable type binds to a fixed set of properties:

Variable type Properties it binds to
Color Color properties, for example fill/color/value and fill/solid/color
Number Float properties, for example opacity, width and shape/rect/cornerRadiusTL. Int properties, such as shape/star/points, do not bind
Image fill/image/imageFileURI, fill/image/previewFileURI and fill/video/fileURI
Typeface text/typeface and caption/typeface

Text variables and gradient color stops cannot bind. On a text block, a bound typeface, text color or font size applies to the whole text.

Changing Variables#

Set a variable again to update every property that is bound to it.

// Every bound property follows the new values
engine.variable.setColor(key = "accent", value = Color.fromRGBA(r = 0.05F, g = 0.55F, b = 0.55F, a = 1F))
engine.variable.setNumber(key = "cornerRadius", value = 24F)
engine.variable.setImage(key = "heroImage", uri = "https://img.ly/static/ubq_samples/sample_2.jpg")
val updatedCardColor = engine.block.getColor(block = cardFill, property = "fill/color/value")

An image variable also empties the source set of the fill, so the fill shows the URI of the variable. When one bound property rejects a value, for example an opacity above 1, the setter throws the first error. The variable and the other bound properties change anyway.

Reading Bindings#

getVariableBinding returns the key of the variable that a property is bound to, or null. findAllVariableBindings lists the bound properties of a block.

val cardFillBinding = engine.block.getVariableBinding(block = cardFill, property = "fill/color/value") // accent
val opacityBinding = engine.block.getVariableBinding(block = card, property = "opacity") // null
// The four corner radius properties of the shape
val shapeBindings = engine.block.findAllVariableBindings(block = cardShape)

Bound Properties Are Locked#

A bound property changes only through its variable. A direct write throws an EngineException with the code EngineErrorCode.BLOCK_PROPERTY_BOUND and leaves the value as it is.

// A bound property changes only through its variable
val boundWriteErrorCode = try {
engine.block.setColor(
block = cardFill,
property = "fill/color/value",
value = Color.fromRGBA(r = 1F, g = 0F, b = 0F, a = 1F),
)
null
} catch (error: EngineException) {
// EngineErrorCode.BLOCK_PROPERTY_BOUND
error.code
}

Moving, resizing, rotating, cropping, aligning and scaling also leave a bound property unchanged. A bound typeface also locks setTypeface and setFont. Keyframes, undo and loading a scene still change a bound property. Changing the design unit also converts a bound length, such as a width or a corner radius. A number variable whose bound properties all hold its value converts with them. Otherwise the variable keeps its value, and the next write to the variable sets the lengths in the new unit.

Removing Bindings#

removeVariableBinding removes the binding. The property keeps its current value and no longer follows the variable.

// The bottom corners keep their current radius and stop following
engine.block.removeVariableBinding(block = cardShape, property = "shape/rect/cornerRadiusBL")
engine.block.removeVariableBinding(block = cardShape, property = "shape/rect/cornerRadiusBR")
engine.variable.setNumber(key = "cornerRadius", value = 60F)
val detachedCornerRadius = engine.block.getFloat(block = cardShape, property = "shape/rect/cornerRadiusBL") // 24
val boundCornerRadius = engine.block.getFloat(block = cardShape, property = "shape/rect/cornerRadiusTL") // 60

remove removes a variable of any type. Its bound properties keep the last value and the binding. When you set a variable with the same key and type again, they follow it again.

Renaming Variables#

rename moves the value and the type of a variable to a new key. Every binding follows the new key. For a text variable, every {{key}} token in text changes to the new key.

// Bindings and {{tokens}} in text follow the new name
engine.variable.rename(from = "accent", to = "brand/accent")
engine.variable.rename(from = "title", to = "campaign/title")
val renamedBinding = engine.block.getVariableBinding(block = cardFill, property = "fill/color/value")
val renamedText = engine.block.getString(block = text, property = "text/text") // {{campaign/title}}

A new name must follow these rules:

  • It is not empty.
  • It does not start or end with whitespace. Spaces inside the name are allowed.
  • It does not contain {{ or }}, because text tokens use them.
  • No other variable uses it. A rename to the current name does nothing.
// BLOCK.VARIABLE_NAME_INVALID for the first three names,
// BLOCK.VARIABLE_NAME_TAKEN for the name of another variable
val renameErrorCodes = listOf("", " padded", "a{{b", "campaign/title").mapNotNull { name ->
try {
engine.variable.rename(from = "brand/accent", to = name)
null
} catch (error: EngineException) {
error.code
}
}

Handling Type Errors#

A getter of another type and a binding to a property of another type throw errors. The variable and the property stay unchanged.

// A color variable has no number value
val typeMismatchErrorCode = try {
engine.variable.getNumber(key = "brand/accent")
null
} catch (error: EngineException) {
// EngineErrorCode.BLOCK_VARIABLE_TYPE_MISMATCH
error.code
}
// Opacity is a number property, so it cannot bind to a color variable
val bindingTypeErrorCode = try {
engine.block.setVariableBinding(block = card, property = "opacity", key = "brand/accent")
null
} catch (error: EngineException) {
// EngineErrorCode.BLOCK_VARIABLE_BINDING_UNSUPPORTED
error.code
}

Saving and Undo#

A scene or an archive saves the typed variables and the bindings. Undo and redo restore variable values, bindings and renames. Variables belong to the engine, not to the scene, so the example removes its variables when it ends.

Exporting the Result#

We export the page as a PNG. The card shows the last values of its variables.

val previewPng = engine.block.export(block = page, mimeType = MimeType.PNG)

Troubleshooting#

  • The variable does not exist (BLOCK.VARIABLE_NOT_FOUND): set the variable before you read or bind it.
  • The key holds another type (BLOCK.VARIABLE_TYPE_MISMATCH): use the getter and setter of the stored type, or remove the variable first.
  • The typeface is not accepted (BLOCK.VARIABLE_TYPEFACE_INVALID): pass a typeface with a name and at least one font.
  • The property cannot bind to this variable (BLOCK.VARIABLE_BINDING_UNSUPPORTED): check the table in Binding Properties, and pass the block that owns the property.
  • Gradient stops do not bind (BLOCK.VARIABLE_BINDING_GRADIENT_STOPS_UNSUPPORTED): bind a color variable to a solid color fill instead.
  • A write to a property fails (BLOCK.PROPERTY_BOUND): set the variable instead, or remove the binding first.
  • A rename fails with an invalid name (BLOCK.VARIABLE_NAME_INVALID): follow the name rules in Renaming Variables.
  • A rename fails with a taken name (BLOCK.VARIABLE_NAME_TAKEN): pick a name that no variable uses.

The Error Catalog lists every error code.

API Reference#

Method Description
engine.variable.setColor(key=_, value=_) / getColor(key=_) Set or read a color variable.
engine.variable.setNumber(key=_, value=_) / getNumber(key=_) Set or read a number variable.
engine.variable.setImage(key=_, uri=_) / getImage(key=_) Set or read an image variable.
engine.variable.setTypeface(key=_, typeface=_) / getTypeface(key=_) Set or read a typeface variable.
engine.variable.set(key=_, value=_) Set a text variable.
engine.variable.getType(key=_) Get the type of a variable.
engine.variable.findAllOfType(type=_) List the keys of the variables of one type.
engine.variable.findAll() List the keys of the text variables.
engine.variable.rename(from=_, to=_) Rename a variable of any type.
engine.variable.remove(key=_) Remove a variable of any type.
engine.block.setVariableBinding(block=_, property=_, key=_) Bind a property to a variable.
engine.block.getVariableBinding(block=_, property=_) Get the key of the bound variable, or null.
engine.block.findAllVariableBindings(block=_) List the bound properties of a block.
engine.block.removeVariableBinding(block=_, property=_) Remove a binding and keep the current value.

Next Steps#