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;
Заметка получилась больше, чем я предполагал, поэтому закругляюсь. Напоследок скажу, что кроме анимации по смене состояния, у меня будет ещё как минимум одна анимация. Анимация-приветствие. Всё вместе выглядит так: