[Delphi] Transition の使い方

# Transition 
なぜか、Transition について書いた記事があまりないので、Transition の紹介。
まずは、↓これを（サイズ大きめなので表示までに時間かかるかも＆実機ではもっと綺麗に表示されてます）。

<img width=360 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/ef5b2bd6-8cef-6481-3bd5-b3c9e3e17c1f.gif">

Transition を使えば、どこかで見たことのあるエフェクトも（比較的）簡単に作れちゃうもんね！！

# Transition とは
日本語では推移効果といい、一般的には２つのシーンを効果付きで切り替える機能です。
Delphi FireMonkey における Transition も同様で、２枚の画像を効果付きで切り替えます。
たとえば、[フェード](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Filter.Effects.TFadeTransitionEffect)しながら切り替えたり、[波紋様](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Filter.Effects.TRippleTransitionEffect)に変形しながら切り替えたりできます。

■Fade と Ripple Transition
<img width=240 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/121d34c7-7824-c4ad-f04b-6c5556216378.gif"> <img width=240 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/36f2e7ee-bf8d-715b-b715-ac5589cebc46.gif">

（上記の画像は Sample ディレクトリに入っている "ShaderFilters" からキャプチャしました。標準のインストール先は```C:\Users\Public\Documents\Embarcadero\Studio\22.0\Samples\Object Pascal\Multi-Device Samples\User Interface\ShaderFilters\```です）

なお冒頭の Transition は、[TBlurTransition](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Filter.Effects.TBlurTransitionEffect) を使っています。

# 実用的な Transition の使い方
２枚の画像を切り替えるだけ、という用途もあまりないと思うので、Transition を利用した画面転換方法を紹介します。

## 0. 画面＆遷移後の画面を作る

■元の画面 (以降 First と呼びます)
<img width=480 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/4756764d-ef31-59b5-3d59-5a551a5ecf4b.png">
■遷移後の画面 (以降 Second と呼びます)
<img width=480 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/d726d5f7-a9fb-44d8-d55d-d06606ca5d17.png">

取りあえず適当にコントロールを乗せてこんな感じの画面を作りました。
Second が TPanel に構築されているのは背景をつけるためで、別に TLayout でも構いません。
ここは本来必要な UI ができているハズなので順番は 0 です。

## 1. Transition の設定
First に Transition をくっつけます。
Transition なら何でもいいのですが、ここではぐにょぐにょするエフェクトをかけてくれる [TWiggleTransitionEffect](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Filter.Effects.TWiggleTransitionEffect) を付けました。

<img width=240 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/96afe96e-ba5c-86cd-58c9-ce180cdb8d9f.png">

## 2. FloatAnimation の設定
1 で付けた Transition に TFloatAnimation をくっつけます。

<img width=240 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/96b06285-1044-a210-d08a-c2b208171c21.png">

そして、プロパティを以下のように設定します。

| プロパティ   | 値                    | 意味                                       |
|--------------|-----------------------|--------------------------------------------|
| Duration     | 3                     | アニメーション時間[sec]<br>ここでは判りやすくするために 3[sec]にしましたが、本来はもっと短く 0.2～0.5 ぐらいが良いと思います|
| PropertyName | Progress              | 変更する親コントロールのプロパティの名前|
| StopValue    | 100                   | アニメーション終了時の値<br>Progress プロパティは 0～100% を指定するものなので終了値は 100 です|
| OnFinish     | FloatAnimation1Finish | アニメーションが終了すると呼ばれるイベント |

## 3. コード
### ScreenShot
まず、Second である pnlSecond の OnApplyStyleLookup で Transition にターゲット画像を設定します。
このターゲット画像を pnlSecond のスクリーンショットにすると画面が遷移したように見えます。
スクリーンショットを撮った後はとりあえず必用無いので Visible を False にして非表示にしています。

```pascal
procedure TfrmSample.pnlSecondApplyStyleLookup(Sender: TObject);
begin
  var Bmp := pnlSecond.MakeScreenshot;
  try
    WiggleTransitionEffect1.Target.Assign(Bmp);
  finally
    Bmp.Free;
  end;

  pnlSecond.Visible := False;
end;
```

:::note warn
MakeScreenshot はコントロールが Visible で実際に画面上にレンダリング(OnApplyStyleLookup 前には取得できません)されていないと正しく取得できません！
:::
:::note warn
Target を指定しないと Android で意図通り動作しません（黒と混合されます）
Target を指定したくないときは
```pascal
var Bmp := TBitmap.Create(1, 1);
try
  Bmp.Clear(0);
  WiggleTransitionEffect1.Target.Assign(Bmp);
finally
  Bmp.Free;
end;
```
として、透明 Bitmap を指定します。
:::

### 開始
Button1 の OnClick でアニメーションをスタートします。
```WiggleTransitionEffect1.Enabled := True;```として Transition を有効化しています。

```pascal
procedure TfrmSample.Button1Click(Sender: TObject);
begin
  WiggleTransitionEffect1.Enabled := True;
  FloatAnimation1.Start;
end;
```

### 終了
アニメーションが終了するとここが呼ばれます。
まず、```WiggleTransitionEffect1.Enabled := False;```で Transition を無効化します。
次に、```layoutFirst.Visible := False;```で First を非表示にします。
最後に、```pnlSecond.Visible := True;```で Second を表示します。

```pascal
procedure TfrmSample.FloatAnimation1Finish(Sender: TObject);
begin
  WiggleTransitionEffect1.Enabled := False;
  layoutFirst.Visible := False;
  pnlSecond.Visible := True;
end;
```

# 実行
ここまでを実行すると、ぐにょおおおおと画面が変わります。

<img width=480 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/3e53236a-e83e-94a4-2f13-f7d13d978b45.gif">

# 冒頭の Transition について

まずこんな画面を作ります。

<img width=360 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/b74369f6-c6db-fe3e-f952-843f76e87091.png">

↓のコードでやっていることは上記のコードと基本的には同じです。
ただ、GridLayout からまるで外に飛び出したように見せるための座標計算、飛び出した画像の大きさを変更するための [TFloatAnimation.OnProcess](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Ani.TAnimation.OnProcess) などが書いてあります。
また、[TBlurTransitionEffect](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Filter.Effects.TBlurTransitionEffect) は画面外に飛び出したように見えるため Target は透明画像を指定しています。

:::note info
Transition の種類によっては Target 画像を指定しない方が上手く見える事も良くあります。
（[MakeScreenshot](https://docwiki.embarcadero.com/Libraries/Sydney/ja/FMX.Controls.TControl.MakeScreenshot) をしなくて良いので使える場合は、こちらの方が楽です）
:::

<details><summary>コード全文</summary>

```pascal
unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    layoutGallery: TGridLayout;
    Image1: TImage;
    Image2: TImage;
    Image24: TImage;
    Image3: TImage;
    Image4: TImage;
    Image5: TImage;
    Image6: TImage;
    Image7: TImage;
    Image8: TImage;
    Image9: TImage;
    Image10: TImage;
    Image11: TImage;
    Image12: TImage;
    Image13: TImage;
    Image14: TImage;
    Image15: TImage;
    Image16: TImage;
    Image17: TImage;
    Image18: TImage;
    Image19: TImage;
    Image20: TImage;
    Image21: TImage;
    Image22: TImage;
    Image23: TImage;
    StyleBook1: TStyleBook;
    BlurTransitionEffect1: TBlurTransitionEffect;
    FloatAnimation1: TFloatAnimation;
    layoutOneImageBase: TLayout;
    imgOneImage: TImage;
    imgTarget: TImage;
    layoutRoot: TLayout;
    procedure FloatAnimation1Finish(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FloatAnimation1Process(Sender: TObject);
    procedure ImageClick(Sender: TObject);
    procedure imgOneImageClick(Sender: TObject);
    procedure layoutGalleryResized(Sender: TObject);
    procedure FormKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char;
      Shift: TShiftState);
  private var
    FTarget: TImage;
    FStartPos: TPointF;
    FStartSize: TSizeF;
    FDeltaPos: TPointF;
    FDeltaSize: TSizeF;
  private
    procedure StartTransition(const AInverse: Boolean);
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

{$IFDEF ANDROID}
uses
  Androidapi.Helpers;
{$ENDIF}

procedure TForm1.FloatAnimation1Finish(Sender: TObject);
begin
  // アニメーションの終了処理
  // Transition 無効化
  BlurTransitionEffect1.Enabled := False;

  // 可視状態変更
  imgTarget.Visible := False;
  FTarget.Visible := True;

  if FloatAnimation1.Inverse then
  begin
    layoutOneImageBase.Visible := False;
    imgOneImage.Bitmap.Assign(nil);
  end
  else
  begin
    layoutOneImageBase.Opacity := 1;
    layoutOneImageBase.Visible := True;
    layoutGallery.Visible := False;
  end;
end;

procedure TForm1.FloatAnimation1Process(Sender: TObject);
begin
  // アニメーションの段階毎に呼ばれる
  // 進捗パーセンテージを計算
  var P := BlurTransitionEffect1.Progress / FloatAnimation1.StopValue;
  if FloatAnimation1.Inverse then
    P := 1 - P;

  // ターゲットの位置と透明度を設定
  imgTarget.Opacity := P;
  imgTarget.SetBounds(
    FStartPos.X + FDeltaPos.X * P,
    FStartPos.Y + FDeltaPos.Y * P,
    FStartSize.cx + FDeltaSize.cx * P,
    FStartSize.cy + FDeltaSize.cy * P);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  // Transition 用透明画像をセット
  var Bmp := TBitmap.Create(1, 1);
  try
    BlurTransitionEffect1.Target.Assign(Bmp);
  finally
    Bmp.Free;
  end;

  imgTarget.Visible := False;
  layoutOneImageBase.Visible := False;
end;

procedure TForm1.FormKeyDown(
  Sender: TObject;
  var Key: Word;
  var KeyChar: Char;
  Shift: TShiftState);
begin
  {$IFDEF ANDROID}
  // Back Key の制御
  if Key = vkHardwareBack then
  begin
    Key := 0;

    if layoutOneImageBase.Visible then
      imgOneImageClick(nil)
    else
      TAndroidHelper.Activity.moveTaskToBack(True);
  end;
  {$ENDIF}
end;

procedure TForm1.ImageClick(Sender: TObject);
begin
  if not (Sender is TImage) then
    Exit;

  // ターゲット設定
  FTarget := TImage(Sender);
  imgOneImage.Bitmap.Assign(FTarget.Bitmap);

  // アニメーション
  StartTransition(False);
end;

procedure TForm1.imgOneImageClick(Sender: TObject);
begin
  // 逆順アニメーション
  StartTransition(True);
end;

procedure TForm1.layoutGalleryResized(Sender: TObject);
begin
  // 画面内が４列になるように計算
  var Size := layoutGallery.Width;
  if Size > layoutGallery.Height then
    Size := layoutGallery.Height;

  Size := (Size - layoutGallery.Padding.Left * 2) / 4;

  layoutGallery.ItemWidth := Size;
  layoutGallery.ItemHeight := Size;
end;

procedure TForm1.StartTransition(const AInverse: Boolean);
const
  ONE_IMAGE_MARGIN = 16 * 2;
begin
  if
    (FTarget = nil) or
    (FTarget.Parent = nil) or
    (imgOneImage.Parent = nil) or
    (not (FTarget.Parent is TControl)) or
    (not (imgOneImage.Parent is TControl))
  then
    Exit;

  // 綺麗に見せるための表示調整
  FTarget.Visible := False;
  layoutGallery.Visible := True;

  layoutOneImageBase.Opacity := 0;
  layoutOneImageBase.Visible := True;

  layoutOneImageBase.BeginUpdate;
  try
    var Size := layoutOneImageBase.Width;
    if Size > layoutOneImageBase.Height then
      Size := layoutOneImageBase.Height;

    Size := Size - ONE_IMAGE_MARGIN;

    imgOneImage.SetBounds(0, 0, Size, Size);
  finally
    layoutOneImageBase.EndUpdate;
  end;

  var StartImg, EndImg: TImage;

  // 座標計算
  if AInverse then
  begin
    StartImg := imgOneImage;
    EndImg := FTarget;
  end
  else
  begin
    StartImg := FTarget;
    EndImg := imgOneImage;
  end;

  // 開始位置・終了位置計算
  FStartPos :=
    TControl(StartImg.Parent).LocalToAbsolute(StartImg.Position.Point);
  var EndPos :=
    TControl(EndImg.Parent).LocalToAbsolute(EndImg.Position.Point);

  FStartPos := layoutRoot.AbsoluteToLocal(FStartPos);
  EndPos := layoutRoot.AbsoluteToLocal(EndPos);

  // 終了位置と開始位置の差
  FDeltaPos.X := EndPos.X - FStartPos.X;
  FDeltaPos.Y := EndPos.Y - FStartPos.Y;

  // 開始状態の大きさ
  FStartSize.cx := StartImg.Width;
  FStartSize.cy := StartImg.Height;

  // 開始と終了の大きさの差
  FDeltaSize.cx := EndImg.Width - StartImg.Width;
  FDeltaSize.cy := EndImg.Height - StartImg.Height;

  // Target Image 設定
  imgTarget.Bitmap.Assign(EndImg.Bitmap);
  imgTarget.Visible := True;

  // スタート
  BlurTransitionEffect1.Enabled := True;

  FloatAnimation1.Inverse := AInverse;
  FloatAnimation1.Start;
end;

end.
```

</details>

# まとめ
Transition の使い方をまとめると
1. Transition を元画面に付ける
1. TFloatAnimation を Transition に付ける
1. Transition.Target に遷移先画面の Bitmap か、透明 Bitmap を指定する
1. 遷移先画面を非表示にする
1. FloatAnimation.Start でアニメーションをスタートする
1. アニメーションが終了したら、元画面を非表示にし、遷移先画面を表示する

となります。
言葉で説明すると長いですが、コードは上に上げたたったこれだけです。

Delphi で作られたアプリケーションがもっと華やかになりますように！