[Android] WebViewのtarget=”_blank”リンクを外部ブラウザで開く方法

AndroidアプリでWebViewを使っていて、target="_blank"が付いたリンクをタップしても何も起きない……なんてことありませんか?実はこれ、WebViewあるあるなんです。

デフォルトのWebViewには新しいウィンドウを開く機能がないので、target="_blank"のリンクがそのまま無視されてしまうんですね。

この記事では、target="_blank"のリンクを検知して、Chromeなどの外部ブラウザで開く方法をKotlinのコード付きで紹介していきます。

ちなみに、逆に「アプリ内の別WebViewで開きたい」という場合は、こちらの記事が参考になります → WebViewでターゲットブランクリンクをアプリ内で開く方法

スポンサーリンク

前提条件

  • Android Studio(最新安定版推奨)
  • minSdk 21 以上
  • Kotlin

そもそも、なぜ target=”_blank” が無視されるの?

ブラウザ(ChromeやSafariなど)では、target="_blank"をクリックすると新しいタブが開きますよね。でもWebViewにはタブという概念がないので、「新しいウィンドウで開いて」と言われても困ってしまうわけです。

結果として、リンクをタップしても何も起こらないという挙動になります。

これを解決するには、WebViewの設定で「マルチウィンドウ対応するよ」と宣言し、WebChromeClientonCreateWindowというコールバックで新しいウィンドウのリクエストを自分でハンドリングしてあげる必要があります。

実装方法

やることはシンプルで、大きく2つです。

  1. WebViewの設定でsetSupportMultipleWindows(true)を有効にする
  2. WebChromeClient#onCreateWindowで新しいウィンドウのリクエストを受け取って、URLを外部ブラウザに投げる

順番に見ていきましょう。

ステップ1:WebViewの基本セットアップ

まずはWebViewの設定から。ポイントはsetSupportMultipleWindows(true)です。

// WebViewの基本設定
webView.settings.apply {
    javaScriptEnabled = true
    setSupportMultipleWindows(true) // ← これが重要!
}

この設定がないと、そもそもonCreateWindowが呼ばれないので注意してください。逆に言うと、trueにしただけでは何も変わらないので、次のステップでコールバックを実装していきます。

ステップ2:WebChromeClientで外部ブラウザを起動する

onCreateWindowの中で、一時的なWebViewを使ってURLを取り出し、Intent.ACTION_VIEWで外部ブラウザに飛ばします。

webView.webChromeClient = object : WebChromeClient() {
    override fun onCreateWindow(
        view: WebView?,
        isDialog: Boolean,
        isUserGesture: Boolean,
        resultMsg: Message?
    ): Boolean {
        // ユーザー操作によるリクエストだけ処理する(広告ポップアップ対策)
        if (!isUserGesture) return false
 
        // 一時的なWebViewを作ってURLを取得する
        val tempWebView = WebView(view!!.context)
        tempWebView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(
                view: WebView?,
                request: WebResourceRequest?
            ): Boolean {
                val url = request?.url?.toString() ?: return false
                // 外部ブラウザで開く
                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                view?.context?.startActivity(intent)
                return true
            }
        }
 
        // WebView.WebViewTransport経由でURLを受け渡す
        val transport = resultMsg?.obj as? WebView.WebViewTransport
        transport?.webView = tempWebView
        resultMsg?.sendToTarget()
        return true
    }
}

ちょっと長いですが、やっていることは意外とシンプルです。target="_blank"のリンクがタップされるとonCreateWindowが呼ばれるので、一時的なWebViewで遷移先のURLを受け取って、それをIntent.ACTION_VIEWで外部ブラウザに渡しているだけです。

ステップ3:全体のコードをまとめる

Activityの全体像はこんな感じになります。コピペしてそのまま動く状態にしておきました。

class WebViewActivity : AppCompatActivity() {
 
    private lateinit var webView: WebView
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_webview)
 
        webView = findViewById(R.id.webView)
        setupWebView()
        webView.loadUrl("https://example.com")
    }
 
    private fun setupWebView() {
        webView.settings.apply {
            javaScriptEnabled = true
            setSupportMultipleWindows(true)
        }
 
        // 通常のリンクはアプリ内WebViewで開く
        webView.webViewClient = WebViewClient()
 
        // target="_blank" は外部ブラウザで開く
        webView.webChromeClient = object : WebChromeClient() {
            override fun onCreateWindow(
                view: WebView?,
                isDialog: Boolean,
                isUserGesture: Boolean,
                resultMsg: Message?
            ): Boolean {
                if (!isUserGesture) return false
 
                val tempWebView = WebView(view!!.context)
                tempWebView.webViewClient = object : WebViewClient() {
                    override fun shouldOverrideUrlLoading(
                        view: WebView?,
                        request: WebResourceRequest?
                    ): Boolean {
                        val url = request?.url?.toString()
                            ?: return false
                        val intent = Intent(
                            Intent.ACTION_VIEW,
                            Uri.parse(url)
                        )
                        startActivity(intent)
                        return true
                    }
                }
 
                val transport = resultMsg?.obj
                    as? WebView.WebViewTransport
                transport?.webView = tempWebView
                resultMsg?.sendToTarget()
                return true
            }
        }
    }
 
    override fun onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack()
        } else {
            super.onBackPressed()
        }
    }
 
    override fun onDestroy() {
        webView.destroy()
        super.onDestroy()
    }
}

必要なimport文

念のため、import文もまとめておきますね。

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Message
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity

仕組みをもう少し詳しく

「なんで一時的なWebViewを作るの?直接URL取れないの?」と思った方もいるかもしれません。実装の流れをざっくり図にするとこんなイメージです。

  1. ユーザーがtarget="_blank"のリンクをタップ
  2. WebViewが「新しいウィンドウ開きたいです!」とonCreateWindowを呼ぶ
  3. resultMsgの中にWebView.WebViewTransportが入っていて、ここに「新しいウィンドウ用のWebView」をセットする
  4. セットした一時WebViewのshouldOverrideUrlLoadingが呼ばれて、遷移先URLが渡される
  5. そのURLをIntent.ACTION_VIEWで外部ブラウザにパス!

ちょっと回りくどいですが、WebView.WebViewTransportを介したこの方法がAndroidの公式な仕組みなので、素直にこのパターンに従うのがベストです。

isUserGesture チェックは忘れずに

onCreateWindowの引数にあるisUserGestureは、ユーザーが実際にタップして発生したリクエストかどうかを示すフラグです。

// ユーザー操作でない場合はブロックする
if (!isUserGesture) return false

これをチェックしないと、JavaScriptによる自動ポップアップ(広告とか)まで外部ブラウザで開いちゃうことになります。地味ですが大事な1行なので、忘れずに入れておきましょう。

応用:特定のドメインだけ外部ブラウザで開く

「自分のサイトのリンクはアプリ内で開いて、外部サイトへのリンクだけブラウザで開きたい」というケースもありますよね。そんなときは、shouldOverrideUrlLoadingの中でドメインを見て分岐させればOKです。

tempWebView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(
        view: WebView?,
        request: WebResourceRequest?
    ): Boolean {
        val url = request?.url?.toString() ?: return false
 
        // 自サイトのドメインならアプリ内WebViewで開く
        if (Uri.parse(url).host == "example.com") {
            webView.loadUrl(url)
            return true
        }
 
        // それ以外は外部ブラウザで開く
        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
        startActivity(intent)
        return true
    }
}

よくあるトラブルと対処法

target=”_blank” をタップしても何も起こらない

setSupportMultipleWindows(true)の設定を忘れていませんか?この設定がないとonCreateWindow自体が呼ばれないので、まずここを確認してみてください。

外部ブラウザが2回開いてしまう

WebViewClientWebChromeClientの両方でURLをハンドリングしていると起きることがあります。通常リンク(target="_blank"なし)はWebViewClient#shouldOverrideUrlLoadingtarget="_blank"付きはWebChromeClient#onCreateWindow、という具合に役割を分けてあげましょう。

ActivityNotFoundException が出る

外部ブラウザがインストールされていない端末(かなりレアですが)だとクラッシュする可能性があります。念のためtry-catchで囲んでおくと安心です。

try {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
    startActivity(intent)
} catch (e: ActivityNotFoundException) {
    // ブラウザが見つからない場合はアプリ内WebViewで開く
    webView.loadUrl(url)
}

まとめ

WebViewでtarget="_blank"のリンクを外部ブラウザで開くためのポイントは、この2つです。

  1. webView.settings.setSupportMultipleWindows(true)を設定する
  2. WebChromeClient#onCreateWindowで一時WebViewを介してURLを取得し、Intent.ACTION_VIEWで外部ブラウザに渡す

アプリ内で完結させるか、外部ブラウザに飛ばすかはプロジェクトの要件次第です。今回の記事とアプリ内で開く方法の記事を合わせて、状況に応じて使い分けてみてくださいね。

コメント

タイトルとURLをコピーしました