Xamarin.Formsで業務アプリで要求されそうなEntryを作る

「Xamarin Advent Calendar 2020」21日目です。

Xamarinに限らず、業務系のアプリケーションを開発している人なら、テキストボックスに対してなんだかんだ細かい要求が出てくる経験をしたことがありますよね。

プロジェクトの規模が大きいと、サードパーティ製のリッチなコンポーネントを使う、なんてこともあるでしょうけど、規模が小さく、いくつかの入力欄に対して限定的な機能だけ用意できればいいようなケースだと、外部のコンポーネントを使うと余計に手間がかかることがあるので、自前で組んだほうが扱いやすいですね。

ということで、特に高度な技でも何でもないんですが、これまでXamarin.Formsでアプリを作ってきてEntryにしてきたカスタマイズをいくつか紹介したいと思います。

## おしながき

1. Entryの枠を消す
2. 組み合わせでデザインする
3. Entry選択時に、内容を全選択する
4. キーボードの入力モードを変更する(iOSだけ)

内容的に細かいバージョンに依存することはないと思いますが、この記事では

* Xamarin.Forms 4.8.0.1687
* Androidは10.0のエミュレータ
* iOSは14.3のシミュレータ

で動かしています。

## 1.Entryの枠を消す

Frameと組み合わせたり、何かの上に重ねたりするときに枠やアンダーラインがないほうが便利です。
Rendererを使って消します。

### CustomEntry.cs(共通プロジェクト)

```c#
public class CustomEntry : Entry {}
```

### CustomEntryRenderer.cs(iOS)

```c#
[assembly: ExportRenderer(typeof(CustomEntry), typeof(CustomEntryRenderer))]
namespace Hoge.iOS
{
    public class CustomEntryRenderer: EntryRenderer
    {
        protected override UITextField CreateNativeControl()
        {
            var control = base.CreateNativeControl();
            control.Layer.BorderWidth = 0;
            control.BorderStyle = UITextBorderStyle.None;

            return control;
        }
    }
}
```

### CustomEntryRenderer.cs(Android)

Paddingを0にしているのは、ほかのコントロールと組み合わせるときに都合がいいからです。

```c#
[assembly: ExportRenderer(typeof(CustomEntry), typeof(CustomEntryRenderer))]
namespace Hoge.Droid
{
    public class CustomEntryRenderer: EntryRenderer
    {
        public CustomEntryRenderer(Context context) : base(context) {}

        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement == null)
            {
                Control.Background = null;
                Control.SetPadding(0, 0, 0, 0);
            }
        }
    }
}
```

### MainPage.xaml

```xml
  <Grid HorizontalOptions="Fill"
        VerticalOptions="Fill">
    <Grid.RowDefinitions>
      <RowDefinition Height="1*"/>
      <RowDefinition Height="1*"/>
      <RowDefinition Height="2*"/>
    </Grid.RowDefinitions>

    <Entry Grid.Row="0"
           WidthRequest="200"
           HorizontalOptions="Center"
           VerticalOptions="Center"
           Text="Entry"/>

    <controls:CustomEntry Grid.Row="1"
                          WidthRequest="200"
                          HorizontalOptions="Center"
                          VerticalOptions="Center"
                          Text="CustomEntry"/>
  </Grid>
```

#### iOS

![1-iphone.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/943070/a8aee929-d96e-cee1-a828-a02630152230.png "iOS")

#### Android

![1-android.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/943070/130af27a-69ba-0900-6f27-00e8880c4732.png "Android")

## 2.組み合わせでデザインする

1で枠を消したEntryを使って組み合わせた例です。

### MainPage.xaml(共通プロジェクト)

```xml
  <Grid HorizontalOptions="Fill"
        VerticalOptions="Fill">

    <Frame BorderColor="LightGray" 
           BackgroundColor="Azure" 
           HasShadow="False"
           VerticalOptions="Center"
           Padding="4"
           Margin="30" 
           CornerRadius="10">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="Auto"/>
          <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Label Grid.Column="0"
               Margin="4,6,2,2"
               VerticalOptions="Center"
               FontSize="Small"
               Text="Item:"/>

        <RelativeLayout Grid.Column="1" VerticalOptions="Center" Margin="4,0,0,0">
          <controls:CustomEntry HorizontalOptions="FillAndExpand"
                                RelativeLayout.HeightConstraint="{ConstraintExpression Type=Constant, Property=Height, Constant=40}"
                                RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToParent, Property=Width}"
                                Text="CustomEntry"/>
        </RelativeLayout>
      </Grid>
    </Frame>
  </Grid>
```

#### iOS

![2-iphone.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/943070/121ece3c-7cf5-7637-321b-2fc4afb2db8b.png "iOS")

#### Android

![2-android.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/943070/5cef578e-4510-ef81-281e-0e2ce8c0110c.png "Android")


## 3.Entry選択時に、内容を全選択する

そのままです。


### CustomEntry.cs(共通プロジェクト)

```c#
public class CustomEntry : Entry {}
```

### CustomEntryRenderer.cs(iOS)

```c#
[assembly: ExportRenderer(typeof(CustomEntry), typeof(CustomEntryRenderer))]
namespace Hoge.iOS
{
    public class CustomEntryRenderer: EntryRenderer
    {
        protected override UITextField CreateNativeControl()
        {
            var control = base.CreateNativeControl();

            control.EditingDidBegin += (s, args) => {
                control.PerformSelector(new ObjCRuntime.Selector("selectAll"), null, 0.0f);
            };

            return control;
        }
    }
}
```

### CustomEntryRenderer.cs(Android)

```c#
[assembly: ExportRenderer(typeof(CustomEntry), typeof(CustomEntryRenderer))]
namespace Hoge.Droid
{
    public class CustomEntryRenderer: EntryRenderer
    {
        public CustomEntryRenderer(Context context) : base(context) {}

        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement == null)
            {
                var nativeEditText = (Android.Widget.EditText)Control;
                nativeEditText.SetSelectAllOnFocus(true);
            }
        }
    }
}
```

## 4.キーボードの入力モードを変更する(iOSだけ)

EntryのKeyboardプロパティを使えば限定した入力ができるキーボードにすることができますが、これだと制限がきつい場合があります。
「ほとんど数字しか入力しないんだけど、まれに数字以外を入力する場合がある」みたいな項目だと、KeyboardをNumericにしてしまうと都合が悪く、かといって普通のキーボードのままだと、入力する側は毎回数値入力モードに切り替えるのが面倒だから何とかしてくれ、みたいなことになります。
なので、Keyboardプロパティを使わず、「標準のキーボードで数値入力状態に切り替わるだけ」という状況にできるとベストです。

…ですが、iOSではできてるんですが、Androidはいろいろ調べても入力モードだけを切り替える方法が見つかりません。
なんとなく無理かも…と思ってるんですが、なにか知ってる方がいらっしゃったら教えていただけると幸いです。

### CustomEntry.cs(共通プロジェクト)

プロパティを用意しておきます。

```c#
public class CustomEntry : Entry {
    public static readonly BindableProperty FieldKeyboardTypeProperty =
        BindableProperty.Create(nameof(FieldKeyboardType), typeof(KeyboardType), typeof(CustomEntry), KeyboardType.Default);

    public KeyboardType FieldKeyboardType
    {
        get => (KeyboardType)GetValue(FieldKeyboardTypeProperty);
        set => SetValue(FieldKeyboardTypeProperty, value);
    }
}

public enum KeyboardType
{
    Default,
    Alphabet,
    Number
}
```

### CustomEntryRenderer.cs(iOS)

UITextFieldにはKeyboardTypeというプロパティがあり、

* [UIKeyboardType | Apple Developer Documentation](https://developer.apple.com/documentation/uikit/uikeyboardtype)

にあるようなキーボード状態を指定できます。
ASCIICapableとNumbersAndPunctuationを指定してやるとモードが変わるので、CustomEntryのプロパティを見てKeyboardTypeを切り替える、ということをします。

```c#
[assembly: ExportRenderer(typeof(CustomEntry), typeof(CustomEntryRenderer))]
namespace Hoge.iOS
{
    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        if (Control == null) return;

        if (e.PropertyName == "FieldKeyboardType" || e.PropertyName == "Renderer")
        {
            Control.KeyboardType = ((CustomEntry)sender).FieldKeyboardType switch
            {
                KeyboardType.Default => UIKeyboardType.Default,
                KeyboardType.Alphabet => UIKeyboardType.ASCIICapable,
                KeyboardType.Number => UIKeyboardType.NumbersAndPunctuation,
                _ => UIKeyboardType.Default
            };
        }
    }
}
```

#### iOS

![4-iphone.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/943070/87907fbf-a608-997b-0a29-666cba062742.gif)
※1～4まで全部まとめた状態です。

## まとめ

どこででも紹介されてるものばかりで恐縮ですが、今まで作ってきたアプリのEntryにちょこちょこやってきたようなものを書いてみました。