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

Эта заметка является продолжением предыдущих: Tap#1, Tap#2, Tap#3. И в ней я поговорю об анимации.
В моей будущей игре у персонажа будет два основных состояния. Назовём их “Открытый” и “Закрытый”. В первом состоянии внешний вид персонажа соответствует тому, который разработан в дизайнере и сохранён в переменной DesignPositions. Во втором, вид персонажа будет другой. Для робота я решил так: меняем цвет на серый, чуть-чуть уменьшаем общий размер, втягиваем ноги, руки и антенны, закрываем глаза.
Соответственно переход из состояния в состояние будет анимированным.
Делаю я следующим образом: сразу после создания фреймы, в методе SaveDesignPositions, произвожу необходимые трансформации и сохраняю положения и размеры объектов в переменную FClosedPositions. Вот так:
type
TframeRobot = class(TNPC)
private
FClosedPositions: TDesignPositions;
public
procedure SaveDesignPositions; override;
//...
procedure TframeRobot.SaveDesignPositions;
procedure SwitchToClose;
const
kBody = 9/10;
kEye = 1/4;
kAnt = 1/4;
kHand = 5/6;
kHand2 = 1/2;
kLeg = 1/3;
begin
// HINT: мы смотрим роботу в лицо, поэтому левая рука - справа, правая - слева
// уменьшаем Body
Body.Position.X := Body.Position.X + Body.Width / 2 * (1 - kBody);
Body.Width := Body.Width * kBody;
Body.Position.Y := Body.Position.Y + Body.Height / 2 * (1 - kBody);
Body.Height := Body.Height * kBody;
// смещаем соединительные линии между телом и ногами
LegLeftCon.Position.Y := LegLeftCon.Position.Y - LegLeftCon.Height * kBody;
LegRightCon.Position.Y := LegRightCon.Position.Y - LegRightCon.Height * kBody;
// уменьшаем Head
Head.Position.X := Head.Position.X + Head.Width / 2 * (1 - kBody);
Head.Width := Head.Width * kBody;
Head.Position.Y := Head.Position.Y + Head.Height / 2 * (1 - kBody);
Head.Height := Head.Height * kBody;
// приближаем Head к Body
HeadCenter.Position.Y := HeadCenter.Position.Y + Head.Height * (1 - kBody);
// закрываем глазки
EyeLeft.Position.Y := EyeLeft.Position.Y * kBody + EyeLeft.Height * kEye;
EyeLeft.Height := EyeLeft.Height * kEye;
EyeRight.Position.Y := EyeRight.Position.Y * kBody + EyeRight.Height * kEye;
EyeRight.Height := EyeRight.Height * kEye;
// уменьшаем и прячем антенны
AntenaRight.Position.Y := AntenaRight.Position.Y * kBody + AntenaRight.Height * kBody;
AntenaRight.Height := AntenaRight.Height * kBody * kAnt;
AntenaRight.Position.Y := AntenaRight.Position.Y - AntenaRight.Height;
AntenaLeft.Position.Y := AntenaLeft.Position.Y * kBody + AntenaLeft.Height * kBody;
AntenaLeft.Height := AntenaLeft.Height * kBody * kAnt;
AntenaLeft.Position.Y := AntenaLeft.Position.Y - AntenaLeft.Height;
// прячем руки
HandRightCenter.RotationAngle := 90;
HandRightCenter.Position.X := HandRightCenter.Position.X + HandRight.Height * kHand;
HandRightCenter.Position.Y := HandRightCenter.Position.Y + HandRight.Width * kHand2;
HandLeftCenter.RotationAngle := -90;
HandLeftCenter.Position.X := HandLeftCenter.Position.X - HandLeft.Height * kHand;
HandLeftCenter.Position.Y := HandLeftCenter.Position.Y + HandLeft.Width * kHand2;
// прячем ноги
LegRightCenter.Position.Y := LegRightCenter.Position.Y - LegRight.Height * kLeg;
LegLeftCenter.Position.Y := LegLeftCenter.Position.Y - LegLeft.Height * kLeg;
end;
begin
inherited;
SwitchToClose;
FClosedPositions := TDesignPositions.Create;
SavePositionsTo(FClosedPositions);
end;
Здесь TframeRobot – это наш персонаж, про TNPC и SaveDesignPositions я писал в предыдущей заметке. Вот так выглядит наш робот до и после трансформации:
DesignPositions соответствуют картинке слева, ClosedPositions – картинке справа. Это начальное и конечное состояние в анимации. Сама анимация протекает во времени. Если взять начало времени за ноль, а конец – за единицу, то положение объекта легко рассчитать по формуле: f(t) = a + (b – a) * t, где t – время от 0 до 1, а – начальная координата, b – конечная координата, f – искомая координата. В библиотеке FMX такая функция уже реализована и называется InterpolateSingle.
Вот так у меня выглядит процедура для изменения размеров и положений объектов робота:
procedure TframeRobot.ProcessOpenCloseAnimation(const AProgress: Single);
var
BodyColor: TAlphaColor;
i: Integer;
CP, DP: TDesignPosition;
AControl: TControl;
begin
BodyColor := InterpolateColor($FF99CC00, TAlphaColors.Gray, AProgress);
// цикл от 1, т.к. саму фрейму масштабировать не надо
for i := 1 to DesignPositions.Count - 1 do
begin
AControl := Controls[i];
CP := ClosedPositions.Items[i];
DP := DesignPositions.Items[i];
// цвет
if (AControl is TShape) and (AControl <> EyeLeft) and (AControl <> EyeRight) then
if TShape(AControl).Fill.Color <> TAlphaColorRec.Null then
TShape(AControl).Fill.Color := BodyColor;
// положение
AControl.Position.X := InterpolateSingle(DP.Left, CP.Left, AProgress) * ScaleFactor;
AControl.Position.Y := InterpolateSingle(DP.Top, CP.Top, AProgress) * ScaleFactor;
// размер применяем ко всем, кроме "опорных" точек
if Pos('Center', AControl.Name) = 0 then
begin
AControl.Width := InterpolateSingle(DP.Width, CP.Width, AProgress) * ScaleFactor;
AControl.Height := InterpolateSingle(DP.Height, CP.Height, AProgress) * ScaleFactor;
end;
// поворот
AControl.RotationAngle := InterpolateSingle(DP.RotationAngle, CP.RotationAngle, AProgress);
end;
end;
(Про ScaleFactor я писал в предыдущей заметке.)
Как видите, ничего тут сложного нет.
Теперь создаём TFloatAnimation-объект, назовём его faOpenClose:
object faOpenClose: TFloatAnimation
Duration = 0.300000000000000000
OnProcess = faOpenCloseProcess
PropertyName = 'OpenCloseValue'
end
Далее у фреймы:
type
TframeRobot = class(TNPC)
..
procedure faOpenCloseProcess(Sender: TObject);
private
FOpenCloseValue: Single;
FClosed: Boolean;
..
public
property OpenCloseValue: Single read FOpenCloseValue write FOpenCloseValue;
..
procedure TframeRobot.faOpenCloseProcess(Sender: TObject);
begin
ProcessOpenCloseAnimation(FOpenCloseValue);
end;
И осталось запустить анимацию в прямом:
procedure TframeRobot.CloseWithAnimation; begin FClosed := True; faOpenClose.Stop; faOpenClose.StartValue := 0; faOpenClose.StopValue := 1; faOpenClose.Start; end;
, либо обратном направлении:
procedure TframeRobot.OpenWithAnimation; begin FClosed := False; faOpenClose.Stop; faOpenClose.StartValue := 1; faOpenClose.StopValue := 0; faOpenClose.Start; end;
Ну и теперь, мы можем реализовать метод ScaleControls, который в прошлой заметке остался не реализованным. Выглядит он у меня так:
procedure TframeRobot.ScaleControls;
procedure ScalePositions;
begin
// устанавливаем расположение контролов в зависимости от текущего состояния
// (процедура анимации учтёт ScaleFactor)
if FClosed then
ProcessOpenCloseAnimation(1)
else
ProcessOpenCloseAnimation(0)
end;
procedure ScaleStrokeThickness;
var
i: Integer;
begin
for i := 1 to Length(Controls) - 1 do
if Controls[i] is TShape then
if TShape(Controls[i]).Stroke.Kind = TBrushKind.bkSolid then
TShape(Controls[i]).Stroke.Thickness := 12 * ScaleFactor;
end;
procedure RecalcBodyRadius;
begin
Body.XRadius := (LegLeft.LocalRect.Width - LegLeft.Stroke.Thickness) / 2;
Body.YRadius := Body.XRadius;
end;
procedure RecalcAntenaRadius;
begin
AntenaLeft.XRadius := (AntenaLeft.Width - AntenaLeft.Stroke.Thickness) / 2;
AntenaLeft.YRadius := AntenaLeft.XRadius;
AntenaRight.XRadius := AntenaLeft.XRadius;
AntenaRight.YRadius := AntenaLeft.XRadius;
end;
begin
ScalePositions;
ScaleStrokeThickness;
RecalcBodyRadius;
RecalcAntenaRadius;
end;
Заметка получилась больше, чем я предполагал, поэтому закругляюсь. Напоследок скажу, что кроме анимации по смене состояния, у меня будет ещё как минимум одна анимация. Анимация-приветствие. Всё вместе выглядит так:
