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.cs
に UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Resize)
と、WORKAROUND のコードを書きます。
iOS 側は、 KeyboardOverlap.Forms.Plugin のカスタムレンダラー KeyboardOverlapRenderer.cs
を少し修正して使用します。
どちらもサンプルアプリを作りました。
/Android
が、 Android-Java で作成した「期待値」で、
/XamarinFormsCustomRenderer
が、 Xamarin.Forms で「期待値」を再現した iOS/Android アプリです。
ListView + Entry のチャット画面に加えて、 ScrollView を使った画面も用意しています。
最後に
このポストのきっかけは、
あとFormsでLINEっぽいの作ってるが、keyboardにentryが隠れるんどうしたらえぇんや・・・教えて偉い人!
— あるま ゆま@ノベルゲーム製作中 (@ArmaYuma) 2017年1月10日
scrollviewに置いても上手くいかへん・・・
そも②listviewにscrollview乗せたらロクなこと起こらん;;#xamarinforms
からの 一連の流れ です。もともと自分のプログラムでも懸案だったので調べてみました。
ここに書かなかったけど知見になりそうなツイートを貼っておきます。
@ArmaYuma @amay077 これを使ったら一応できました。仕様というよりバグなんですかね?https://t.co/6cgM7yABIU pic.twitter.com/lGjXtfQNC6
— サンテア (@Santea3173) 2017年1月11日
@amay077 iOSではTableView、ListView、ScrollViewがKeyboardInsetTrackerというクラスを使ってキーボード表示、非表示を監視してるようなので、追いかけてみるといいかも?
— ざまりん.ふぉーむずマン👀 (@ticktackmobile) 2017年1月11日
関わっていただいた皆さん、ありがとうございました。