9
votes

Background

I've created a GUI using some FireMonkey controls.

  • Some controls are animated and their appearance updates automatically.
  • Some controls only update in response to user interaction (sliders etc).

Problem

Interaction with the user controls prevents updates to the animated controls, resulting in jerky discontinuous animation.

Video of glitchy animation

The animated control in the video above is driven by a TTimer component. The problem persists when using FireMonkey's animation components.

Investigation

The slider controls call Repaint() when adjusted. Smoothly adjusting a slider will generate a dense stream of Repaint() calls which block other controls from being updated.

What To Do?

Freezing animations while one control is continuously updated is not appropriate for my application. My first thought is to swap the Repaint() calls for something similar to the VCL Invalidate() method, but FireMonkey doesn't have anything comparable AFAIK.

Is there a good workaround for this problem?

1
Have you tried to replace TTimer with some TAnimation descendants?Torbins
@Torbins: Yes, I tried with a TFloatAnimation component. It was no better. Looking at the FMX source, the TAnimation components are triggered by a global TTimer object as well.Shannon Matthews
Then maybe a separate thread? But I am not sure whether FireMonkey is threadsafe or not. Probably not.Torbins
So the only thing left: wait until they fix this issue among lots of others. Some people even call FireMonkey a "technology preview".Torbins
@Torbins The FireMonkey code is not protected by critical sections AFAIK, so I suspect it is not thread-safe. Just like the VCL. What about overriding RePaint and use your own dedicated timer-based method?Arnaud Bouchez

1 Answers

4
votes

I've created a timer based repaint method as Arnaud Bouchez suggested in the comments above. So far it seems to work.

Code

unit FmxInvalidateHack;

interface

uses
  Fmx.Types;

procedure InvalidateControl(aControl : TControl);


implementation

uses
  Contnrs;

type
  TInvalidator = class
  private
  protected
    Timer : TTimer;
    List  : TObjectList;
    procedure Step(Sender : TObject);
  public
    constructor Create;
    destructor Destroy; override;

    procedure AddToQueue(aControl : TControl);
  end;

var
  GlobalInvalidator : TInvalidator;

procedure InvalidateControl(aControl : TControl);
begin
  if not assigned(GlobalInvalidator) then
  begin
    GlobalInvalidator := TInvalidator.Create;
  end;
  GlobalInvalidator.AddToQueue(aControl);
end;


{ TInvalidator }

constructor TInvalidator.Create;
const
  FrameRate = 30;
begin
  List  := TObjectList.Create;
  List.OwnsObjects := false;

  Timer := TTimer.Create(nil);
  Timer.OnTimer  := Step;
  Timer.Interval := round(1000 / FrameRate);
  Timer.Enabled  := true;
end;

destructor TInvalidator.Destroy;
begin
  Timer.Free;
  List.Free;
  inherited;
end;

procedure TInvalidator.AddToQueue(aControl: TControl);
begin
  if List.IndexOf(aControl) = -1 then
  begin
    List.Add(aControl);
  end;
end;

procedure TInvalidator.Step(Sender: TObject);
var
  c1: Integer;
begin
  for c1 := 0 to List.Count-1 do
  begin
    (List[c1] as TControl).Repaint;
  end;
  List.Clear;
end;


initialization

finalization
  if assigned(GlobalInvalidator) then GlobalInvalidator.Free;

end.

==

Usage

A control can be repainted by calling:

InvalidateControl(MyControl);

The InvalidateControl() procedure doesn't repaint the control immediately. Instead it adds the control to a list. A global timer later checks the list, calls Repaint() and removes the control from the list. Using this method, a control can be invalidated as needed but will not block other controls from being updated, as rapid Repaint() calls do.