Search code examples
multithreadingdelphianimationdelphi-10.2-tokyo

TFloatAnimation within anonymous thread. Thread wont die and animation wont restart


I've been struggling with this for a day or two and cannot find an answer anywhere. I thought this answer might help but it didn't.

In my sample code below I have two Timage Components each containing a "Start Image". When clicking on the "Start Button" two anonymous threads are created, one animates Image1 between the Start Image and End Image, the other does the same for Image2.

My problem is that when the KillAnimation boolean is set to True both animations should stop (which they do) but only one of the threads exit, the other one stops animating but leaves the image mid-animation.

I have the same problem if I use a predefined thread as well.

The sample application has only two images, the real world app can have anywhere from 15 to 24. Anonymous threads seem to suit because I can create them and no have to worry about defining up to 24 TThreads. I hope that makes sense.

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes,
  System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics,  FMX.Objects,
  FMX.Controls.Presentation, FMX.StdCtrls, FMX.Ani, FMX.Effects,
  FMX.Filter.Effects;

type
  TForm1 = class(TForm)
    Image1: TImage;
    Image2: TImage;
    BtnStop: TButton;
    BtnStart: TButton;
    BtnReset: TButton;
    procedure BtnStopClick(Sender: TObject);
    procedure BtnStartClick(Sender: TObject);
    procedure BtnResetClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    procedure createthread(TheImage: TImage; TargetBMP: String);
  end;

var
  Form1: TForm1;
  KillAnimation: Boolean;

implementation

{$R *.fmx}

procedure TForm1.BtnStopClick(Sender: TObject);
begin
  KillAnimation := true;
end;

procedure TForm1.createthread(TheImage: TImage; TargetBMP: String);
begin
  { The thread works right up until it is stopped. Even though two
    threads are started only one finishes. In addition, the images
    do not end up as the target image, and neither image can be
    reset even if I reload them from files. }

  TThread.CreateAnonymousThread(
    procedure()
    var
      Wiggle: TWiggleTransitionEffect;
      TheFloat: TFloatAnimation;
    begin
      TThread.NameThreadForDebugging('Animate ' + TheImage.Name);
      Wiggle := TWiggleTransitionEffect.Create(Nil);
      Wiggle.RandomSeed := 0.3;
      Wiggle.Progress := 0;
      Wiggle.Parent := TheImage;

      TThread.Synchronize(TThread.CurrentThread,
        procedure()
        Begin
          Wiggle.Target.LoadFromFile(TargetBMP)
        end);

      TheFloat := TFloatAnimation.Create(Nil);
      TheFloat.Parent := Wiggle;
      TheFloat.PropertyName := 'Progress';
      TheFloat.Duration := 2;
      TheFloat.AutoReverse := true;
      TheFloat.Loop := true;
      TheFloat.StartValue := 0;
      TheFloat.StopValue := 100;
      TheFloat.StartFromCurrent := false;
      TheFloat.start;

      while not KillAnimation do
        application.handlemessage;

      TheFloat.stop;
    end).start;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin

  Image1.Bitmap.LoadFromFile('c:\sample\startimage.png');
  Image2.Bitmap.LoadFromFile('c:\sample\startimage.png');

end;

procedure TForm1.BtnStartClick(Sender: TObject);
begin
  KillAnimation := false;
  createthread(Image1, 'c:\sample\endimage.png');
  createthread(Image2, 'c:\sample\endimage.png');

end;

procedure TForm1.BtnResetClick(Sender: TObject);
begin
  KillAnimation := false;
  Image1.Bitmap.LoadFromFile('c:\sample\startimage.png');
  Image2.Bitmap.LoadFromFile('c:\sample\startimage.png');
end;

end.

I would have thought that creating the Transition and FloatAnimation within the thread meant they would be destroyed when done because FreeOnTerminate is True.

When I try to reset the Images using "image1.bitmap.loadfromfile" it doesn't change.

The images are 170 x 170 png files.

What have I done wrong here?

My goal is to pass TImage and an Image File to the thread, let it animate until told to stop. It is unlikely that I'll need all 24 images to animate, but you never know.

Apologies if I shouldn't have posted all the code from my sample. At least you can see everything going on.


Solution

  • You should NOT be using a thread for this at all. UI elements, including visual effects, should be used only in the main UI thread. And unless you are developing your app for mobile platforms, objects are not destroyed automatically when they go out of scope, you have to destroy them yourself when you are done using them, or else assign them an Owner that will destroy them for you.

    Try this instead:

    unit Unit1;
    
    interface
    
    uses
      System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
      FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Objects, FMX.Controls.Presentation,
      FMX.StdCtrls, FMX.Ani, FMX.Effects, FMX.Filter.Effects;
    
    type
      TForm1 = class(TForm)
        Image1: TImage;
        Image2: TImage;
        BtnStop: TButton;
        BtnStart: TButton;
        BtnReset: TButton;
        procedure BtnStopClick(Sender: TObject);
        procedure BtnStartClick(Sender: TObject);
        procedure BtnResetClick(Sender: TObject);
        procedure FormCreate(Sender: TObject);
      private
        { Private declarations }
        Float1: TFloatAnimation;
        Float2: TFloatAnimation;
        function PrepareEffect(TheImage: TImage; const TargetBMP: String): TFloatAnimation;
        procedure ResetImages;
      public
        { Public declarations }
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.fmx}
    
    procedure TForm1.BtnResetClick(Sender: TObject);
    begin
      ResetImages;
    end;
    
    procedure TForm1.BtnStartClick(Sender: TObject);
    begin
      Float1.start;
      Float2.start;
    end;
    
    procedure TForm1.BtnStopClick(Sender: TObject);
    begin
      Float1.stop;
      Float2.stop;
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      ResetImages;
      Float1 := PrepareEffect(Image1, 'c:\sample\endimage.png');
      Float2 := PrepareEffect(Image2, 'c:\sample\endimage.png');
    end;
    
    function TForm1.PrepareEffect(TheImage: TImage; const TargetBMP: String): TFloatAnimation;
    var
      Wiggle: TWiggleTransitionEffect;
      TheFloat: TFloatAnimation;
    begin
      Wiggle := TWiggleTransitionEffect.Create(Self);
      Wiggle.RandomSeed := 0.3;
      Wiggle.Progress := 0;
      Wiggle.Parent := TheImage;
    Wiggle.Target.LoadFromFile(TargetBMP);
    
      TheFloat := TFloatAnimation.Create(Self);
      TheFloat.Parent := Wiggle;
      TheFloat.PropertyName := 'Progress';
      TheFloat.Duration := 2;
      TheFloat.AutoReverse := true;
      TheFloat.Loop := true;
      TheFloat.StartValue := 0;
      TheFloat.StopValue := 100;
      TheFloat.StartFromCurrent := false;
    
      Result := TheFloat;
    end;
    
    procedure TForm1.ResetImages;
    begin   
      Image1.Bitmap.LoadFromFile('c:\sample\startimage.png');
      Image2.Bitmap.LoadFromFile('c:\sample\startimage.png');
    end;
    
    end.