1
0
Fork 0

For #7158: Improves browser to search animation (#8961)

master
Sawyer Blatz 2020-03-05 12:29:23 -08:00 committed by GitHub
parent 89a260ecd9
commit 3548e58c00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 155 additions and 28 deletions

View File

@ -41,7 +41,7 @@ class BrowserAnimator(
private val unwrappedSwipeRefresh: View? private val unwrappedSwipeRefresh: View?
get() = swipeRefresh.get() get() = swipeRefresh.get()
private val browserInValueAnimator = ValueAnimator.ofFloat(0f, END_ANIMATOR_VALUE).apply { private val browserZoomInValueAnimator = ValueAnimator.ofFloat(0f, END_ANIMATOR_VALUE).apply {
addUpdateListener { addUpdateListener {
unwrappedSwipeRefresh?.scaleX = STARTING_XY_SCALE + XY_SCALE_MULTIPLIER * it.animatedFraction unwrappedSwipeRefresh?.scaleX = STARTING_XY_SCALE + XY_SCALE_MULTIPLIER * it.animatedFraction
unwrappedSwipeRefresh?.scaleY = 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 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 * Triggers the *zoom in* browser animation to run if necessary (based on the SHOULD_ANIMATE_FLAG).
* removes the flag from the bundle so it is not played on future entries into the fragment. * Also removes the flag from the bundle so it is not played on future entries into the fragment.
*/ */
fun beginAnimateInIfNecessary() { fun beginAnimateInIfNecessary() {
val shouldAnimate = arguments.getBoolean(SHOULD_ANIMATE_FLAG, false) val shouldAnimate = arguments.getBoolean(SHOULD_ANIMATE_FLAG, false)
@ -68,7 +83,7 @@ class BrowserAnimator(
viewLifeCycleScope?.launch(Dispatchers.Main) { viewLifeCycleScope?.launch(Dispatchers.Main) {
delay(ANIMATION_DELAY) delay(ANIMATION_DELAY)
captureEngineViewAndDrawStatically { captureEngineViewAndDrawStatically {
browserInValueAnimator.start() browserZoomInValueAnimator.start()
} }
} }
} else { } else {
@ -79,13 +94,25 @@ class BrowserAnimator(
} }
/** /**
* Triggers the *out* browser animation to run. * Triggers the *zoom out* browser animation to run.
*/ */
fun beginAnimateOut() { fun beginAnimateOut() {
viewLifeCycleScope?.launch(Dispatchers.Main) { viewLifeCycleScope?.launch(Dispatchers.Main) {
captureEngineViewAndDrawStatically { captureEngineViewAndDrawStatically {
unwrappedEngineView?.asView()?.visibility = View.GONE 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()
} }
} }
} }

View File

@ -51,8 +51,6 @@ interface BrowserToolbarController {
fun handleTabCounterClick() fun handleTabCounterClick()
} }
typealias onComplete = () -> Unit
@Suppress("LargeClass") @Suppress("LargeClass")
class DefaultBrowserToolbarController( class DefaultBrowserToolbarController(
private val store: BrowserFragmentStore, private val store: BrowserFragmentStore,

View File

@ -7,6 +7,9 @@ package org.mozilla.fenix.search
import android.content.Context import android.content.Context
import androidx.navigation.NavController 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.search.SearchEngine
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.isUrl
@ -41,7 +44,9 @@ interface SearchController {
class DefaultSearchController( class DefaultSearchController(
private val context: Context, private val context: Context,
private val store: SearchFragmentStore, private val store: SearchFragmentStore,
private val navController: NavController private val navController: NavController,
private val lifecycleScope: CoroutineScope,
private val clearToolbarFocus: () -> Unit
) : SearchController { ) : SearchController {
override fun handleUrlCommitted(url: String) { override fun handleUrlCommitted(url: String) {
@ -75,7 +80,13 @@ class DefaultSearchController(
} }
override fun handleEditingCancelled() { 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) { override fun handleTextChanged(text: String) {
@ -164,4 +175,8 @@ class DefaultSearchController(
handleExistingSessionSelected(session) handleExistingSessionSelected(session)
} }
} }
companion object {
internal const val KEYBOARD_ANIMATION_DELAY = 5L
}
} }

View File

@ -18,6 +18,7 @@ import android.view.ViewStub
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
@ -112,9 +113,11 @@ class SearchFragment : Fragment(), UserInteractionHandler {
} }
val searchController = DefaultSearchController( val searchController = DefaultSearchController(
activity as HomeActivity, context = activity as HomeActivity,
searchStore, store = searchStore,
findNavController() navController = findNavController(),
lifecycleScope = viewLifecycleOwner.lifecycleScope,
clearToolbarFocus = ::clearToolbarFocus
) )
searchInteractor = SearchInteractor( searchInteractor = SearchInteractor(
@ -139,6 +142,10 @@ class SearchFragment : Fragment(), UserInteractionHandler {
return view return view
} }
private fun clearToolbarFocus() {
toolbarView.view.clearFocus()
}
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@SuppressWarnings("LongMethod") @SuppressWarnings("LongMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -286,6 +293,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
} }
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {
// Note: Actual navigation happens in `handleEditingCancelled` in SearchController
return when { return when {
qrFeature.onBackPressed() -> { qrFeature.onBackPressed() -> {
view?.search_scan_button?.isChecked = false view?.search_scan_button?.isChecked = false

View File

@ -5,4 +5,4 @@
<alpha xmlns:android="http://schemas.android.com/apk/res/android" <alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/decelerate_quad" android:interpolator="@android:interpolator/decelerate_quad"
android:fromAlpha="0.0" android:toAlpha="1.0" android:fromAlpha="0.0" android:toAlpha="1.0"
android:duration="150" /> android:duration="250" />

View File

@ -0,0 +1,14 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:interpolator="@android:interpolator/decelerate_quad"
android:fromAlpha="0" android:toAlpha="1"
android:duration="125" />
<translate
android:interpolator="@android:interpolator/decelerate_quad"
android:fromYDelta="7%" android:toYDelta="0%"
android:duration="125"/>
</set>

View File

@ -5,4 +5,4 @@
<alpha xmlns:android="http://schemas.android.com/apk/res/android" <alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/accelerate_quad" android:interpolator="@android:interpolator/accelerate_quad"
android:fromAlpha="1.0" android:toAlpha="0" android:fromAlpha="1.0" android:toAlpha="0"
android:duration="150" /> android:duration="250" />

View File

@ -0,0 +1,14 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:interpolator="@android:interpolator/accelerate_quad"
android:fromAlpha="1" android:toAlpha="0"
android:duration="125" />
<translate
android:interpolator="@android:interpolator/accelerate_quad"
android:fromYDelta="0%" android:toYDelta="7%"
android:duration="125"/>
</set>

View File

@ -177,12 +177,15 @@
<fragment <fragment
android:id="@+id/browserFragment" android:id="@+id/browserFragment"
android:name="org.mozilla.fenix.browser.BrowserFragment" android:name="org.mozilla.fenix.browser.BrowserFragment"
app:exitAnim="@anim/fade_out"
tools:layout="@layout/fragment_browser"> tools:layout="@layout/fragment_browser">
<action <action
android:id="@+id/action_browserFragment_to_homeFragment" android:id="@+id/action_browserFragment_to_homeFragment"
app:destination="@id/homeFragment" /> app:destination="@id/homeFragment" />
<action <action
android:id="@+id/action_browserFragment_to_searchFragment" android:id="@+id/action_browserFragment_to_searchFragment"
app:enterAnim="@anim/fade_in_up"
app:popExitAnim="@anim/fade_out_down"
app:destination="@id/searchFragment" /> app:destination="@id/searchFragment" />
<argument <argument
android:name="activeSessionId" android:name="activeSessionId"

View File

@ -4,12 +4,15 @@
package org.mozilla.fenix.search package org.mozilla.fenix.search
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
@ -29,10 +32,12 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.searchEngineManager import org.mozilla.fenix.ext.searchEngineManager
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.DefaultSearchController.Companion.KEYBOARD_ANIMATION_DELAY
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.whatsnew.clear import org.mozilla.fenix.whatsnew.clear
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@Config(application = TestApplication::class) @Config(application = TestApplication::class)
class DefaultSearchControllerTest { class DefaultSearchControllerTest {
@ -45,6 +50,8 @@ class DefaultSearchControllerTest {
private val searchEngine: SearchEngine = mockk(relaxed = true) private val searchEngine: SearchEngine = mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true) private val metrics: MetricController = mockk(relaxed = true)
private val sessionManager: SessionManager = mockk(relaxed = true) private val sessionManager: SessionManager = mockk(relaxed = true)
private val lifecycleScope: LifecycleCoroutineScope = mockk(relaxed = true)
private val clearToolbarFocus: (() -> Unit) = mockk(relaxed = true)
private lateinit var controller: DefaultSearchController private lateinit var controller: DefaultSearchController
private lateinit var settings: Settings private lateinit var settings: Settings
@ -60,7 +67,9 @@ class DefaultSearchControllerTest {
controller = DefaultSearchController( controller = DefaultSearchController(
context = context, context = context,
store = store, store = store,
navController = navController navController = navController,
lifecycleScope = lifecycleScope,
clearToolbarFocus = clearToolbarFocus
) )
settings = testContext.settings().apply { testContext.settings().clear() } settings = testContext.settings().apply { testContext.settings().clear() }
@ -84,10 +93,23 @@ class DefaultSearchControllerTest {
} }
@Test @Test
fun handleEditingCancelled() { fun handleEditingCancelled() = runBlockingTest {
controller = DefaultSearchController(
context = context,
store = store,
navController = navController,
lifecycleScope = this,
clearToolbarFocus = clearToolbarFocus
)
controller.handleEditingCancelled() controller.handleEditingCancelled()
verify { navController.navigateUp() } advanceTimeBy(KEYBOARD_ANIMATION_DELAY)
verify {
clearToolbarFocus()
navController.popBackStack()
}
} }
@Test @Test

View File

@ -5,6 +5,7 @@
package org.mozilla.fenix.search package org.mozilla.fenix.search
import android.content.Context import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
@ -14,6 +15,8 @@ import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkObject import io.mockk.mockkObject
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.SearchEngineManager import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
@ -33,9 +36,14 @@ import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.whatsnew.clear import org.mozilla.fenix.whatsnew.clear
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@Config(application = TestApplication::class) @Config(application = TestApplication::class)
class SearchInteractorTest { class SearchInteractorTest {
private val lifecycleScope: LifecycleCoroutineScope = mockk(relaxed = true)
private val clearToolbarFocus = { }
@Test @Test
fun onUrlCommitted() { fun onUrlCommitted() {
val context: HomeActivity = mockk(relaxed = true) val context: HomeActivity = mockk(relaxed = true)
@ -64,7 +72,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController( val searchController: SearchController = DefaultSearchController(
context, context,
store, store,
mockk() mockk(),
lifecycleScope,
clearToolbarFocus
) )
val interactor = SearchInteractor(searchController) val interactor = SearchInteractor(searchController)
@ -81,7 +91,7 @@ class SearchInteractorTest {
} }
@Test @Test
fun onEditingCanceled() { fun onEditingCanceled() = runBlockingTest {
val navController: NavController = mockk(relaxed = true) val navController: NavController = mockk(relaxed = true)
val store: SearchFragmentStore = mockk(relaxed = true) val store: SearchFragmentStore = mockk(relaxed = true)
@ -90,14 +100,18 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController( val searchController: SearchController = DefaultSearchController(
mockk(), mockk(),
store, store,
navController navController,
this,
clearToolbarFocus
) )
val interactor = SearchInteractor(searchController) val interactor = SearchInteractor(searchController)
interactor.onEditingCanceled() interactor.onEditingCanceled()
advanceTimeBy(DefaultSearchController.KEYBOARD_ANIMATION_DELAY)
verify { verify {
navController.navigateUp() clearToolbarFocus()
navController.popBackStack()
} }
} }
@ -115,7 +129,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController( val searchController: SearchController = DefaultSearchController(
context, context,
store, store,
mockk() mockk(),
lifecycleScope,
clearToolbarFocus
) )
val interactor = SearchInteractor(searchController) val interactor = SearchInteractor(searchController)
@ -149,7 +165,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController( val searchController: SearchController = DefaultSearchController(
context, context,
store, store,
mockk() mockk(),
lifecycleScope,
clearToolbarFocus
) )
val interactor = SearchInteractor(searchController) val interactor = SearchInteractor(searchController)
@ -192,7 +210,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController( val searchController: SearchController = DefaultSearchController(
context, context,
store, store,
mockk() mockk(),
lifecycleScope,
clearToolbarFocus
) )
val interactor = SearchInteractor(searchController) val interactor = SearchInteractor(searchController)
@ -223,7 +243,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController( val searchController: SearchController = DefaultSearchController(
context, context,
store, store,
mockk() mockk(),
lifecycleScope,
clearToolbarFocus
) )
val interactor = SearchInteractor(searchController) val interactor = SearchInteractor(searchController)
val searchEngine: SearchEngine = mockk(relaxed = true) val searchEngine: SearchEngine = mockk(relaxed = true)
@ -253,7 +275,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController( val searchController: SearchController = DefaultSearchController(
mockk(), mockk(),
store, store,
navController navController,
lifecycleScope,
clearToolbarFocus
) )
val interactor = SearchInteractor(searchController) val interactor = SearchInteractor(searchController)
@ -280,7 +304,9 @@ class SearchInteractorTest {
val searchController: SearchController = DefaultSearchController( val searchController: SearchController = DefaultSearchController(
context, context,
store, store,
navController navController,
lifecycleScope,
clearToolbarFocus
) )
val interactor = SearchInteractor(searchController) val interactor = SearchInteractor(searchController)
val session = Session("http://mozilla.org", false) val session = Session("http://mozilla.org", false)