Delphi плюс Android? Есть идея! Tap#3

Эта заметка является продолжением предыдущих. Tap#1, Tap#2.

И прежде чем приступить к анимации, стоит определиться с масштабированием персонажа. То есть необходимо настроить фрейм таким образом, чтобы, задавая его размеры “сверху”, все объекты персонажа автоматически подгоняли свой размер, и персонаж (по своим внешним размерам) оказался вписанным во фрейм.

Сделать это можно очень просто. Для самой фреймы устанавливаем свойство Align = alFit (чтобы сохранить пропорцию у соотношения сторон фреймы), а всем контролам на фрейме –  Align = alScale.  Тогда, при изменении размеров фреймы, контролы будут масштабироваться автоматически. Оговорюсь, что размеры и положения контролов в FM задаются вещественными числами, поэтому масштабирование будет плавным.

Однако есть такой нюанс: платформа FM масштабирует контролы относительно их текущих размеров. Что это значит? А это значит, что при задании маленьких размеров фреймы, наш персонаж может оказаться очень маленьким. Настолько маленьким, что при последующем увеличении фреймы персонаж перестанет масштабироваться.

Правда если рассчитывать только на мобильное устройство, в котором размер экрана фиксирован и приложение всегда развёрнуто на весь экран, то можно и не беспокоиться. Автоматическое масштабирование нам подойдёт, т.к. оно будет происходить:

  • один раз при запуске приложения;
  • при повороте экрана.

Однако я хочу, чтобы приложение можно было так же запускать и в окне в Windows. А в оконном режиме существует ненулевая вероятность того, что пользователь захочет “поиграться” с размерами окна и уменьшит его до очень маленького размера…

Поэтому масштабирование всех контролов я буду делать вручную, в обработчике OnResize. При этом, я хочу ориентироваться на исходный размер контролов, т.е. после загрузки фреймы из fmx-ресурса, необходимо сохранить позиции и размеры контролов.

Для этого создаём простую запись:

type
  TDesignPosition = record
    Left, Top, Width, Height: Single;
    RotationAngle: Single;
    FontSize: Single;
  end;

И класс-наследник от TList<T>:

type
  TDesignPositions = class(TList<TDesignPosition>)
  private
    function IndexByControl(AControl: TControl): Integer;
    function GetItemByControl(AControl: TControl): TDesignPosition;
  public
    procedure AddByControl(AControl: TControl);
    property ItemByControl[AControl: TControl]: TDesignPosition read GetItemByControl; default;
  end;

{ TDesignPositionsList }

function TDesignPositions.IndexByControl(AControl: TControl): Integer;
begin
  Result := AControl.Tag;
end;

function TDesignPositions.GetItemByControl(AControl: TControl): TDesignPosition;
begin
  Result := Items[IndexByCOntrol(AControl)];
end;

procedure TDesignPositions.AddByControl(AControl: TControl);
var
  DP: TDesignPosition;
begin
  DP.Left := AControl.Position.X;
  DP.Top := AControl.Position.Y;
  DP.Width := AControl.Width;
  DP.Height := AControl.Height;
  DP.RotationAngle := AControl.RotationAngle;
  if AControl is TTextControl then
    DP.FontSize := TTextControl(AControl).Font.Size;
  Add(DP);
end;

Тут TDesignPositions – это простой список записей типа TDesignPosition. Так как я не планирую в Run-Time добавлять или удалять контролы, то (для простоты) соответствие между элементом в списке и контролом задаётся свойством TControl.Tag.

Заполняется список так:

procedure TNPC.SavePositionsTo(AList: TDesignPositions);
var
  i: Integer;
begin
  AList.Clear;
  i := 0;
  ForEach(
    procedure (AControl: TControl)
    begin
      AControl.Tag := i;
      Inc(i);
      AList.AddByControl(AControl);
    end
  );
end;

Здесь TNPC – это базовый фрейм для персонажа, от него будут наследоваться все персонажи в будущем. ForEach – это специальный метод, который вызывает переданную анонимную функцию и перебирает все контролы фреймы. Метод я объявил в специальном хелпере, т.к. планирую его использовать ещё не один раз. (Идею с хелпером я подсмотрел у Bioan Mitov.)

type
  TControlHelper = class helper for TControl
  public
    procedure ForEach(AProc: TProc<TControl>);
  end;

{ TControlHelper }

procedure TControlHelper.ForEach(AProc: TProc<TControl>);
var
  i: Integer;
begin
  AProc(Self);
  for i := 0 to ControlsCount - 1 do
    Controls[i].ForEach(AProc);
end;

Т.к. мы контролы вкладывали друг в друга, то здесь используется рекурсия. Метод ForEach сначала вызывает указанную процедуру, а потом перебирает дочерние контролы. Таким образом, в список TDesignPositions сначала добавляются размеры самой фреймы, а потом уже размеры её контролов. Я этим воспользуюсь, чтобы определить коэффициент масштабирования:

procedure TNPC.RecalcScaleFactor;
var
  dw, dh: Single;
begin
  dw := Width / FDesignPositions.Items[0].Width;
  dh := Height / FDesignPositions.Items[0].Height;
  if dw < dh then
    FScaleFactor := dw
  else
    FScaleFactor := dh;
end;

На текущий момент базовый фрейм для персонажа у меня выглядит примерно так:

  TNPC = class(TFrame)
  private
    FControls: TControls;
    FDesignPositions: TDesignPositions;
    FScaleFactor: Single;
    FIsLoaded: Boolean;
  protected
    procedure Loaded; override;
    procedure Resize; override;
    procedure SaveDesignPositions; virtual;
    procedure SaveControls;
    procedure ScaleControls; virtual;
  public
    procedure SavePositionsTo(AList: TDesignPositions);
    property IsLoaded: Boolean read FIsLoaded;
    property ScaleFactor: Single read FScaleFactor;
    // Controls - простой массив для быстрого доступа ко всем контролам на фрейме
    //  нулейвой элемент соответствует самой фрейме
    //  у каждого контрола свойство Tag соответствует индексу в этом массиве
    property Controls: TControls read FControls;
    // DesignPositions - координаты и размеры всех контролов, заданных в дизайнере
    //  нулейвой элемент соответствует самой фрейме
    property DesignPositions: TDesignPositions read FDesignPositions;
  end;
..
procedure TNPC.Loaded;
begin
  inherited;
  SaveDesignPositions;
  SaveControls;
  FIsLoaded := True;
end;

procedure TNPC.SaveDesignPositions;
begin
  FDesignPositions := TDesignPositions.Create;
  SavePositionsTo(FDesignPositions);
end;

procedure TNPC.SaveControls;
var
  i: Integer;
begin
  SetLength(FControls, DesignPositions.Count);
  i := 0;
  ForEach(
    procedure (AControl: TControl)
    begin
      FControls[i] := AControl;
    end
  );
end;

procedure TNPC.Resize;
begin
  inherited;

  if IsLoaded then
  begin
    BeginUpdate;
    try
      RecalcScaleFactor;
      ScaleControls;
    finally
      EndUpdate;
    end;
  end;
end;

procedure TNPC.ScaleControls;
begin
  // do nothing
end;

Самый интересный метод – ScaleControls, он отвечает за масштабирование. Как раз здесь можно использовать DesignPositions и ScaleFactor. Но на уровне базовой фреймы я решил, что он будет пустым, т.к. у персонажа будет ещё анимация и будут внутренние состояния. И именно от текущего состояния будут рассчитываться положения объектов. Другими словами, свойствами DesignPositions и ScaleFactor мы будем активно пользоваться в наследниках.

А в следующий раз уже поговорим об анимации.

UPD. Для удобства я сделал ещё такую вещь: все контролы с фреймы сохранил в линейный массив Controls.

Читать на сайте автора.