Drive block colors, sizes, images and typefaces from typed variables, so one change updates every bound property.
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.COLORval cornerRadius = engine.variable.getNumber(key = "cornerRadius") // 40val colorVariables = engine.variable.findAllOfType(type = VariableType.COLOR) // [accent]// findAll() lists text variables onlyval 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 shapeval 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 fillval 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 variableval 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 onceengine.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 valuesengine.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") // accentval opacityBinding = engine.block.getVariableBinding(block = card, property = "opacity") // null// The four corner radius properties of the shapeval 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 variableval 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 followingengine.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") // 24val boundCornerRadius = engine.block.getFloat(block = cardShape, property = "shape/rect/cornerRadiusTL") // 60remove 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 nameengine.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 variableval 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 valueval 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 variableval 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#
- Text Variables: fill
{{key}}tokens in text with values. - Placeholders: mark the blocks that template users can replace.
- Undo and History: work with undo steps.