N.Mです.
最近はAndroid用のアプリケーション,"TLExtension"を作っていました.AndroidでTwitter用のブラウザを見つつ,タブ操作で簡単に今まで作ってきたTwitter用ツールも利用できるアプリケーションです.自分用に作ったので,アプリ自体の公開はしていませんが,ひな型部分のソースコードはGitHubにも公開しました.
このアプリはXamarin.Formsを利用して作っているのですが,今回の記事はこれを作る際に困ったこととその解決策をまとめたものとなっております.
(主にAndroidアプリをXamarin.Formsで開発する際に,こんな機能はどう作ればいいのかといった内容になります。)
結局,Xamarin.Formsにあるプラットフォーム共通の機能じゃ足りないので,Xamarin.Androidでの実装が必要になるわけですが...
長くなってしまったので,2回に分けてお送りします.今回はWebView周りの話です.
(次回は画像や動画などの処理に関するメディア編です。)
WebView内に表示されたHTML取得
参考:https://github.com/xamarin/xamarin-forms-samples/tree/master/CustomRenderers/HybridWebView/Droid
CoreTweetでREST APIの認証を取得する際に,過去の記事でも触れたように,PIN番号の入力を自動化しようとすると,HTMLからPIN番号を抽出し,C#側で使えるようにする必要があります.また,Twitter内でのページ遷移(ホーム画面から個々のツイート画面への遷移など)では,WebView.Sourceに格納されるURLが変化しないためか,Navigatingなどのイベントも反応しません.TwitterのページのHTMLソースにはそのページのURLがあるので,これを取得すれば,遷移したかどうかがわかります.こんな感じで,なにかとC#側でHTMLを取得する必要があります.これを実現するには以下のような手順でjavascriptとC#を連携できるようにする必要があります.
① Xamarin.Forms.WebView
クラスを継承して,自作のWebView
クラスを作ります.(自分のリポジトリでは,TLExtensionWebView
というクラス名にしています.)
② Android側のプロジェクトでWebViewRenderer
クラスを作成します.(Xamarin.Forms.Platform.Android.WebViewRenderer
,自分のリポジトリではTLExtensionWebViweRenderer
というクラス名にしています.)このWebViewRenderer
と1で作ったWebView
を以下のようにして連携させます.このように連携すると,WebViewRender.Element
に連携したWebView
が格納されます.
//以下の文章をWebViewRenderクラスを宣言しているところの直前に記述します.
[assembly: ExportRenderer(typeof(TLExtensionWebView), typeof(TLExtensionWebViewRenderer))]
③ javascriptから呼び出せるメソッドを格納したJSBridge
クラス(Java.Lang.Object
を継承)を作ります.コード自体はリポジトリ内のJSBridge.cs
を参考にしてほしいですが,クラス内のメソッドの前に以下のように記述することで,javascript内でC#のそのメソッドを呼び出せるようになります.Export
の中に書いた文字列が,javascript内でのメソッド名になります.
//以下の文章をjavascriptで呼び出したい,JSBridgeのメソッドの直前に記述します.
[JavascriptInterface]
[Export("invokeAction")]
④ WebViewRenderer
クラスのOnElementChanged
メソッドに以下を追加します.
if (e.OldElement != null)
{
Control.RemoveJavascriptInterface("jsBridge");
}
if (e.NewElement != null)
{
Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
}
⑤ これで,Xamarin.Forms.WebView
のEval
メソッドやAndroid.Webkit.WebView
のEvaluateJavascript
メソッドにjavascript: jsBridge.invokeAction();
を渡すと,JSBridge
クラスのメソッドが呼び出されます(jsBridge
は④のAddJavascriptInterface
で登録した名前,invokeAction
は③のExport
に登録した名前にです.).JSBridge
クラスのメソッドの引数として文字列を渡せるようにしておけば,javascript: jsBridge.invokeAction(document.documentElement.outerHTML);
で,そのページのHTMLをC#に取得することができます.
⑥ Xamarin.Forms
のWebView
でこの取得したHTMLを使用するならば,WebViewRenderer
でJSBridge
からデータを受け取り,WebViewRenderer.Element
にそのデータを渡します.
動画の全画面表示
参考:https://github.com/mhaggag/XFAndroidFullScreenWebView
デフォルトだと,Twitterの動画で全画面表示を押しても全画面になりません(全画面表示になったという通知がXamarin.Forms
側までいかないためだと思っています).全画面表示になったかは,Xamarin.Forms.Platform.Android.FormsWebChromeClient
クラスを使えば検知できるので,ここからXamarin.Forms
側まで通知を伝えていきます.
① フルスクリーンに入った時のイベント用のEventArgs
を作ります.(自分のリポジトリではTLExtensionWebChromeClient.cs
にあります.)
public class EnterFullScreenRequestedEventArgs : EventArgs
{
public View View { get; }
public EnterFullScreenRequestedEventArgs(View view)
{
View = view;
}
}
② FormsWebChromeClient
クラスを作ります(自分のリポジトリではTLExtensionWebChromeClient
).全画面表示になるとOnShowCustomView
が,全画面表示が終了するとOnHideCustomView
が呼び出されます.
public class TLExtensionWebChromeClient : FormsWebChromeClient
{
public event EventHandler<EnterFullScreenRequestedEventArgs> EnterFullScreenRequested;
public event EventHandler ExitFullScreenRequested;
public override void OnHideCustomView()
{
ExitFullScreenRequested?.Invoke(this, EventArgs.Empty);
}
public override void OnShowCustomView(View view, ICustomViewCallback callback)
{
EnterFullScreenRequested?.Invoke(this, new EnterFullScreenRequestedEventArgs(view));
}
}
③ Xamarin.Forms
側のWebView
クラスに以下のフルスクリーンになった時,解除されたときのアクションを追加します.
public static readonly BindableProperty EnterFullScreenCmmandProperty =
BindableProperty.Create(
propertyName: "EnterFullScreenCommand",
returnType: typeof(ICommand),
declaringType: typeof(TLExtensionWebView),
defaultValue: new Command(async (view) => await DefaultEnterAsync((View)view))
);
public ICommand EnterFullScreenCommand
{
get => (ICommand)GetValue(EnterFullScreenCmmandProperty);
set => SetValue(EnterFullScreenCmmandProperty, value);
}
public static readonly BindableProperty ExitFullScreenCmmandProperty =
BindableProperty.Create(
propertyName: "ExitFullScreenCommand",
returnType: typeof(ICommand),
declaringType: typeof(TLExtensionWebView),
defaultValue: new Command(async (view) => await DefaultExitAsync())
);
public ICommand ExitFullScreenCommand
{
get => (ICommand)GetValue(ExitFullScreenCmmandProperty);
set => SetValue(ExitFullScreenCmmandProperty, value);
}
//フルスクリーンを実現するためのメソッド
private static async Task DefaultEnterAsync(View view)
{
var page = new ContentPage
{
Content = view
};
await Application.Current.MainPage.Navigation.PushModalAsync(page);
}
private static async Task DefaultExitAsync()
{
await Application.Current.MainPage.Navigation.PopModalAsync();
}
//フルスクリーンを実現するためのメソッド ここまで
④ WebViewRenderer
クラスに②で作ったFormsWebChromeClient
を登録します.FormsWebChromeClient
で全画面表示の通知が発生したら,③で実装したDefaultEnterAsync
やDefaultExitAsync
が呼び出されるように,処理を繋げます.(以下はフルスクリーン対応に必要な部分のみ抜き出しています.)
public class TLExtensionWebViewRenderer : WebViewRenderer
{
private TLExtensionWebView _webView;
private TLExtensionWebChromeClient webClient;
public TLExtensionWebViewRenderer(Context context) : base(context)
{
webClient = new TLExtensionWebChromeClient();
webClient.EnterFullScreenRequested += OnEnterFullScreenRequested;
webClient.ExitFullScreenRequested += OnExitFullScreenRequested;
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
Control.SetWebChromeClient(webClient);
}
_webView = (TLExtensionWebView)e.NewElement;
}
//フルスクリーンにするための追加メソッド
protected override FormsWebChromeClient GetFormsWebChromeClient()
{
return webClient;
}
private void OnEnterFullScreenRequested(object sender, EnterFullScreenRequestedEventArgs eventArgs)
{
if (_webView.EnterFullScreenCommand != null && _webView.EnterFullScreenCommand.CanExecute(null))
{
_webView.EnterFullScreenCommand.Execute(eventArgs.View.ToView());
}
}
private void OnExitFullScreenRequested(object sender, EventArgs eventArgs)
{
if (_webView.ExitFullScreenCommand != null && _webView.ExitFullScreenCommand.CanExecute(null))
{
_webView.ExitFullScreenCommand.Execute(null);
}
}
}
全画面表示になるときは
TLExtensionWebChromeClient.OnShowCustomView
→TLExtensionWebViewRenderer.OnEnterFullScreenRequsted
→TLExtensionWebView.DefaultEnterAsync
の順で
全画面表示が解除されるときは
TLExtensionWebChromeClient.OnHideCustomView
→TLExtensionWebViewRenderer.OnExitFullScreenRequsted
→TLExtensionWebView.DefaultExitAsync
の順で
呼び出され,Xamarin.Forms
側まで通知が行くようになり,全画面表示に対応できるようになります.
ソフトウェアキーボード表示時のWebViewの縮小
参考:https://qiita.com/amay077/items/6fcdec829a96bc604532
デフォルトのWebViewでTwitterのリプライをしようとすると,ソフトウェアキーボードが邪魔で自分が書いているツイートが見えなくなります.
ひとまず,Xamarin.Android
側のMainActivity
のOnCreate
メソッドに以下を入れれば解決します.
//appにはMainActivityでロードするAppクラスのオブジェクトを入れておく.
app.On<Xamarin.Forms.PlatformConfiguration.Android>().
UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Resize);
しかし,検索で文字を打ち,サジェストを選択したときに,キーボードは閉じるがWebViewが縮んだままになるケースがありました.この場合は後述するリロードに,いったんWebViewを非表示にしてから,表示する処理を加えると,1,2回のリロードでもとに戻るようになりました.(自分用のアプリなので,ひとまずこれで妥協.)
Twitter外のページに遷移したときなどに,Chromeなどの別ブラウザで開く
参考:https://itblogdsi.blog.fc2.com/blog-entry-171.html
WebView
で遷移する際に,Chromeなどの別ブラウザで開く際には,Xamarin.Android
側で別ブラウザを開くためのIntentを呼び出す必要があるみたいです.上記の記事が分かりやすいので,詳細はそちらをご覧ください.
Xamarin.Android
側のOnPageStarted
で,遷移開始時に処理をする場合は,OnPageStarted
は2回呼ばれることがあります.最初のOnPageStarted
でブラウザを開き,OnPageFinished
が呼ばれるまではブラウザを開かないようにする必要もあるようです.(参考:https://qiita.com/kikuchy/items/d0f5599a883e5e9350df)
また,ページ遷移する際にWebView
側もページ遷移しているので,WebView.GoBack()
で戻らないと,遷移先のページがWebView
に表示されたままになってしまいます.
WebViewへのjavascript実行
WebView.Eval
でjavascriptのスクリプトを入れれば,実行できますが,どうやら1つの文しか処理できないようです.複数文を処理する場合は,それらを1つの関数にまとめて一度WebView.Eval
で登録した後,2回目のWebView.Eval
でその関数を呼び出す必要があるみたいです.
また,HTMLが更新されると登録されていた関数も消えてしまうのと,Twitterではページ表示後もHTMLの更新が頻繁に発生するため,javascriptの関数を使用するタイミングで,その都度WebView.Eval
で関数を登録する必要があります.
WebViewのリロード
最近のXamarin.Forms.WebView
にはreload
メソッドがあるみたいですが,自分が使用していたXamarin.Forms
が古かったためか(バージョンは3.0.0.561731),reload
メソッドはありませんでした.
この場合は,WebView
と連携しているWebViewRenderer
のControl.Reload()
を呼ぶ必要があります.リロードボタンを作る際はXamarin.Forms.WebView
側にAction
型の変数を用意しておき,WebViewRenderer
でその変数にControl.Reload()
を呼ぶアクションを登録する必要があります.リロードボタンのイベントで登録したアクションを発動します.
//WebViewRednererのOnElementChanged内
//reloadActionはWebView側の変数
_webView = (TLExtensionWebView)e.NewElement;
_webView.reloadAction = new Action(() => { Control.Reload(); });
WebViewの履歴削除
REST APIの認証が完了してから,認証ページに戻らないように,WebViewの履歴を消す処理も実装しました.しかし,履歴削除のメソッドは現在のXamarin.Forms.WebView
にも内容です.リロードの時と同じように,WebView
と連携しているWebViewRenderer
のControl.ClearHistory()
を呼ぶ必要があります.
(おまけ)TabbedPageのスライド禁止
Twitterでは,画像表示時にその画像が複数枚ある場合は,横スライドで画像を切り替えます.Xamarin.Forms.TabbedPage
を使っている場合,TabbedPage
のタブ切替えのための横スライドと重なり,デフォルトだと正常に画像を切り替えることができなくなります.
TabbedPage
の横スライドによるタブ切替えを禁止するには,以下を呼び出します.
//thisTabbedPageはTabbedPageのオブジェクト
//falseをtrueにすると,スライドによるタブ切替えができるようになる.
thisTabbedPage.On<Xamarin.Forms.PlatformConfiguration.Android>().SetIsSwipePagingEnabled(false);
まとめ
Xamarin.Forms
で開発してても,WebView
に少し複雑な機能を追加しようとするとAndroid側の実装が必要になってしまうようです.Xamarin.Forms.WebView
と連携しているXamarin.Forms.Platform.Android.WebViewRenderer
から,Android固有の実装を追加する場合が多いので,webView
を用いるAndroidアプリケーションを開発する場合は,このWebViewRenderer
にお世話になる機会も多くなるかと思います.