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)