@@ -4,22 +4,41 @@ import android.annotation.TargetApi
44import android.app.Activity
55import android.content.Context
66import android.content.Intent
7+ import android.graphics.Bitmap
78import android.graphics.drawable.ColorDrawable
89import android.graphics.drawable.Drawable
910import android.net.Uri
1011import android.os.Build
1112import android.os.Bundle
1213import android.os.Message
14+ import android.util.TypedValue
1315import android.view.Menu
1416import android.view.MenuItem
15- import android.webkit.*
17+ import android.view.View
18+ import android.view.ViewGroup
19+ import android.webkit.ValueCallback
20+ import android.webkit.WebChromeClient
21+ import android.webkit.WebResourceRequest
22+ import android.webkit.WebView
1623import android.webkit.WebView.HitTestResult.SRC_ANCHOR_TYPE
1724import android.webkit.WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
18- import androidx.annotation.*
25+ import android.webkit.WebViewClient
26+ import android.widget.FrameLayout
27+ import androidx.annotation.ColorInt
28+ import androidx.annotation.DrawableRes
29+ import androidx.annotation.NonNull
30+ import androidx.annotation.Nullable
31+ import androidx.annotation.RequiresApi
1932import androidx.appcompat.app.ActionBar
2033import androidx.appcompat.app.AppCompatActivity
34+ import androidx.appcompat.widget.Toolbar
2135import androidx.core.content.res.ResourcesCompat
36+ import androidx.core.graphics.Insets
2237import androidx.core.graphics.drawable.DrawableCompat
38+ import androidx.core.util.TypedValueCompat
39+ import androidx.core.view.OnApplyWindowInsetsListener
40+ import androidx.core.view.ViewCompat
41+ import androidx.core.view.WindowInsetsCompat
2342
2443class WebKitWebViewActivity : AppCompatActivity () {
2544 companion object {
@@ -36,7 +55,11 @@ class WebKitWebViewActivity: AppCompatActivity() {
3655 }
3756 }
3857
58+ private lateinit var mRootFrameLayout: FrameLayout
59+ private lateinit var mToolbar: Toolbar
60+ private lateinit var mToolbarFrameLayout: FrameLayout
3961 private lateinit var mWebView: WebView
62+ private var mLastSeenInsets: Insets ? = null
4063 private var result: Uri ? = null
4164 private val handles = StartActivityHandles <ValueCallback <Array <Uri >>>()
4265
@@ -97,6 +120,17 @@ class WebKitWebViewActivity: AppCompatActivity() {
97120 private const val USERSCRIPT_USER_SELECT_NONE = " document.documentElement.style.webkitUserSelect='none';document.documentElement.style.userSelect='none';" ;
98121 }
99122
123+ override fun onPageStarted (view : WebView ? , url : String? , favicon : Bitmap ? ) {
124+ super .onPageStarted(view, url, favicon)
125+ // onPageStarted is not always called, but when it is called, it is called before
126+ // onPageFinished.
127+ // Therefore, we put the edge-to-edge handling here hoping that
128+ // the safe area insets can be set as soon as possible.
129+ view?.evaluateJavascript(USERSCRIPT_USER_SELECT_NONE , null );
130+ activity.handleNonEdgeToEdge()
131+ activity.handleEdgeToEdge()
132+ }
133+
100134 override fun onPageFinished (view : WebView ? , url : String? ) {
101135 super .onPageFinished(view, url)
102136 // android.webkit.view does not have WKUserContentController that allows us to inject userscript.
@@ -106,6 +140,8 @@ class WebKitWebViewActivity: AppCompatActivity() {
106140 // The caveat is that the script is run in the main frame only.
107141 // But we do not actually use iframes so it does not matter.
108142 view?.evaluateJavascript(USERSCRIPT_USER_SELECT_NONE , null );
143+ activity.handleNonEdgeToEdge()
144+ activity.handleEdgeToEdge()
109145 }
110146
111147 @TargetApi(Build .VERSION_CODES .N )
@@ -205,17 +241,86 @@ class WebKitWebViewActivity: AppCompatActivity() {
205241 }
206242 }
207243
244+ private fun getActionBarSizeInDp (): Float {
245+ var actionBarSizeInDp = 44f
246+ var tv = TypedValue ()
247+ if (this .theme.resolveAttribute(android.R .attr.actionBarSize, tv, true )) {
248+ val actionBarSizeInPx = TypedValue .complexToDimensionPixelSize(tv.data, this .resources.displayMetrics)
249+ actionBarSizeInDp = TypedValueCompat .pxToDp(actionBarSizeInPx.toFloat(), this .resources.displayMetrics)
250+ }
251+ return actionBarSizeInDp
252+ }
253+
254+ private fun applyInsetsToWebView (safeAreaInsets : Insets ) {
255+ val actionBarSizeInDp = this .getActionBarSizeInDp()
256+ val displayMetrics = this .resources.displayMetrics
257+ val actionBarSizeInPx = TypedValueCompat .dpToPx(actionBarSizeInDp, displayMetrics)
258+ val top = TypedValueCompat .pxToDp(safeAreaInsets.top.toFloat() + actionBarSizeInPx, displayMetrics)
259+ val right = TypedValueCompat .pxToDp(safeAreaInsets.right.toFloat(), displayMetrics)
260+ val bottom = TypedValueCompat .pxToDp(safeAreaInsets.bottom.toFloat(), displayMetrics)
261+ val left = TypedValueCompat .pxToDp(safeAreaInsets.left.toFloat(), displayMetrics)
262+
263+ val safeAreaJs = """
264+ document.documentElement.style.setProperty('--safe-area-inset-top', '${top} px');
265+ document.documentElement.style.setProperty('--safe-area-inset-right', '${right} px');
266+ document.documentElement.style.setProperty('--safe-area-inset-bottom', '${bottom} px');
267+ document.documentElement.style.setProperty('--safe-area-inset-left', '${left} px');
268+ """
269+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .KITKAT ) {
270+ this .mWebView.evaluateJavascript(safeAreaJs, null )
271+ }
272+ }
273+
274+ private fun handleNonEdgeToEdge () {
275+ // In non edge-to-edge, the insets listener is not called.
276+ // So we have to apply the insets here.
277+ val insets = this .mLastSeenInsets ? : Insets .NONE
278+ this .applyInsetsToWebView(insets)
279+ }
280+
281+ private fun handleEdgeToEdge () {
282+ // In edge-to-edge, we ask the system to invoke the insets listener.
283+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .KITKAT_WATCH ) {
284+ this .mRootFrameLayout.requestApplyInsets()
285+ }
286+ }
287+
208288 override fun onCreate (savedInstanceState : Bundle ? ) {
209289 super .onCreate(savedInstanceState)
210290
291+ this .mRootFrameLayout = FrameLayout (this ).apply {
292+ layoutParams = ViewGroup .LayoutParams (
293+ ViewGroup .LayoutParams .MATCH_PARENT ,
294+ ViewGroup .LayoutParams .MATCH_PARENT
295+ )
296+ }
297+ this .mToolbarFrameLayout = FrameLayout (this ).apply {
298+ layoutParams = ViewGroup .LayoutParams (
299+ ViewGroup .LayoutParams .MATCH_PARENT ,
300+ ViewGroup .LayoutParams .WRAP_CONTENT
301+ )
302+ }
303+
304+ val actionBarSizeInDp = this .getActionBarSizeInDp()
305+
306+ this .mToolbar = Toolbar (this ).apply {
307+ layoutParams = FrameLayout .LayoutParams (
308+ ViewGroup .LayoutParams .MATCH_PARENT ,
309+ TypedValueCompat .dpToPx(actionBarSizeInDp, this .context.resources.displayMetrics).toInt()
310+ )
311+ }
312+ setSupportActionBar(this .mToolbar)
313+
211314 val options = this .getOptions()
212315
213316 // Do not show title.
214317 supportActionBar?.setDisplayShowTitleEnabled(false )
215318
216319 // Configure navigation bar background color.
217320 options.actionBarBackgroundColor?.let {
218- supportActionBar?.setBackgroundDrawable(ColorDrawable (it))
321+ val colorDrawable = ColorDrawable (it)
322+ supportActionBar?.setBackgroundDrawable(colorDrawable)
323+ this .mToolbarFrameLayout.setBackgroundDrawable(colorDrawable)
219324 }
220325
221326 // Show back button.
@@ -230,16 +335,56 @@ class WebKitWebViewActivity: AppCompatActivity() {
230335 supportActionBar?.setHomeAsUpIndicator(backButtonDrawable)
231336
232337 // Configure web view.
233- this .mWebView = WebView (this )
338+ this .mWebView = WebView (this ).apply {
339+ layoutParams = FrameLayout .LayoutParams (
340+ ViewGroup .LayoutParams .MATCH_PARENT ,
341+ ViewGroup .LayoutParams .MATCH_PARENT
342+ )
343+ }
234344 this .mWebView.settings.setSupportMultipleWindows(true )
235345 this .mWebView.settings.domStorageEnabled = true
236346 this .mWebView.settings.javaScriptEnabled = true
237- this .setContentView(this .mWebView)
238347 this .mWebView.setWebViewClient(MyWebViewClient (this ))
239348 this .mWebView.setWebChromeClient(MyWebChromeClient (this ))
240349
241- if (savedInstanceState == null ) {
242- this .mWebView.loadUrl(options.url.toString())
350+ this .mRootFrameLayout.addView(this .mWebView)
351+ this .mRootFrameLayout.addView(this .mToolbarFrameLayout)
352+ this .mToolbarFrameLayout.addView(this .mToolbar)
353+ this .setContentView(this .mRootFrameLayout)
354+
355+ ViewCompat .setOnApplyWindowInsetsListener(this .mRootFrameLayout, object : OnApplyWindowInsetsListener {
356+ override fun onApplyWindowInsets (
357+ v : View ,
358+ insets : WindowInsetsCompat
359+ ): WindowInsetsCompat {
360+ val safeAreaInsets = insets.getInsets(
361+ WindowInsetsCompat .Type .systemBars() or
362+ WindowInsetsCompat .Type .displayCutout() or
363+ WindowInsetsCompat .Type .ime()
364+ )
365+ this @WebKitWebViewActivity.mLastSeenInsets = safeAreaInsets
366+
367+ (mToolbar.layoutParams as ViewGroup .MarginLayoutParams ).setMargins(
368+ safeAreaInsets.left,
369+ safeAreaInsets.top,
370+ safeAreaInsets.right,
371+ 0
372+ )
373+ this @WebKitWebViewActivity.applyInsetsToWebView(safeAreaInsets)
374+
375+ return WindowInsetsCompat .CONSUMED
376+ }
377+ })
378+ this .mRootFrameLayout.post {
379+ // We want the content view to draw at least once before loading the URL.
380+ //
381+ // In non edge-to-edge, the insets listener is never called so mLastSeenInsets is null.
382+ //
383+ // In edge-to-edge, the insets listener will be called at least once in the first draw,
384+ // so by the time onPageStart / onPageFinished is called, mLastSeenInsets is not null.
385+ if (savedInstanceState == null ) {
386+ this .mWebView.loadUrl(options.url.toString())
387+ }
243388 }
244389 }
245390
0 commit comments