［小ネタ］タブコントロールのタブを中央寄せする

#はじめに
スマートフォン上でも<code>TTabControl</code> の<code>FullSize</code>を<code>False</code>にして使いたいなあ、という場面があったのですが、ジェスチャでページ送りするとタブが隠れて辛い！となったので、センタリングするようにしてみました。
![tabmoveanimetion.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33684/4dea525b-576a-80c4-bf52-bc2b04dde82d.gif)（こんな感じ）

Delphi10.3 Community Editionにて確認しています。
記事内のコードの命名の微妙さについては・・ご指摘頂ければと思います。

##やり方

###移動量の計算
TTabControlのボタン部分はスクロール可能な領域になっており、スクロール位置は<code>TabContentPosition</code>で取得・変更可能です。やりたいのは中央寄せなので、目標のタブボタンの中心と、タブコントロールの中心の距離を計算します。（下図参照）
![fig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33684/e81de699-4324-3b7d-c709-2d4ac74e14b7.png)

```pascal
function CalcTabCenteringDistance(targetTabIndex:Integer;tabControl:TTabControl):Single;
var
  i,n:Integer;
  targetTabCenter:Single;
begin
  n := Min(targetTabIndex, tabControl.TabCount-1);

  targetTabCenter := tabControl.TabContentPosition;
  for i := 0 to n do
  begin
    if i < n then
      targetTabCenter := targetTabCenter + tabControl.Tabs[i].Width
    else
      targetTabCenter := targetTabCenter + tabControl.Tabs[i].Width / 2;
  end;

  result := tabControl.Width / 2 - targetTabCenter;
end;
```
※<code>TabItem</code>に<code>Margin</code>を設定して使用している場合は、上記の計算では考慮されていないので、<code>targetTabCenter</code> に足すようコードを修正してください。

###アニメーションさせる　
実際にアクティブタブを中央寄せするメソッドは以下です。<code>TabContentPosition</code>の値の範囲は<code>(Width-TabContentSize.Width) ～ 0</code>なので、一応それも考慮します（内部で超えないようにしてくれているは思う）。
<code>FloatAnimation1</code>は、TabControl1 の子として配置し、<code>PropatyName</code>を<code>TabContentPosition</code>、<code>StartFromCurrent</code>を<code>True</code>に設定しておきます。

```pascal
procedure TForm1.AlignActiveTabWithCenter;
var
  newPosition:Single;
begin
  newPosition := TabControl1.TabContentPosition +
    CalcTabCenteringDistance(TabControl1.TabIndex, TabControl1);

  if newPosition > 0 then
    newPosition := 0
  else if newPosition < TabControl1.Width - TabControl1.TabContentSize.Width then
    newPosition := TabControl1.Width - TabControl1.TabContentSize.Width;

  FloatAnimation1.StopValue := newPosition;
  FloatAnimation1.Start;
end;
```

###上手くいくと思ったのに！
さっそく上記の<code>AlignActiveTabWithCenter</code>メソッドを<code>TabControl.OnChange</code>イベントで呼び出して・・。と、やってみると、中央寄せはされますが、アニメーションはスキップされてしまいます。なぜだ。

###代替案
色々試してみると、どうもタブの遷移アニメーション中は、ボタン部分のアニメーションが動かないようです。
よって、タブ遷移完了後に<code>AlignActiveTabWithCenter</code>を呼ぶ必要があります。

その辺りを考慮して次のように実装してみました。

```pascal

var FAligningActiveTab : Boolean;

procedure TForm1.PrevButtonClick(Sender: TObject);
var
  newIndex:Integer;
begin
  newIndex := TabControl1.FindVisibleTab(TTabControl.TFindKind.Back);
  if newIndex > -1 then
  begin
    FAligningActiveTab := True;

    TabControl1.SetActiveTabWithTransitionAsync(
      TabControl1.Tabs[newIndex],
      TTabTransition.Slide,
      TTabTransitionDirection.Reversed,
      AlignActiveTabWithCenter );
  end;
end;

procedure TForm1.NextButtonClick(Sender: TObject);
var
  newIndex:Integer;
begin
  newIndex := TabControl1.FindVisibleTab(TTabControl.TFindKind.Next);
  if newIndex > -1 then
  begin
    FAligningActiveTab := True;

    TabControl1.SetActiveTabWithTransitionAsync(
      TabControl1.Tabs[newIndex],
      TTabTransition.Slide,
      TTabTransitionDirection.Normal,
      AlignActiveTabWithCenter );
  end;
end;

procedure TForm1.FloatAnimation1Finish(Sender: TObject);
begin
  FAligningActiveTab := False;
end;

procedure TForm1.TabControl1Change(Sender: TObject);
begin
  if not FAligningActiveTab then
  begin
    AlignActiveTabWithCenter;
  end;
end;
```
遷移アニメーションが発生しないタブ切り替えの場合、<code>OnChange</code>イベントで中央寄せを行います。
遷移アニメーションがある（したい）場合は、<code>FAligningActiveTab</code> フラグを立て、OnChangeイベント側をスルーするようにしておいて、<code>SetActiveTabWithTransitionAsync</code>の<code>AOnFinish</code>引数にメソッドを渡すことで中央寄せを行います。

以上です！

##こぼれ話
<code>TTabControl</code>の<code>FullSize</code>を<code>False</code>にした状態でTabItem内に<code>TPanel</code>を配置すると、Panel上でPANジェスチャした際にタブボタンがスクロールする、という現象が発生します（Android上でのみ。iOSは未確認です）。

対処としては、Panelプロパティにて<code>InteractiveGesture.Pan = True</code>とした上で、同Panelの<code>OnGestrue</code>イベント内で<code>Handled:=True</code>とすると回避できました。