Skip to content

Commit

Permalink
fix(deck-options): add progress bar to stop flickering
Browse files Browse the repository at this point in the history
`deckOptionsReady` allows us to know exactly when the backend is ready,
instead of showing the WebView in PageWebViewClient.onPageFinished

Stop showing the WebView in `onPageFinished`, and wait for `deckOptionsReady`

A `CircularProgressIndicator` has been added as the wait is noticeable (~300ms)

Once the WebView is shown, hide the Progress Bar

promiseToWaitFor and `:page-fully-loaded` are also removed
as unused, and these were primarily for DeckOptions

Fixes 14194 (better): deck options no longer flicker
  • Loading branch information
david-allison authored and mikehardy committed Jan 28, 2025
1 parent 988402a commit a08be69
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 51 deletions.
19 changes: 18 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/pages/DeckOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ package com.ichi2.anki.pages
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebView
import androidx.activity.OnBackPressedCallback
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import anki.collection.OpChanges
import anki.collection.Progress
Expand Down Expand Up @@ -136,6 +138,15 @@ class DeckOptions : PageFragment() {
}
}

/** @see onWebViewReady */
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
pageLoadingIndicator.isVisible = true
super.onViewCreated(view, savedInstanceState)
}

override fun onWebViewCreated(webView: WebView) {
// addJavascriptInterface needs to happen before loadUrl
webView.addJavascriptInterface(ModalJavaScriptInterfaceListener(), "ankidroid")
Expand All @@ -152,6 +163,11 @@ class DeckOptions : PageFragment() {
return object : PageWebViewClient() {
private val ankiManualHostRegex = Regex("^docs\\.ankiweb\\.net\$")

/** @see onWebViewReady */
override fun onShowWebView(webView: WebView) {
// no-op: handled in onVebViewReady
}

override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?,
Expand Down Expand Up @@ -205,7 +221,8 @@ class DeckOptions : PageFragment() {

fun onWebViewReady() {
Timber.d("WebView ready to receive input")
// TODO: handle this
webView.isVisible = true
pageLoadingIndicator.isVisible = false
}

companion object {
Expand Down
10 changes: 10 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.annotation.LayoutRes
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.ichi2.anki.R
import com.ichi2.anki.SingleFragmentActivity
import com.ichi2.themes.Themes
Expand All @@ -46,6 +47,15 @@ open class PageFragment(
lateinit var webView: WebView
private val server = AnkiServer(this).also { it.start() }

/**
* A loading indicator for the page. May be shown before the WebView is loaded to
* stop flickering
*
* @exception IllegalStateException if accessed before [onViewCreated]
*/
val pageLoadingIndicator: CircularProgressIndicator
get() = requireView().findViewById(R.id.page_loading)

/**
* Override this to set a custom [WebViewClient] to the page.
* This is called in [onViewCreated].
Expand Down
61 changes: 14 additions & 47 deletions AnkiDroid/src/main/java/com/ichi2/anki/pages/PageWebViewClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ import java.io.IOException
* Base WebViewClient to be used on [PageFragment]
*/
open class PageWebViewClient : WebViewClient() {
/** Wait for the provided promise to complete before showing the WebView */
open val promiseToWaitFor: String? = null

val onPageFinishedCallbacks: MutableList<OnPageFinishedCallback> = mutableListOf()

override fun shouldInterceptRequest(
Expand Down Expand Up @@ -87,57 +84,27 @@ open class PageWebViewClient : WebViewClient() {
}
}

/**
* Shows the WebView after the page is loaded
*
* This may be overridden if additional 'screen ready' logic is provided by the backend
* @see DeckOptions
*/
open fun onShowWebView(webView: WebView) {
Timber.v("Displaying WebView")
webView.isVisible = true
}

override fun onPageFinished(
view: WebView?,
url: String?,
) {
super.onPageFinished(view, url)
if (view == null) return
onPageFinishedCallbacks.map { callback -> callback.onPageFinished(view) }
if (promiseToWaitFor == null) {
/** [PageFragment.webView] is invisible by default to avoid flashes while
* the page is loaded, and can be made visible again after it finishes loading */
Timber.v("displaying WebView")
view.isVisible = true
} else {
view.evaluateJavascript(
"""$promiseToWaitFor.then(() => { console.log("page-fully-loaded:"); window.location.href = "page-fully-loaded:" } )""",
) {
Timber.v("waiting for '$promiseToWaitFor' before displaying WebView")
}
}
}

@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") // still needed for API 23
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
if (view == null || url == null) return super.shouldOverrideUrlLoading(view, url)
if (handleUrl(view, url)) {
return true
}
return super.shouldOverrideUrlLoading(view, url)
}

override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?,
): Boolean {
if (view == null || request == null) return super.shouldOverrideUrlLoading(view, request)
if (handleUrl(view, request.url.toString())) {
return true
}
return super.shouldOverrideUrlLoading(view, request)
}

private fun handleUrl(
view: WebView,
url: String,
): Boolean {
if (url == "page-fully-loaded:") {
Timber.v("displaying WebView after '$promiseToWaitFor' executed")
view.isVisible = true
return true
}
return false
/** [PageFragment.webView] is invisible by default to avoid flashes while
* the page is loaded, and can be made visible again after it finishes loading */
onShowWebView(view)
}
}

Expand Down
29 changes: 26 additions & 3 deletions AnkiDroid/src/main/res/layout/page_fragment.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
Expand All @@ -15,10 +16,32 @@
app:navigationIcon="?attr/homeAsUpIndicator"
/>

<WebView
android:id="@+id/webview"

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"/>
>
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/page_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />

<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

</LinearLayout>

0 comments on commit a08be69

Please sign in to comment.