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?
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()
}
}
}

View File

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

View File

@ -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
}
}

View File

@ -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

View File

@ -5,4 +5,4 @@
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/decelerate_quad"
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"
android:interpolator="@android:interpolator/accelerate_quad"
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
android:id="@+id/browserFragment"
android:name="org.mozilla.fenix.browser.BrowserFragment"
app:exitAnim="@anim/fade_out"
tools:layout="@layout/fragment_browser">
<action
android:id="@+id/action_browserFragment_to_homeFragment"
app:destination="@id/homeFragment" />
<action
android:id="@+id/action_browserFragment_to_searchFragment"
app:enterAnim="@anim/fade_in_up"
app:popExitAnim="@anim/fade_out_down"
app:destination="@id/searchFragment" />
<argument
android:name="activeSessionId"

View File

@ -4,12 +4,15 @@
package org.mozilla.fenix.search
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
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.searchEngineManager
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.whatsnew.clear
import org.robolectric.annotation.Config
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@Config(application = TestApplication::class)
class DefaultSearchControllerTest {
@ -45,6 +50,8 @@ class DefaultSearchControllerTest {
private val searchEngine: SearchEngine = mockk(relaxed = true)
private val metrics: MetricController = 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 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

View File

@ -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)