23
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Xamarin.Forms でソフトウェアキーボードが表示された時に画面が隠れないようにする

Last updated at Posted at 2017-01-12

Xamarin.Forms で、ソフトウェアキーボードを表示した時の動きが、Android と iOS で違って、いずれも目的の動作と合わなかったので、調べてみました。

やりたいこと

これ↓

いわゆる LINE のような画面、リストビューと文字列入力があって、文字列入力にフォーカスが当たるとソフトウェアキーボードが表示され、その分リストビューの高さが縮む、という動きです。
これを Xamarin.Forms(Android と iOS)で実現したいです。

Android の場合

Xamarin.Forms アプリの Android 側で、特になにもせずに LINE 風の画面を作って動かすと、下図のようになります。

ソフトウェアキーボードによって、画面が隠れることはありませんが、ListView の高さが縮んでいるのではなく、 画面全体が上へスライド しています。そのため、キーボードを表示したまま、ListView の先頭の項目を見ることができません。

Android ネイティブでは、 AndroidManifest.xml の activity の属性に windowSoftInputMode="adjustResize" を設定することで実現できます(付けなくても既定値がこれなのかな?)。

おーけーおーけー、Xamarin では AndroidManifest.xml ではなく MainActivity.cs のクラスの属性に書けばOKだな、というわけで下のように記述してみました。

// MainActivity.cs
[Activity(Label = "ImeStretchSample.Droid",
            Icon = "@drawable/icon",
            Theme = "@style/MyTheme",
            MainLauncher = true,
            ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,
            WindowSoftInputMode = SoftInput.AdjustResize)]  // ←ここだよー!!!
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    protected override void OnCreate(Bundle bundle)
    {
        // 以下略

ところがこれが機能しません。

ググってみると Bugzilla に登録されてました。

Application.Current.On<Android>().UseWindowSoftInputModeAdjust(

Xamarin.Forms の 2.3.3 以降で、上記メソッドが使えるらしい、と。
現在の Stable は 2.3.3.180 なので使えますね、使ってみましょう。

// MainActivity.cs
protected override void OnCreate(Bundle bundle)
{
    TabLayoutResource = Resource.Layout.Tabbar;
    ToolbarResource = Resource.Layout.Toolbar;

    base.OnCreate(bundle);
    global::Xamarin.Forms.Forms.Init(this, bundle);
    LoadApplication(new App());

    App.Current.On<Xamarin.Forms.PlatformConfiguration.Android>()
        .UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Resize); // ←ここ!!
}

これを実行すると、

ListView は適切に縮んでいますが、 なんだあのステータスバー付近の空白は!!!

さらにググります。

こんな Workaround を見つけました。
適用してみます。

// MainActivity.cs
protected override void OnCreate(Bundle bundle)
{
    TabLayoutResource = Resource.Layout.Tabbar;
    ToolbarResource = Resource.Layout.Toolbar;

    base.OnCreate(bundle);

    if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop)
	{
		Window.DecorView.SystemUiVisibility = 0;
		var statusBarHeightInfo = typeof(FormsAppCompatActivity).GetField("_statusBarHeight", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
		statusBarHeightInfo.SetValue(this, 0);
		Window.SetStatusBarColor(new Android.Graphics.Color(18, 52, 86, 255));
	}

    global::Xamarin.Forms.Forms.Init(this, bundle);
    LoadApplication(new App());

    App.Current.On<Xamarin.Forms.PlatformConfiguration.Android>()
        .UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Resize);
}

リフレクションを使っていたり、 SetStatusBarColor が色固定になっていたりと激しく不安ですが、これでようやく、期待どおりの動きになりました。

iOS の場合

Xamarin.Forms の iOS 側で、特になにもせずに、ソフトウェアキーボードを表示させると、ListView と文字列入力項目の手前に被さってしまいます。

通常の画面なら、 ScrollView で囲ってあげることで、適切に ScollView の高さが縮んで、その中がスクロール可能になります。
が、ScrollView と ListView のようにスクローラブルなコントロールを入れ子で使うとトラブルの素なので、ScrollView は選択できません。

iOS ネイティブでは、キーボードが表示されたかどうかを検知して、AutoLayout の制約を設定したり、自力で View のサイズを再計算するようです。

Xamarin.Forms の iOS 側での対策をググって探します。
こんなライブラリを見つけました。

これを適用してみると、以下のような動きになります。

んー、 Android 側の初期状態とおなじく、 画面全体が上へスライド しています。

このライブラリの ソースコード を見てみます。
これは Custom Renderer で実現されていて、キーボードが表示されたら、Page の位置を上方向へ移動させているようです(ShiftPageUp(), ShiftPageDown() というメソッド名だし)。

であれば、この処理を改造して、「移動」ではなく「高さのリサイズ」をすればよいことになります。
以下のように修正しました(コメントアウトは旧コードです)。

// KeyboardOverlapRenderer.cs
private void ShiftPageUp(nfloat keyboardHeight, double activeViewBottom)
{
    var pageFrame = Element.Bounds;

//    var newY = pageFrame.Y + CalculateShiftByAmount(pageFrame.Height, keyboardHeight, activeViewBottom);
//    Element.LayoutTo(new Rectangle(pageFrame.X, newY,
//        pageFrame.Width, pageFrame.Height));

    var newHeight = pageFrame.Height + CalculateShiftByAmount(pageFrame.Height, keyboardHeight, activeViewBottom);
    Element.LayoutTo(new Rectangle(pageFrame.X, pageFrame.Y,
        pageFrame.Width, newHeight));

    _pageWasShiftedUp = true;
}

private void ShiftPageDown(nfloat keyboardHeight, double activeViewBottom)
{
    var pageFrame = Element.Bounds;

//    var newY = pageFrame.Y - CalculateShiftByAmount(pageFrame.Height, keyboardHeight, activeViewBottom);
//    Element.LayoutTo(new Rectangle(pageFrame.X, newY,
//        pageFrame.Width, pageFrame.Height));

    var newHeight = pageFrame.Height + keyboardHeight;
    Element.LayoutTo(new Rectangle(pageFrame.X, pageFrame.Y,
        pageFrame.Width, newHeight));

    _pageWasShiftedUp = false;
}

これを動かすと、下図のようになります。

iOS 側も、求めていた動きになりました。

まとめ

改めて、期待通りの動きになった Xamarin.Forms での画面(Android と iOS)です。

Android 側は、 MainActivity.csUseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Resize) と、WORKAROUND のコードを書きます。

iOS 側は、 KeyboardOverlap.Forms.Plugin のカスタムレンダラー KeyboardOverlapRenderer.cs を少し修正して使用します。

どちらもサンプルアプリを作りました。

/Android が、 Android-Java で作成した「期待値」で、
/XamarinFormsCustomRenderer が、 Xamarin.Forms で「期待値」を再現した iOS/Android アプリです。

ListView + Entry のチャット画面に加えて、 ScrollView を使った画面も用意しています。

最後に

このポストのきっかけは、

からの 一連の流れ です。もともと自分のプログラムでも懸案だったので調べてみました。

ここに書かなかったけど知見になりそうなツイートを貼っておきます。

関わっていただいた皆さん、ありがとうございました。

23
13
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?