Skip to content

Commit 51c09b8

Browse files
authored
Support both non-edge-to-edge and edge-to-edge #85
ref DEV-3001
2 parents f45a14e + 122b1bf commit 51c09b8

File tree

4 files changed

+156
-8
lines changed

4 files changed

+156
-8
lines changed

android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ android {
4848

4949
dependencies {
5050
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
51+
implementation "androidx.core:core-ktx:1.12.0"
5152
implementation "androidx.browser:browser:1.4.0"
5253
// NOTE(backup): Please search NOTE(backup) before you update security-crypto or tink-android.
5354
implementation "androidx.security:security-crypto:1.1.0-alpha06"

android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,41 @@ import android.annotation.TargetApi
44
import android.app.Activity
55
import android.content.Context
66
import android.content.Intent
7+
import android.graphics.Bitmap
78
import android.graphics.drawable.ColorDrawable
89
import android.graphics.drawable.Drawable
910
import android.net.Uri
1011
import android.os.Build
1112
import android.os.Bundle
1213
import android.os.Message
14+
import android.util.TypedValue
1315
import android.view.Menu
1416
import 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
1623
import android.webkit.WebView.HitTestResult.SRC_ANCHOR_TYPE
1724
import 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
1932
import androidx.appcompat.app.ActionBar
2033
import androidx.appcompat.app.AppCompatActivity
34+
import androidx.appcompat.widget.Toolbar
2135
import androidx.core.content.res.ResourcesCompat
36+
import androidx.core.graphics.Insets
2237
import 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

2443
class 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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<resources>
2-
<style name="AuthgearTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
2+
<style name="AuthgearTheme" parent="Theme.AppCompat.Light.NoActionBar">
33
</style>
44
</resources>

example/lib/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,8 @@ class _MyAppState extends State<MyApp> {
925925
),
926926
android: WebKitWebViewUIImplementationOptionsAndroid(
927927
wechatRedirectURI: wechatRedirectURI,
928+
actionBarBackgroundColor: 0x00ffffff,
929+
actionBarButtonTintColor: 0xff000000,
928930
),
929931
sendWechatAuthRequest: _sendWechatAuthRequest,
930932
),

0 commit comments

Comments
 (0)