diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserAnimator.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserAnimator.kt
index 74c4224db..8f465d767 100644
--- a/app/src/main/java/org/mozilla/fenix/browser/BrowserAnimator.kt
+++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserAnimator.kt
@@ -41,7 +41,7 @@ class BrowserAnimator(
private val unwrappedSwipeRefresh: View?
get() = swipeRefresh.get()
- private val browserInValueAnimator = ValueAnimator.ofFloat(0f, END_ANIMATOR_VALUE).apply {
+ private val browserZoomInValueAnimator = ValueAnimator.ofFloat(0f, END_ANIMATOR_VALUE).apply {
addUpdateListener {
unwrappedSwipeRefresh?.scaleX = STARTING_XY_SCALE + XY_SCALE_MULTIPLIER * it.animatedFraction
unwrappedSwipeRefresh?.scaleY = STARTING_XY_SCALE + XY_SCALE_MULTIPLIER * it.animatedFraction
@@ -58,9 +58,24 @@ class BrowserAnimator(
duration = ANIMATION_DURATION
}
+ private val browserFadeInValueAnimator = ValueAnimator.ofFloat(0f, END_ANIMATOR_VALUE).apply {
+ addUpdateListener {
+ unwrappedSwipeRefresh?.alpha = it.animatedFraction
+ }
+
+ doOnEnd {
+ unwrappedEngineView?.asView()?.visibility = View.VISIBLE
+ unwrappedSwipeRefresh?.background = null
+ arguments.putBoolean(SHOULD_ANIMATE_FLAG, false)
+ }
+
+ interpolator = DecelerateInterpolator()
+ duration = ANIMATION_DURATION
+ }
+
/**
- * Triggers the *in* browser animation to run if necessary (based on the SHOULD_ANIMATE_FLAG). Also
- * removes the flag from the bundle so it is not played on future entries into the fragment.
+ * Triggers the *zoom in* browser animation to run if necessary (based on the SHOULD_ANIMATE_FLAG).
+ * Also removes the flag from the bundle so it is not played on future entries into the fragment.
*/
fun beginAnimateInIfNecessary() {
val shouldAnimate = arguments.getBoolean(SHOULD_ANIMATE_FLAG, false)
@@ -68,7 +83,7 @@ class BrowserAnimator(
viewLifeCycleScope?.launch(Dispatchers.Main) {
delay(ANIMATION_DELAY)
captureEngineViewAndDrawStatically {
- browserInValueAnimator.start()
+ browserZoomInValueAnimator.start()
}
}
} else {
@@ -79,13 +94,25 @@ class BrowserAnimator(
}
/**
- * Triggers the *out* browser animation to run.
+ * Triggers the *zoom out* browser animation to run.
*/
fun beginAnimateOut() {
viewLifeCycleScope?.launch(Dispatchers.Main) {
captureEngineViewAndDrawStatically {
unwrappedEngineView?.asView()?.visibility = View.GONE
- browserInValueAnimator.reverse()
+ browserZoomInValueAnimator.reverse()
+ }
+ }
+ }
+
+ /**
+ * Triggers the *fade out* browser animation to run.
+ */
+ fun beginFadeOut() {
+ viewLifeCycleScope?.launch(Dispatchers.Main) {
+ captureEngineViewAndDrawStatically {
+ unwrappedEngineView?.asView()?.visibility = View.GONE
+ browserFadeInValueAnimator.reverse()
}
}
}
diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt
index 074418199..5c64120aa 100644
--- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt
+++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt
@@ -51,8 +51,6 @@ interface BrowserToolbarController {
fun handleTabCounterClick()
}
-typealias onComplete = () -> Unit
-
@Suppress("LargeClass")
class DefaultBrowserToolbarController(
private val store: BrowserFragmentStore,
diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt b/app/src/main/java/org/mozilla/fenix/search/SearchController.kt
index 3a21ff54a..b08d0a3c6 100644
--- a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt
+++ b/app/src/main/java/org/mozilla/fenix/search/SearchController.kt
@@ -7,6 +7,9 @@ package org.mozilla.fenix.search
import android.content.Context
import androidx.navigation.NavController
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
import mozilla.components.support.ktx.kotlin.isUrl
@@ -41,7 +44,9 @@ interface SearchController {
class DefaultSearchController(
private val context: Context,
private val store: SearchFragmentStore,
- private val navController: NavController
+ private val navController: NavController,
+ private val lifecycleScope: CoroutineScope,
+ private val clearToolbarFocus: () -> Unit
) : SearchController {
override fun handleUrlCommitted(url: String) {
@@ -75,7 +80,13 @@ class DefaultSearchController(
}
override fun handleEditingCancelled() {
- navController.navigateUp()
+ lifecycleScope.launch {
+ clearToolbarFocus()
+ // Delay a short amount so the keyboard begins animating away. This makes exit animation
+ // much smoother instead of having two separate parts (keyboard hides THEN animation)
+ delay(KEYBOARD_ANIMATION_DELAY)
+ navController.popBackStack()
+ }
}
override fun handleTextChanged(text: String) {
@@ -164,4 +175,8 @@ class DefaultSearchController(
handleExistingSessionSelected(session)
}
}
+
+ companion object {
+ internal const val KEYBOARD_ANIMATION_DELAY = 5L
+ }
}
diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt
index b4e0087f5..374e703fc 100644
--- a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt
+++ b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt
@@ -18,6 +18,7 @@ import android.view.ViewStub
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.transition.TransitionInflater
@@ -112,9 +113,11 @@ class SearchFragment : Fragment(), UserInteractionHandler {
}
val searchController = DefaultSearchController(
- activity as HomeActivity,
- searchStore,
- findNavController()
+ context = activity as HomeActivity,
+ store = searchStore,
+ navController = findNavController(),
+ lifecycleScope = viewLifecycleOwner.lifecycleScope,
+ clearToolbarFocus = ::clearToolbarFocus
)
searchInteractor = SearchInteractor(
@@ -139,6 +142,10 @@ class SearchFragment : Fragment(), UserInteractionHandler {
return view
}
+ private fun clearToolbarFocus() {
+ toolbarView.view.clearFocus()
+ }
+
@ExperimentalCoroutinesApi
@SuppressWarnings("LongMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -286,6 +293,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
}
override fun onBackPressed(): Boolean {
+ // Note: Actual navigation happens in `handleEditingCancelled` in SearchController
return when {
qrFeature.onBackPressed() -> {
view?.search_scan_button?.isChecked = false
diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml
index aebd4461c..8024b31fd 100644
--- a/app/src/main/res/anim/fade_in.xml
+++ b/app/src/main/res/anim/fade_in.xml
@@ -5,4 +5,4 @@
\ No newline at end of file
+ android:duration="250" />
\ No newline at end of file
diff --git a/app/src/main/res/anim/fade_in_up.xml b/app/src/main/res/anim/fade_in_up.xml
new file mode 100644
index 000000000..d0d6b10b8
--- /dev/null
+++ b/app/src/main/res/anim/fade_in_up.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml
index 50a520082..4c00bc4a6 100644
--- a/app/src/main/res/anim/fade_out.xml
+++ b/app/src/main/res/anim/fade_out.xml
@@ -5,4 +5,4 @@
\ No newline at end of file
+ android:duration="250" />
\ No newline at end of file
diff --git a/app/src/main/res/anim/fade_out_down.xml b/app/src/main/res/anim/fade_out_down.xml
new file mode 100644
index 000000000..6448c37cc
--- /dev/null
+++ b/app/src/main/res/anim/fade_out_down.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index 153eee267..087c1d389 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -177,12 +177,15 @@
Unit) = mockk(relaxed = true)
private lateinit var controller: DefaultSearchController
private lateinit var settings: Settings
@@ -60,7 +67,9 @@ class DefaultSearchControllerTest {
controller = DefaultSearchController(
context = context,
store = store,
- navController = navController
+ navController = navController,
+ lifecycleScope = lifecycleScope,
+ clearToolbarFocus = clearToolbarFocus
)
settings = testContext.settings().apply { testContext.settings().clear() }
@@ -84,10 +93,23 @@ class DefaultSearchControllerTest {
}
@Test
- fun handleEditingCancelled() {
+ fun handleEditingCancelled() = runBlockingTest {
+ controller = DefaultSearchController(
+ context = context,
+ store = store,
+ navController = navController,
+ lifecycleScope = this,
+ clearToolbarFocus = clearToolbarFocus
+ )
+
controller.handleEditingCancelled()
- verify { navController.navigateUp() }
+ advanceTimeBy(KEYBOARD_ANIMATION_DELAY)
+
+ verify {
+ clearToolbarFocus()
+ navController.popBackStack()
+ }
}
@Test
diff --git a/app/src/test/java/org/mozilla/fenix/search/SearchInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/search/SearchInteractorTest.kt
index b56d90942..90549a48f 100644
--- a/app/src/test/java/org/mozilla/fenix/search/SearchInteractorTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/search/SearchInteractorTest.kt
@@ -5,6 +5,7 @@
package org.mozilla.fenix.search
import android.content.Context
+import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -14,6 +15,8 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.browser.session.Session
@@ -33,9 +36,14 @@ import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.whatsnew.clear
import org.robolectric.annotation.Config
+@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@Config(application = TestApplication::class)
class SearchInteractorTest {
+
+ private val lifecycleScope: LifecycleCoroutineScope = mockk(relaxed = true)
+ private val clearToolbarFocus = { }
+
@Test
fun onUrlCommitted() {
val context: HomeActivity = mockk(relaxed = true)
@@ -64,7 +72,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController(
context,
store,
- mockk()
+ mockk(),
+ lifecycleScope,
+ clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
@@ -81,7 +91,7 @@ class SearchInteractorTest {
}
@Test
- fun onEditingCanceled() {
+ fun onEditingCanceled() = runBlockingTest {
val navController: NavController = mockk(relaxed = true)
val store: SearchFragmentStore = mockk(relaxed = true)
@@ -90,14 +100,18 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController(
mockk(),
store,
- navController
+ navController,
+ this,
+ clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
interactor.onEditingCanceled()
+ advanceTimeBy(DefaultSearchController.KEYBOARD_ANIMATION_DELAY)
verify {
- navController.navigateUp()
+ clearToolbarFocus()
+ navController.popBackStack()
}
}
@@ -115,7 +129,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController(
context,
store,
- mockk()
+ mockk(),
+ lifecycleScope,
+ clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
@@ -149,7 +165,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController(
context,
store,
- mockk()
+ mockk(),
+ lifecycleScope,
+ clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
@@ -192,7 +210,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController(
context,
store,
- mockk()
+ mockk(),
+ lifecycleScope,
+ clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
@@ -223,7 +243,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController(
context,
store,
- mockk()
+ mockk(),
+ lifecycleScope,
+ clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
val searchEngine: SearchEngine = mockk(relaxed = true)
@@ -253,7 +275,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController(
mockk(),
store,
- navController
+ navController,
+ lifecycleScope,
+ clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
@@ -280,7 +304,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController(
context,
store,
- navController
+ navController,
+ lifecycleScope,
+ clearToolbarFocus
)
val interactor = SearchInteractor(searchController)
val session = Session("http://mozilla.org", false)