Search code examples
delphifiremonkey

Delphi FMX TAnimator. How to animate a TPointF


I have a TScrollbox on my form. Placed onto the Scrollbox is a TLayout that is wider and taller than the device viewport so the horiz and vert scrollbars show and the user can manually move the layout.

I also have a gesture setup so that the user can longpress the Scrollbox and get the layout back to origin (0,0).

procedure TfrmMain.ScrollBox1Gesture(Sender: TObject;
  const EventInfo: TGestureEventInfo; var Handled: Boolean);
begin
  if EventInfo.GestureID = System.UITypes.igiLongTap then
  begin
     ScrollBox1.ViewportPosition := PointF(0, 0);
     ScrollBox1.RealignContent;
  end;

Now this works great, but as it happens rather quickly. I thought perhaps I could use:

TAnimator.AnimateFloat(Scrollbox1, 'ScrollBox1.ViewportPosition.X', 0, 0.3);

to make the movement from current position back to 0 a bit more gentle, but of course, that's never going to work because you can't assign a value to a PointF.X or PointF.Y directly (and therefore neither can the animator).

So how can it be done? Thanks


Solution

  • As a built-in PointAnimation is missing, here is a self written one.

    type
      TPointAnimation = class(TCustomPropertyAnimation)
      private
        FStartFloat: TPointF;
        FStartFromCurrent: Boolean;
        FStopFloat: TPointF;
      protected
        procedure FirstFrame; override;
        procedure ProcessAnimation; override;
      public
        constructor Create(AOwner: TComponent); override;
      published
        property AnimationType default TAnimationType.In;
        property AutoReverse default False;
        property Delay;
        property Duration nodefault;
        property Enabled default False;
        property Interpolation default TInterpolationType.Linear;
        property Inverse default False;
        property Loop default False;
        property OnFinish;
        property OnProcess;
        property PropertyName;
        property StartFromCurrent: Boolean read FStartFromCurrent write
            FStartFromCurrent default False;
        property StartValue: TPointF read FStartFloat write FStartFloat stored True;
        property StopValue: TPointF read FStopFloat write FStopFloat stored True;
        property Trigger;
        property TriggerInverse;
      end;
    
    constructor TPointAnimation.Create(AOwner: TComponent);
    begin
      inherited;
      Duration := 0.2;
      FStartFloat := PointF(0, 0);
      FStopFloat := PointF(0, 0);
    end;
    
    procedure TPointAnimation.FirstFrame;
    var
      T: TRttiType;
      P: TRttiProperty;
    begin
      if StartFromCurrent then
      begin
        T := SharedContext.GetType(FInstance.ClassInfo);
        if T <> nil then
        begin
          P := T.GetProperty(FPath);
          if (P <> nil) and (P.PropertyType.TypeKind = tkRecord) then
            StartValue := P.GetValue(FInstance).AsType<TPointF>;
        end;
      end;
    end;
    
    procedure TPointAnimation.ProcessAnimation;
    var
      newPoint: TPointF;
      T: TRttiType;
      P: TRttiProperty;
    begin
      if FInstance <> nil then
      begin
        T := SharedContext.GetType(FInstance.ClassInfo);
        if T <> nil then
        begin
          P := T.GetProperty(FPath);
          if (P <> nil) and (P.PropertyType.TypeKind = tkRecord) then begin
            newPoint := PointF(InterpolateSingle(FStartFloat.X, FStopFloat.X, NormalizedTime),
                               InterpolateSingle(FStartFloat.Y, FStopFloat.Y, NormalizedTime));
            P.SetValue(FInstance, TValue.From<TPointF>(newPoint));
          end;
        end;
      end;
    end;
    

    Declare a field inside the forms private section

    ani: TPointAnimation;
    

    Create an instance during FormCreate

    ani := TPointAnimation.Create(Self);
    ani.PropertyName := 'ViewportPosition';
    ani.StartFromCurrent := true;
    ani.Duration := 0.3;
    ScrollBox1.AddObject(ani);
    

    and start it when needed

    ani.Start;