Open4

[.NET MAUI] UIに独自の状態変化

GomitaGomita

https://zenn.dev/gomita/scraps/dd5b805746e692
ここから引き続き、「車種」や「乗車地」などのリストから選択する対象のUIを標準のEntryから独自のBHEDITに置き換える。

BHEDIT.xaml
...
    <Border BindingContext="{x:Reference this}"
            x:Name="ActivityBorder"
            Stroke="Transparent"
            StrokeThickness="4"
            StrokeShape="RoundRectangle 8">
        <Label Text="{Binding InputText}"
               HeightRequest="40"
               FontSize="18" TextColor="#000" BackgroundColor="#FFF"
               Padding="4" />
    </Border>

    <ContentView.GestureRecognizers>
        <TapGestureRecognizer NumberOfTapsRequired="2" Tapped="DoubleTapped" />
    </ContentView.GestureRecognizers>

</ContentView>

UIとして表示する文字列を独自のInputTextプロパティとして実装する。
また、UIをダブルタップして文字列を消去する処理もこちら側に実装しておく。

BHEDIT.xaml.cs
...
public partial class BHEDIT : ContentView
{
	public static readonly BindableProperty InputTextProperty = BindableProperty.Create(nameof(InputText), typeof(string), typeof(BHEDIT), string.Empty);

	public string InputText
	{
		get => (string)GetValue(InputTextProperty);
		set => SetValue(InputTextProperty, value);
	}

	public BHEDIT()
	{
		InitializeComponent();
	}

	private void DoubleTapped(object sender, EventArgs e)
	{
		InputText = "";
	}

}
GomitaGomita

独自コントロールを使用するMainPageはこのようになる。

MainPage.xaml
                    <HorizontalStackLayout Margin="6">
                        <controls:BHPANEL LabelText="車種" VerticalOptions="Center" />
+                       <controls:BHEDIT x:Name="CarTypeInput" VerticalOptions="Center"
+                                        WidthRequest="180">
+                          <controls:BHEDIT.GestureRecognizers>
+                              <TapGestureRecognizer NumberOfTapsRequired="1" Tapped="SingleTapped" />
+                          </controls:BHEDIT.GestureRecognizers>
+                       </controls:BHEDIT>
                    </HorizontalStackLayout>

また、この後BHEDITから擬似的に「フォーカスが外れた」ことを検知するためにContentPage > AbsoluteLayout > Gridとかなり上の階層にGestureRecognizerを追加しておく。

MainPage.xaml
+            <Grid.GestureRecognizers>
+                <TapGestureRecognizer NumberOfTapsRequired="1" Tapped="SingleTapped" />
+            </Grid.GestureRecognizers>
GomitaGomita

独自の状態変化

やりたいことは…

  • BHEDITクラスのUIをタップすると、活性状態になって枠が水色になる。
  • BHEDITクラスの外側をタップすると、非活性状態になって枠が透明になる。

BHEDIT.xamlに戻って、ルートのContentView直下にVisualStateManagerを追加する。
活性/非活性状態をActivityStatesと名付けて、値がActiveの場合はBorderを水色に変更、Inactiveの場合は透明に変更する。

BHEDIT.xaml
+    <VisualStateManager.VisualStateGroups>
+        <VisualStateGroup Name="ActivityStates">
+            <VisualState Name="Active">
+                <VisualState.Setters>
+                    <Setter TargetName="ActivityBorder" Property="Border.Stroke" Value="LightSkyBlue" />
+                </VisualState.Setters>
+            </VisualState>
+            <VisualState Name="Inactive">
+                <VisualState.Setters>
+                    <Setter TargetName="ActivityBorder" Property="Border.Stroke" Value="White" />
+                </VisualState.Setters>
+            </VisualState>
+        </VisualStateGroup>
+    </VisualStateManager.VisualStateGroups>

コードビハインド側で、SetActiveという名前の状態変更用メソッドを実装する。引数がtrueであればVisualStateManager.GoToStateでthisすなわちルートのContentViewの状態をActiveにする。

BHEDIT.xaml.cs
+    public void SetActive(bool isActive)
+    {
+        string state = isActive ? "Active" : "Inactive";
+        VisualStateManager.GoToState(this, state);
+    }
GomitaGomita

MainPage側はこのようになる。

MainPage.xaml.cs
	async void SingleTapped(System.Object sender, Microsoft.Maui.Controls.TappedEventArgs e)
	{
		Console.WriteLine("SingleTapped: " + sender.ToString());
+		// 最初に全部のBHEDITの活性状態をリセットする
+		CarTypeInput.SetActive(false);
+		RegionFromInput.SetActive(false);
+		RegionToInput.SetActive(false);
+		if (sender == CarTypeInput)
+		{
+			CarTypeInput.SetActive(true);
+		}
+		else if (sender == RegionFromInput || sender == RegionToInput)
+		{
+			((Controls.BHEDIT)sender).SetActive(true);
+		}
    }

これでようやく、BHEDITクラスのUIの内側/外側をタップして枠の色を変えることに成功した!

ただし問題があり、Androidの場合はGridに対するTapGestureRecognizerが反応せず(厳密にはGridの上に被さっているHorizontalStackLayoutなどにTapGesutreRecognizerを設定する必要があり)、ちょっとめんどくさいので一旦このまま先へ進むことにした。
どうやらAndroidとiOSなどでイベントのバブリング的な動作が異なるっぽい。