Open7

[.NET MAUI] 入力用リスト

GomitaGomita

AbsoluteLayoutによるUIの重ね

ページ(最上位のGrid)の前面に入力用リスト(ListView)を重ねて表示させるため、ページ全体をAbsoluteLayoutでレイアウトし直す。

MainPage.xaml
<ContentPage ...
...
+    <AbsoluteLayout>
+        <Grid RowSpacing="0" ColumnSpacing="0"
+              AbsoluteLayout.LayoutFlags="PositionProportional,HeightProportional"
+              AbsoluteLayout.LayoutBounds="0.5,0,375,1">
-        <Grid RowSpacing="0" ColumnSpacing="0">
...
        </Grid>

+        <!-- ▼車種入力用リスト -->
+        <ListView x:Name="CarTypeList"
+                  AbsoluteLayout.LayoutFlags="PositionProportional"
+                  AbsoluteLayout.LayoutBounds="0.5,1,375,300">
+        </ListView>
+
+    </AbsoluteLayout>
</ContentPage>

AbsoluteLayout.LayoutFlagsとAbsoluteLayout.LayoutBoundsの使い方はこれが参考になる。
https://qiita.com/P3PPP/items/688c658e3dd95df78eb5

今回の場合、一番親のGridはLayoutFlagsにPositionProportionalとHeightProportionalを指定しているため、LayoutBoundsのカンマ区切りの4つの値は下記のように解釈される。
※PositionPropotionalは「XPropotional,YPropotional」と等価。

0.5 X方向の位置 相対指定 中寄せ
0 Y方向の位置 相対指定 上寄せ
375 絶対指定 375
1 高さ 相対指定 縦方向いっぱい
GomitaGomita

リストの内容生成

車種入力用リストの内容を空っぽではなく「コンパクト」「ミニバン」とかにしてみる。
リストはテンプレートを使って生成する。「.NET MAUIによるマルチプラットフォームアプリ開発」という書籍を参照してコードを書いたが、なぜか書籍通り<ViewCell>に<Label>を入れる方法ではラベルの文字列が表示されなかったので、下記の通り<TextCell>を使用する。

MainPage.xaml
        <!-- ▼車種入力用リスト -->
        <ListView x:Name="CarTypeList"
                  AbsoluteLayout.LayoutFlags="PositionProportional"
                  AbsoluteLayout.LayoutBounds="0.5,1,375,300">
+            <ListView.ItemTemplate>
+                <DataTemplate>
+                    <TextCell Text="{Binding Name}" />
+                </DataTemplate>
+            </ListView.ItemTemplate>
        </ListView>

コードビハインドは下記の通り。ページロード時にリストを生成する。CarTypeという独自のクラスを定義しておき、CarTypeクラスのNameプロパティをListViewのTextCellの文字列にバインドする。

MainPage.xaml.cs
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
+        this.Loaded += MainPage_Loaded;
    }

+    private void MainPage_Loaded(object sender, EventArgs e)
+    {
+        var lst = new List<CarType>();
+        lst.Add(new CarType() { Name = "コンパクト" });
+        lst.Add(new CarType() { Name = "ミニバン" });
+        lst.Add(new CarType() { Name = "セダン" });
+        lst.Add(new CarType() { Name = "ワゴン" });
+        lst.Add(new CarType() { Name = "SUV" });
+        lst.Add(new CarType() { Name = "スポーツ" });
+        lst.Add(new CarType() { Name = "商用" });
+        CarTypeList.ItemsSource = lst;
+    }
    
}

+public class CarType
+{
+    public string Name { get; set; } = "";
+}

めでたく車種入力用リストが表示された!

GomitaGomita

リストの表示/非表示

このままではページを表示した直後に車種入力用リストが表示されたままになってしまうので、下記のような動作にする。

  • ページ表示直後はリスト非表示
  • 車種の入力欄にフォーカスしたらリスト表示
  • フォーカスがロストしたらリスト非表示
MainPage.xaml
                        <controls:BHPANEL LabelText="車種" />
                        <Entry x:Name="CarTypeEntry"
                               WidthRequest="180"
+                              Focused="CarType_Focused"
+                              Unfocused="CarType_Unfocused"
MainPage.xaml.cs
    private void MainPage_Loaded(object sender, EventArgs e)
    {
        ...
+        // 車種入力用リストを非表示にする
+        CarType_Unfocused(sender, e);
    }
    
+    private void CarType_Focused(object sender, EventArgs e)
+    {
+        CarTypeList.IsVisible = true;
+    }
+
+    private void CarType_Unfocused(object sender, EventArgs e)
+    {
+        CarTypeList.IsVisible = false;
+    }

ページ表示直後のリスト非表示について、本来はMainPage.xaml側で<ListView ... IsVisible="false">としたかったが、AbsoluteLayoutとの兼ね合いか、なぜかその後IsVisibleをtrueに変えても何も描画されないバギーな動きをしたので、やむを得ずMainPage_Loaded内でCarType_Unfocusedを呼ぶことにした。

GomitaGomita

アニメーション

こちらの記事を参考に、リスト表示/非表示にちょっとしたアニメーションを付けてみる。
アニメーションはViewExtensions.TranslateToを使い、リストの高さ分だけイージングかけて上下移動させる。

MainPage.xaml.cs
+    private async void CarType_Focused(object sender, EventArgs e)
-    private void CarType_Focused(object sender, EventArgs e)
    {
+        await CarTypeList.TranslateTo(0, CarTypeList.Height, 0);
        CarTypeList.IsVisible = true;
+        await CarTypeList.TranslateTo(0, 0, 200, Easing.CubicOut);
    }

+    private async void CarType_Unfocused(object sender, EventArgs e)
-    private void CarType_Unfocused(object sender, EventArgs e)
    {
+        await CarTypeList.TranslateTo(0, CarTypeList.Height, 200, Easing.CubicOut);
        CarTypeList.IsVisible = false;
    }
GomitaGomita

リスト選択時の動作

リストから選択した車種を入力欄に反映する。

MainPage.xaml
        <!-- ▼車種入力用リスト -->
        <ListView x:Name="CarTypeList"
+                  ItemSelected="CarTypeList_ItemSelected"
MainPage.xaml.cs
+    void CarTypeList_ItemSelected(System.Object sender, Microsoft.Maui.Controls.SelectedItemChangedEventArgs e)
+    {
+        CarType item = e.SelectedItem as CarType;
+        CarTypeEntry.Text = item.Name;
+        CarTypeEntry.Unfocus();
+    }
GomitaGomita

ここまで、リストから入力する対象のUIをEntryで作成して、FocusedUnfocusedイベントでリストの表示/非表示を実装してきたが、iPhone実機で動作確認したところ、Entryに対するFocusedイベントでiOS標準のソフトキーボードが表示されてしまうことが判明した。

こちらにあるEntryクラスのHideSoftInputAsyncを使うとソフトキーボードを非表示にすることは可能だが、非表示にした瞬間Entryに対するフォーカスも失われてしまうため、求めている動作とならなかった。

他にも、CommunityTookit.MauiのKeyboardExtensionsを使ってEntry.HideKeyboardAsyncも試してみたが、これも結果は全く同じ。
https://learn.microsoft.com/ja-jp/dotnet/communitytoolkit/maui/extensions/keyboard-extensions

かなり試行錯誤した結果、Entryを辞めてLabelを継承した独自のBHEDITコントロールを作成することにした。入力用リストを表示するのはTapGestureRecognizerTappedイベントで実装できるが、問題はフォーカスが外れた時にリストの非表示をどう実装すべきかという点。

これは、ContentPageのかなり上の階層にあるGridに対してTapGestureRecognizerを追加して、Tapped時に入力用リストを非表示にする処理を追加することで解決。ただし、なぜかAndroidではGridに対するTapGestureRecognizerが反応してくれないという別の問題が発生。ひとまず今回はiOSとMacをターゲットにしているのでこのまま先へ進むことにした。

この続きはこちら
https://zenn.dev/gomita/scraps/e51bae5707869e