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にお世話になる機会も多くなるかと思います.