An open-source set of Delphi components for the FireMonkey framework that utilizes the Skia4Delphi library.
- Base controls
- Svg components
- Animated image components
- Text components
- FMX style objects - revamped
- Adding default style for components
ZX defines the TZxCustomControl
and TZxStyledControl
classes, which inherit from TSkCustomControl
and TSkStyledControl
, respectively. They implement only a workaround for an FMX behavior on mobile platforms: panning and releasing execute Click on the control on which you released your finger. This is especially annoying when you're scrolling through clickable components. The most simple workaround I found was the following:
procedure TZxCustomControl.Click;
begin
{$IFDEF ZX_FIXMOBILECLICK}
if FManualClick then
{$ENDIF}
inherited;
DoClick;
end;
procedure TZxCustomControl.Tap(const Point: TPointF);
begin
{$IFDEF ZX_FIXMOBILECLICK}
FManualClick := True;
Click;
FManualClick := False;
{$ENDIF}
inherited;
DoTap(Point);
end;
This way, Click only executes from the Tap method, and Tap is executed only if the internal calculation concluded it was a tap, not a pan. With this implementation, it is enough to assign only OnClick events for all platforms, instead of assigning both OnClick and OnTap events, and then wrap the OnClick implementation into a compiler directive condition.
Note: I left this feature disabled by default since I'm unsure on how it may impact someone's existing code. I've also marked both Click and Tap methods as final overrides and added new DoClick and DoTap methods, since it would be easy to break the fix by overriding those in a descendant class. To enable it, go to Project > Options > Delphi Compiler, and in Conditional defines add ZX_FIXMOBILECLICK
.
Thanks to skia4delphi, we have a proper SVG support in Delphi!
ZX provides a new glyph component that draws SVG instead of a bitmap. The class implements the IGlyph
interface and is almost the same implementation as in TGlyph
(with properties such as AutoHide) but without all the bitmap-related code.
This class inherits from TBaseImageList
and is a collection of TSkSvgBrush
items. This implementation allows you to, for example, attach the TZxSvgBrushList
component to a TActionList.ImageList
, and then use the brushes by assigning TAction.ImageIndex
. To display an item, use the above-mentioned TZxSvgGlyph
. The component also has a design-time editor.
With implementation very similar to (TZxSvgBrushList
), this component stores a list of sources that can then be assigned to TSkAnimatedImage
's Source property. The component also has a design-time editor.
Since the arrival of skia4delphi, we're able to:
- manipulate more text settings with the
TSkTextSettings
, such as MaxLines, LetterSpacing, font's Weight and Stretch, etc. - define a custom style for text settings, and
- retrieve the true text size before the draw occurs.
Reimplementation of FMX's TText
control that implements ISkTextSettings
instead of ITextSettings
. There are 2 major differences when compared to TText
:
- The parent class is
TZxStyledControl
meaning it can be styled through the StyleLookup property. The control expects the TSkStyleTextObject in its style object and applies the text settings according to its StyledSettings property. This way you can define and change your text settings in one place! - It contains a published AutoSize property, which is self-explanatory. If you wish to retrieve the text size in runtime without having the AutoSize property set to True, use the public property ParagraphBounds, which returns a TRectF value.
The FMX framework defines TTextControl
and TPresentedTextControl
classes, which serve as a base for all text controls, such as TButton
, TListBoxItem
, etc. They rely on their style objects to contain the TText
control, whose TextSettings can be modified in the Style Designer. Unfortunately, integrating those features into the mentioned base FMX classes is like getting blood from a stone, so ZX reimplemented the TTextControl
into TZxTextControl
, which utilizes ISkTextSettings
instead of ITextSettings
. The implementation is similar, except it contains the AutoSize property. If true, the control will resize to the TZxText.ParagraphBounds and any other objects (depending on their alignment and visibility) from its style resource.
This class implements the same functionality as FMX's TCustomButton
, except its property Images is a TBaseImageList
instead of TCustomImageList
, which allows you to assign both TZxSvgBrushList
(mentioned earlier) and TImageList
. If using the former, the style resource should contain the TZxSvgGlyph
(mentioned earlier) component with StyleName set to 'glyph'.
These classes are the same as their counterparts, TButton
and TSpeedButton
.
The current FMX's approach to the application style relies on TBitmapLinks
that links to a part of a large image. Custom styles can be defined per platform, which allows the developer to bring the styles closer to the platform's native look. I won't go further into the details, as the official documentation covers it well.
Looking at the currently most popular cross-platform applications, such as Discord, WhatsApp, and Slack, all share (almost) the same style across platforms. More applications tend to follow that path, and I am a fan as well: from the designer's standpoint, there is only one style that needs to be worked upon, and from the user's standpoint, I like to have the ability to switch platforms and still feel familiar with the same application. Keep in mind that I'm talking specifically about the application style and not the user interface.
The FMX framework introduces style object classes for defining the bitmap links. Their ancestor class is TCustomStyleObject
, which draws a bitmap on the canvas by retrieving the bitmap link from a virtual abstract function GetCurrentLink and getting the actual bitmap from the Source, the large image I mentioned above. Every child class defines its own set of bitmap link published properties (e.g. TActiveStyleObject
has SourceLink and ActiveLink) that they return in the GetCurrentLink function, depending on which one is active.
What I find troubling with this implementation is that the FMX has set the bitmap links and the image source as the base of their style object classes, and built upon that are the child classes functionalities. It is impossible to define any other link types (such as TAlphaColor
, or since skia4delphi, TSkSvgBrush
) and implement a custom drawing to the canvas. Because of that, ZX reimplements the style object classes so that the functionalities are defined first, and then the child classes can implement the drawing in any way they want.
An additional limitation of the bitmap links is the lack of animations. If you take a look at some the FMX's style objects implementation, you'll see that they use the TAnimation
internally; but not for doing actual animations. TAnimation
, besides its core functionality, provides two additional properties: Trigger and TriggerInverse, which are used to decide when to start the animation (learn more). Some style objects use this, while the animation is set to never start, only to notify the handler that the trigger has occurred.
Style object similar to TActiveStyleObject
; it contains a published property ActiveTrigger for defining the trigger type. Unlike its counterpart, it contains no implementation for handling the trigger (e.g. drawing a bitmap link). It also contains a public property Duration, for setting the animation duration.
A descendant of TZxCustomActiveStyleObject
that draws a colored rectangle (fill color only) depending on the trigger state. When the trigger occurs, the color change is animated through interpolation. To set the colors, use the published properties SourceColor and ActiveColor. The class also contains RadiusX and RadiusY, allowing you to draw a round rectangle.
Thanks to skia4delphi, we can display animated images through TSkAnimatedImage
. This class implements a TSkAnimatedImage
whose animation starts when the trigger is executed. There are two possible states, depending on the boolean value of the AniLoop property:
- If set to false, the animation starts on the trigger and starts inversed on the inverse trigger, but does not loop, and
- if set to true, the animation starts on the trigger in a loop and stops on the inverse trigger. There are also additional properties for the image animation: AniDelay, AniSource, and AniSpeed.
This class implements the functionality of the TButtonStyleObject
with the button trigger types (Normal, Hot, Pressed, Focused), but whose animations have a changeable duration through the published Duration property.
A descendant of TZxCustomButtonStyleObject
that draws a colored rectangle (fill color only) depending on the trigger state. When any of the triggers occur, the animation interpolates the previous trigger color and the new trigger color. To set the trigger colors, use the published properties NormalColor, HotColor, PressedColor, or FocusedColor. The class also contains RadiusX and RadiusY, allowing you to draw a round rectangle.
While writing these components, I had trouble finding how to define a default style for a component that:
- doesn't require overriding the GetStyleObject method in every class to load the style from a resource, and
- works in both runtime and design-time (without opening the unit in which the style is defined).
The current documentation doesn't satisfy the first request, and for the second one, to get the style to show up in runtime, the style resource has to be added to the application (at least that is the only solution I found). What bothered me is that the FMX controls have their default styles and do not load the same way as described in the existing documentation. In pursuit of achieving the same behavior, I realized that the styles are loaded in design-time similar to that in the runtime, except the loading of the TStyleBook
component. That gave me the idea to get the design-time's current TStyleContainer
through TStyleManager
's function ActiveStyle
, and then add the custom control's default style to its children list. To implement this, ZX provides a TZxStyleManager
class with the following class methods:
class function AddStyles(const ADataModuleClass: TDataModuleClass): IZxStylesHolder; overload;
class function AddStyles(const AStyleContainer: TStyleContainer; const AClone: Boolean): IZxStylesHolder; overload;
class procedure RemoveStyles(const AStylesHodler: IZxStylesHolder);
Note: The first AddStyles procedure with the ADataModuleClass parameter internally creates the data module instance, loops through its children, and for every TStyleBook
instance does the same process as the second AddStyles method.
The IZxStyleHolder
interface has no exposed methods or properties and is used only as a reference to the added styles which can be removed by calling the method RemoveStyles. The actual instance that implements the interface holds a list of the added styles, which the TZxStyleManager
handles and uses for removing the styles from the global style container.
The benefits of this implementation are:
- no need for data resources,
- no need to override the GetStyleObject method, and
- the styles are always visible, without needing to open the data module unit in which the style is defined.
Caveats to this implementation are yet to be found.
WARNING: This implementation is not thoroughly tested on all platforms, so there is a possibility you may run into bugs or problems when using this approach. It has been tested with only one platform/collection defined, Default, as ZX's style approach is one-for-all. Feel free to report any issues or suggest improvements!
There are 2 ways you can use the TZxStyleManager
:
- The simpler implementation includes calling TZxStyleManager.AddStyles without a
IZxStylesHolder
instance, which means theTZxStyleManager
will handle the removal of your registered styles. - Manually create a
TZxStylesHolder
instance and handle its lifetime. Call TZxStyleManager.AddStyles with that instance;TZxStyleManager
will put the registered styles inside it. To unregister the styles, call TZxStyleManager.RemoveStyles. This implementation allows you to add and remove different styles during run-time with an ease.
The simpler implementation steps:
- Create a
TDataModule
, add aTStyleBook
, and fill it with styles (you should also remove the global field that is used for registering the module to the FMX.Forms.Application instance). - Add Zx.StyleManager unit in the uses section.
- In the initialization section, call TZxStyleManager.AddStyles with your data module class as the parameter.
The manual handling implementation steps:
- Create a
TDataModule
, add aTStyleBook
, and fill it with styles (you should also remove the global field that is used for registering the module to the FMX.Forms.Application instance). - Add Zx.StyleManager unit in the uses section.
- In the implementation section, declare a variable of type
IZxStylesHolder
. - In the initialization section, create a
TZxStylesHolder
instance and assign it to the previously declared variable. - In the initialization section, call TZxStyleManager.AddStyles with your data module class as the first parameter, and your previously declared variable as the second parameter.
- In the finalization section, call TZxStyleManager.RemoveStyles with the previously declared variable as the parameter.