1
0
Fork 0

For #167: Improves home to browser animation

master
Sawyer Blatz 2020-02-27 13:29:47 -08:00 committed by Emily Kager
parent a3d40b7045
commit e96732604b
17 changed files with 269 additions and 143 deletions

View File

@ -321,7 +321,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
BrowserDirection.FromGlobal -> BrowserDirection.FromGlobal ->
NavGraphDirections.actionGlobalBrowser(customTabSessionId) NavGraphDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHome -> BrowserDirection.FromHome ->
HomeFragmentDirections.actionHomeFragmentToBrowserFragment(customTabSessionId) HomeFragmentDirections.actionHomeFragmentToBrowserFragment(customTabSessionId, true)
BrowserDirection.FromSearch -> BrowserDirection.FromSearch ->
SearchFragmentDirections.actionSearchFragmentToBrowserFragment(customTabSessionId) SearchFragmentDirections.actionSearchFragmentToBrowserFragment(customTabSessionId)
BrowserDirection.FromSettings -> BrowserDirection.FromSettings ->

View File

@ -6,8 +6,6 @@ package org.mozilla.fenix.browser
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
@ -15,11 +13,9 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -89,6 +85,7 @@ import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import java.lang.ref.WeakReference
/** /**
* Base fragment extended by [BrowserFragment]. * Base fragment extended by [BrowserFragment].
@ -98,6 +95,7 @@ import org.mozilla.fenix.theme.ThemeManager
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer { abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer {
protected lateinit var browserFragmentStore: BrowserFragmentStore protected lateinit var browserFragmentStore: BrowserFragmentStore
private lateinit var browserAnimator: BrowserAnimator
private var _browserInteractor: BrowserToolbarViewInteractor? = null private var _browserInteractor: BrowserToolbarViewInteractor? = null
protected val browserInteractor: BrowserToolbarViewInteractor protected val browserInteractor: BrowserToolbarViewInteractor
@ -164,6 +162,15 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
initializeEngineView(toolbarHeight) initializeEngineView(toolbarHeight)
browserAnimator = BrowserAnimator(
fragment = WeakReference(this),
engineView = WeakReference(engineView),
swipeRefresh = WeakReference(swipeRefresh),
arguments = arguments!!
).apply {
beginAnimationIfNecessary()
}
return getSessionById()?.also { session -> return getSessionById()?.also { session ->
val browserToolbarController = DefaultBrowserToolbarController( val browserToolbarController = DefaultBrowserToolbarController(
store = browserFragmentStore, store = browserFragmentStore,
@ -177,10 +184,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
browsingModeManager = (activity as HomeActivity).browsingModeManager, browsingModeManager = (activity as HomeActivity).browsingModeManager,
sessionManager = requireComponents.core.sessionManager, sessionManager = requireComponents.core.sessionManager,
findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } }, findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } },
browserLayout = view.browserLayout,
engineView = engineView, engineView = engineView,
swipeRefresh = swipeRefresh, swipeRefresh = swipeRefresh,
adjustBackgroundAndNavigate = ::adjustBackgroundAndNavigate, browserAnimator = browserAnimator,
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
getSupportUrl = { getSupportUrl = {
SupportUtils.getSumoURLForTopic( SupportUtils.getSumoURLForTopic(
@ -499,26 +505,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
view: View view: View
): List<ContextMenuCandidate> ): List<ContextMenuCandidate>
private fun adjustBackgroundAndNavigate(directions: NavDirections) {
context?.let {
viewLifecycleOwner.lifecycleScope.launch {
// isAdded check is necessary because of a bug in viewLifecycleOwner. See AC#3828
if (!this@BaseBrowserFragment.isAdded) return@launch
engineView.captureThumbnail { bitmap ->
if (!this@BaseBrowserFragment.isAdded) return@captureThumbnail
// If the bitmap is null, the best we can do to reduce the flash is set transparent
swipeRefresh.background = bitmap?.toDrawable(it.resources)
?: ColorDrawable(Color.TRANSPARENT)
engineView.asView().visibility = View.GONE
findNavController().nav(R.id.browserFragment, directions)
}
}
}
}
@CallSuper @CallSuper
override fun onSessionSelected(session: Session) { override fun onSessionSelected(session: Session) {
(activity as HomeActivity).updateThemeForSession(session) (activity as HomeActivity).updateThemeForSession(session)

View File

@ -0,0 +1,125 @@
/* 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/. */
package org.mozilla.fenix.browser
import android.animation.ValueAnimator
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.View
import android.view.animation.DecelerateInterpolator
import androidx.core.animation.doOnEnd
import androidx.core.graphics.drawable.toDrawable
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.EngineView
import java.lang.ref.WeakReference
/**
* Handles properly animating the browser engine based on `SHOULD_ANIMATE_FLAG` passed in through
* nav arguments.
*/
class BrowserAnimator(
private val fragment: WeakReference<Fragment>,
private val engineView: WeakReference<EngineView>,
private val swipeRefresh: WeakReference<View>,
private val arguments: Bundle
) {
private val viewLifeCycleScope: LifecycleCoroutineScope?
get() = fragment.get()?.viewLifecycleOwner?.lifecycleScope
private val unwrappedEngineView: EngineView?
get() = engineView.get()
private val unwrappedSwipeRefresh: View?
get() = swipeRefresh.get()
/**
* Triggers the 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 beginAnimationIfNecessary() {
val shouldAnimate = arguments.getBoolean(SHOULD_ANIMATE_FLAG, false)
if (shouldAnimate) {
viewLifeCycleScope?.launch(Dispatchers.Main) {
delay(ANIMATION_DELAY)
captureEngineViewAndDrawStatically {
animateBrowserEngine(unwrappedSwipeRefresh)
}
}
} else {
unwrappedSwipeRefresh?.alpha = 1f
unwrappedEngineView?.asView()?.visibility = View.VISIBLE
unwrappedSwipeRefresh?.background = null
}
}
/**
* Details exactly how the browserEngine animation should look and plays it.
*/
private fun animateBrowserEngine(browserEngine: View?) {
val valueAnimator = ValueAnimator.ofFloat(0f, END_ANIMATOR_VALUE)
valueAnimator.addUpdateListener {
browserEngine?.scaleX = STARTING_XY_SCALE + XY_SCALE_MULTIPLIER * it.animatedFraction
browserEngine?.scaleY = STARTING_XY_SCALE + XY_SCALE_MULTIPLIER * it.animatedFraction
browserEngine?.alpha = it.animatedFraction
}
valueAnimator.doOnEnd {
unwrappedEngineView?.asView()?.visibility = View.VISIBLE
unwrappedSwipeRefresh?.background = null
arguments.putBoolean(SHOULD_ANIMATE_FLAG, false)
}
valueAnimator.interpolator = DecelerateInterpolator()
valueAnimator.duration = ANIMATION_DURATION
valueAnimator.start()
}
/**
* Makes the swipeRefresh background a screenshot of the engineView in its current state.
* This allows us to "animate" the engineView.
*/
fun captureEngineViewAndDrawStatically(onComplete: () -> Unit) {
unwrappedEngineView?.asView()?.context.let {
viewLifeCycleScope?.launch {
// isAdded check is necessary because of a bug in viewLifecycleOwner. See AC#3828
if (!fragment.isAdded()) { return@launch }
unwrappedEngineView?.captureThumbnail { bitmap ->
if (!fragment.isAdded()) { return@captureThumbnail }
unwrappedSwipeRefresh?.apply {
alpha = 0f
// If the bitmap is null, the best we can do to reduce the flash is set transparent
background = bitmap?.toDrawable(context.resources)
?: ColorDrawable(Color.TRANSPARENT)
}
onComplete()
}
}
}
}
private fun WeakReference<Fragment>.isAdded(): Boolean {
val unwrapped = get()
return unwrapped != null && unwrapped.isAdded
}
companion object {
private const val SHOULD_ANIMATE_FLAG = "shouldAnimate"
private const val ANIMATION_DELAY = 50L
private const val ANIMATION_DURATION = 115L
private const val END_ANIMATOR_VALUE = 500f
private const val XY_SCALE_MULTIPLIER = .05f
private const val STARTING_XY_SCALE = .95f
}
}

View File

@ -64,7 +64,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val view = super.onCreateView(inflater, container, savedInstanceState) val view = super.onCreateView(inflater, container, savedInstanceState)
view.browserLayout.transitionName = "$TAB_ITEM_TRANSITION_NAME${getSessionById()?.id}"
startPostponedEnterTransition() startPostponedEnterTransition()
return view return view
} }

View File

@ -6,16 +6,8 @@ package org.mozilla.fenix.components.toolbar
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.core.graphics.drawable.toDrawable
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import androidx.navigation.fragment.FragmentNavigator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -30,6 +22,7 @@ import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator
import org.mozilla.fenix.browser.BrowserFragment import org.mozilla.fenix.browser.BrowserFragment
import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
@ -57,6 +50,8 @@ 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,
@ -66,9 +61,8 @@ class DefaultBrowserToolbarController(
private val browsingModeManager: BrowsingModeManager, private val browsingModeManager: BrowsingModeManager,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val findInPageLauncher: () -> Unit, private val findInPageLauncher: () -> Unit,
private val browserLayout: ViewGroup,
private val engineView: EngineView, private val engineView: EngineView,
private val adjustBackgroundAndNavigate: (NavDirections) -> Unit, private val browserAnimator: BrowserAnimator,
private val swipeRefresh: SwipeRefreshLayout, private val swipeRefresh: SwipeRefreshLayout,
private val customTabSession: Session?, private val customTabSession: Session?,
private val getSupportUrl: () -> String, private val getSupportUrl: () -> String,
@ -89,12 +83,14 @@ class DefaultBrowserToolbarController(
internal var ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO) internal var ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
override fun handleToolbarPaste(text: String) { override fun handleToolbarPaste(text: String) {
adjustBackgroundAndNavigate.invoke( browserAnimator.captureEngineViewAndDrawStatically {
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment( val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
sessionId = currentSession?.id, sessionId = currentSession?.id,
pastedText = text pastedText = text
) )
)
navController.nav(R.id.browserFragment, directions)
}
} }
override fun handleToolbarPasteAndGo(text: String) { override fun handleToolbarPasteAndGo(text: String) {
@ -112,9 +108,14 @@ class DefaultBrowserToolbarController(
activity.components.analytics.metrics.track( activity.components.analytics.metrics.track(
Event.SearchBarTapped(Event.SearchBarTapped.Source.BROWSER) Event.SearchBarTapped(Event.SearchBarTapped.Source.BROWSER)
) )
adjustBackgroundAndNavigate.invoke(
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(currentSession?.id) browserAnimator.captureEngineViewAndDrawStatically {
) val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
currentSession?.id
)
navController.nav(R.id.browserFragment, directions)
}
} }
override fun handleTabCounterClick() { override fun handleTabCounterClick() {
@ -132,12 +133,14 @@ class DefaultBrowserToolbarController(
ToolbarMenu.Item.Forward -> sessionUseCases.goForward.invoke(currentSession) ToolbarMenu.Item.Forward -> sessionUseCases.goForward.invoke(currentSession)
ToolbarMenu.Item.Reload -> sessionUseCases.reload.invoke(currentSession) ToolbarMenu.Item.Reload -> sessionUseCases.reload.invoke(currentSession)
ToolbarMenu.Item.Stop -> sessionUseCases.stopLoading.invoke(currentSession) ToolbarMenu.Item.Stop -> sessionUseCases.stopLoading.invoke(currentSession)
ToolbarMenu.Item.Settings -> adjustBackgroundAndNavigate.invoke( ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically {
BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment() val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
) navController.nav(R.id.browserFragment, directions)
ToolbarMenu.Item.Library -> adjustBackgroundAndNavigate.invoke( }
BrowserFragmentDirections.actionBrowserFragmentToLibraryFragment() ToolbarMenu.Item.Library -> browserAnimator.captureEngineViewAndDrawStatically {
) val directions = BrowserFragmentDirections.actionBrowserFragmentToLibraryFragment()
navController.nav(R.id.browserFragment, directions)
}
is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke( is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke(
item.isChecked, item.isChecked,
currentSession currentSession
@ -297,28 +300,14 @@ class DefaultBrowserToolbarController(
} }
private fun animateTabAndNavigateHome() { private fun animateTabAndNavigateHome() {
// We need to dynamically add the options here because if you do it in XML it overwrites browserAnimator.captureEngineViewAndDrawStatically {
val options = NavOptions.Builder().setPopUpTo(R.id.nav_graph, false) if (!navController.popBackStack(R.id.homeFragment, false)) {
.setEnterAnim(R.anim.fade_in).build() val directions = BrowserFragmentDirections.actionBrowserFragmentToHomeFragment()
val extras = FragmentNavigator.Extras.Builder().addSharedElement( navController.nav(
browserLayout, R.id.browserFragment,
"${TAB_ITEM_TRANSITION_NAME}${currentSession?.id}" directions,
).build() null
engineView.captureThumbnail { bitmap -> )
scope.launch {
// If the bitmap is null, the best we can do to reduce the flash is set transparent
swipeRefresh.background = bitmap?.toDrawable(activity.resources)
?: ColorDrawable(Color.TRANSPARENT)
engineView.asView().visibility = View.GONE
if (!navController.popBackStack(R.id.homeFragment, false)) {
navController.nav(
R.id.browserFragment,
R.id.action_browserFragment_to_homeFragment,
null,
options,
extras
)
}
} }
} }
} }

View File

@ -38,7 +38,6 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import androidx.transition.TransitionInflater
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.*
@ -60,8 +59,8 @@ import mozilla.components.feature.media.ext.getSession
import mozilla.components.feature.media.state.MediaState import mozilla.components.feature.media.state.MediaState
import mozilla.components.feature.media.state.MediaStateMachine import mozilla.components.feature.media.state.MediaStateMachine
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.util.dpToPx
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -145,9 +144,6 @@ class HomeFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
postponeEnterTransition() postponeEnterTransition()
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
.setDuration(SHARED_TRANSITION_MS)
val sessionObserver = BrowserSessionsObserver(sessionManager, singleSessionObserver) { val sessionObserver = BrowserSessionsObserver(sessionManager, singleSessionObserver) {
emitSessionChanges() emitSessionChanges()
@ -270,6 +266,10 @@ class HomeFragment : Fragment() {
sessionControlView!!.view.layoutManager?.onRestoreInstanceState(parcelable) sessionControlView!!.view.layoutManager?.onRestoreInstanceState(parcelable)
} }
homeViewModel.layoutManagerState = null homeViewModel.layoutManagerState = null
// We have to delay so that the keyboard collapses and the view is resized before the
// animation from SearchFragment happens
delay(ANIMATION_DELAY)
} }
viewLifecycleOwner.lifecycleScope.launch(IO) { viewLifecycleOwner.lifecycleScope.launch(IO) {
@ -309,14 +309,7 @@ class HomeFragment : Fragment() {
view.toolbar_wrapper.setOnClickListener { view.toolbar_wrapper.setOnClickListener {
invokePendingDeleteJobs() invokePendingDeleteJobs()
hideOnboardingIfNeeded() hideOnboardingIfNeeded()
val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment( navigateToSearch()
sessionId = null
)
val extras =
FragmentNavigator.Extras.Builder()
.addSharedElement(toolbar_wrapper, "toolbar_wrapper_transition")
.build()
nav(R.id.homeFragment, directions, extras)
requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME)) requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME))
} }
@ -550,7 +543,12 @@ class HomeFragment : Fragment() {
val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment( val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment(
sessionId = null sessionId = null
) )
nav(R.id.homeFragment, directions)
val extras = FragmentNavigator.Extras.Builder()
.addSharedElement(toolbar_wrapper, "toolbar_wrapper_transition")
.build()
nav(R.id.homeFragment, directions, extras)
} }
private fun openSettingsScreen() { private fun openSettingsScreen() {
@ -897,12 +895,13 @@ class HomeFragment : Fragment() {
} }
companion object { companion object {
private const val ANIMATION_DELAY = 100L
private const val NON_TAB_ITEM_NUM = 3 private const val NON_TAB_ITEM_NUM = 3
private const val ANIM_SCROLL_DELAY = 100L private const val ANIM_SCROLL_DELAY = 100L
private const val ANIM_ON_SCREEN_DELAY = 200L private const val ANIM_ON_SCREEN_DELAY = 200L
private const val FADE_ANIM_DURATION = 150L private const val FADE_ANIM_DURATION = 150L
private const val ANIM_SNACKBAR_DELAY = 100L private const val ANIM_SNACKBAR_DELAY = 100L
private const val SHARED_TRANSITION_MS = 200L
private const val CFR_WIDTH_DIVIDER = 1.7 private const val CFR_WIDTH_DIVIDER = 1.7
private const val CFR_Y_OFFSET = -20 private const val CFR_Y_OFFSET = -20

View File

@ -6,7 +6,6 @@ package org.mozilla.fenix.home.sessioncontrol
import android.view.View import android.view.View
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.fragment.FragmentNavigator
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -341,27 +340,19 @@ class DefaultSessionControlController(
invokePendingDeleteJobs() invokePendingDeleteJobs()
val session = sessionManager.findSessionById(sessionId) val session = sessionManager.findSessionById(sessionId)
sessionManager.select(session!!) sessionManager.select(session!!)
val directions = HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null) activity.openToBrowser(BrowserDirection.FromHome)
val extras =
FragmentNavigator.Extras.Builder()
.addSharedElement(
tabView,
"$TAB_ITEM_TRANSITION_NAME$sessionId"
)
.build()
navController.nav(R.id.homeFragment, directions, extras)
} }
override fun handleSelectTopSite(url: String) { override fun handleSelectTopSite(url: String) {
invokePendingDeleteJobs()
metrics.track(Event.TopSiteOpenInNewTab) metrics.track(Event.TopSiteOpenInNewTab)
if (url == SupportUtils.POCKET_TRENDING_URL) { if (url == SupportUtils.POCKET_TRENDING_URL) { metrics.track(Event.PocketTopSiteClicked) }
metrics.track(Event.PocketTopSiteClicked) activity.components.useCases.tabsUseCases.addTab.invoke(
} url = url,
activity.components.useCases.tabsUseCases.addTab.invoke(url, true, true) selectTab = true,
navController.nav( startLoading = true
R.id.homeFragment,
HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)
) )
activity.openToBrowser(BrowserDirection.FromHome)
} }
override fun handleShareTabs() { override fun handleShareTabs() {

View File

@ -59,11 +59,11 @@ class SearchFragment : Fragment(), UserInteractionHandler {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
postponeEnterTransition() postponeEnterTransition()
sharedElementEnterTransition = sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move) TransitionInflater.from(context).inflateTransition(android.R.transition.move)
.setDuration( .setDuration(SHARED_TRANSITION_MS)
SHARED_TRANSITION_MS
)
requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea) requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea)
} }
@ -346,7 +346,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
} }
companion object { companion object {
private const val SHARED_TRANSITION_MS = 200L private const val SHARED_TRANSITION_MS = 250L
private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1 private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1
} }
} }

View File

@ -19,7 +19,7 @@ class FragmentPreDrawManager(
fragment.postponeEnterTransition() fragment.postponeEnterTransition()
} }
fun execute(code: () -> Unit) { fun execute(code: suspend () -> Unit) {
fragment.view?.doOnPreDraw { fragment.view?.doOnPreDraw {
fragment.viewLifecycleOwner.lifecycleScope.launch { fragment.viewLifecycleOwner.lifecycleScope.launch {
code() code()

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="250" /> android:duration="150" />

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="250" /> android:duration="150" />

View File

@ -0,0 +1,19 @@
<!-- 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/linear"
android:fromAlpha="1.0" android:toAlpha="0"
android:duration="125" />
<scale
android:interpolator="@android:interpolator/linear"
android:pivotX="50%"
android:pivotY="50%"
android:fromXScale="100%"
android:toXScale="113%"
android:fromYScale="100%"
android:toYScale="113%"
android:duration="125" />
</set>

View File

@ -15,6 +15,7 @@
android:background="@drawable/toolbar_background_top" android:background="@drawable/toolbar_background_top"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:transitionName="toolbar_wrapper_transition"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed" app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed"
app:browserToolbarClearColor="?primaryText" app:browserToolbarClearColor="?primaryText"

View File

@ -14,9 +14,11 @@
android:id="@+id/swipeRefresh" android:id="@+id/swipeRefresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:alpha="0"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<mozilla.components.concept.engine.EngineView <mozilla.components.concept.engine.EngineView
android:id="@+id/engineView" android:id="@+id/engineView"
android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -75,7 +75,7 @@
<action <action
android:id="@+id/action_homeFragment_to_browserFragment" android:id="@+id/action_homeFragment_to_browserFragment"
app:destination="@id/browserFragment" app:destination="@id/browserFragment"
app:exitAnim="@anim/fade_out" app:exitAnim="@anim/zoom_in_fade"
app:popEnterAnim="@anim/fade_in" /> app:popEnterAnim="@anim/fade_in" />
<action <action
android:id="@+id/action_homeFragment_to_libraryFragment" android:id="@+id/action_homeFragment_to_libraryFragment"
@ -187,6 +187,10 @@
android:name="activeSessionId" android:name="activeSessionId"
app:argType="string" app:argType="string"
app:nullable="true" /> app:nullable="true" />
<argument
android:name="shouldAnimate"
app:argType="boolean"
android:defaultValue="false" />
<action <action
android:id="@+id/action_browserFragment_to_settingsFragment" android:id="@+id/action_browserFragment_to_settingsFragment"
app:destination="@id/settingsFragment" /> app:destination="@id/settingsFragment" />

View File

@ -6,7 +6,6 @@ package org.mozilla.fenix.components.toolbar
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.ViewGroup
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
@ -16,12 +15,14 @@ import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.verify import io.mockk.verify
import io.mockk.verifyOrder import io.mockk.verifyOrder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.setMain import kotlinx.coroutines.test.setMain
@ -37,6 +38,7 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator
import org.mozilla.fenix.browser.BrowserFragment import org.mozilla.fenix.browser.BrowserFragment
import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
@ -59,7 +61,6 @@ import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
class DefaultBrowserToolbarControllerTest { class DefaultBrowserToolbarControllerTest {
private val mainThreadSurrogate = newSingleThreadContext("UI thread") private val mainThreadSurrogate = newSingleThreadContext("UI thread")
private var browserLayout: ViewGroup = mockk(relaxed = true)
private var swipeRefreshLayout: SwipeRefreshLayout = mockk(relaxed = true) private var swipeRefreshLayout: SwipeRefreshLayout = mockk(relaxed = true)
private var activity: HomeActivity = mockk(relaxed = true) private var activity: HomeActivity = mockk(relaxed = true)
private var analytics: Analytics = mockk(relaxed = true) private var analytics: Analytics = mockk(relaxed = true)
@ -75,7 +76,7 @@ class DefaultBrowserToolbarControllerTest {
private val searchUseCases: SearchUseCases = mockk(relaxed = true) private val searchUseCases: SearchUseCases = mockk(relaxed = true)
private val sessionUseCases: SessionUseCases = mockk(relaxed = true) private val sessionUseCases: SessionUseCases = mockk(relaxed = true)
private val scope: LifecycleCoroutineScope = mockk(relaxed = true) private val scope: LifecycleCoroutineScope = mockk(relaxed = true)
private val adjustBackgroundAndNavigate: (NavDirections) -> Unit = mockk(relaxed = true) private val browserAnimator: BrowserAnimator = mockk(relaxed = true)
private val snackbar = mockk<FenixSnackbar>(relaxed = true) private val snackbar = mockk<FenixSnackbar>(relaxed = true)
private val tabCollectionStorage = mockk<TabCollectionStorage>(relaxed = true) private val tabCollectionStorage = mockk<TabCollectionStorage>(relaxed = true)
private val topSiteStorage = mockk<TopSiteStorage>(relaxed = true) private val topSiteStorage = mockk<TopSiteStorage>(relaxed = true)
@ -92,12 +93,11 @@ class DefaultBrowserToolbarControllerTest {
browsingModeManager = browsingModeManager, browsingModeManager = browsingModeManager,
findInPageLauncher = findInPageLauncher, findInPageLauncher = findInPageLauncher,
engineView = engineView, engineView = engineView,
adjustBackgroundAndNavigate = adjustBackgroundAndNavigate, browserAnimator = browserAnimator,
customTabSession = null, customTabSession = null,
getSupportUrl = getSupportUrl, getSupportUrl = getSupportUrl,
openInFenixIntent = openInFenixIntent, openInFenixIntent = openInFenixIntent,
scope = scope, scope = scope,
browserLayout = browserLayout,
swipeRefresh = swipeRefreshLayout, swipeRefresh = swipeRefreshLayout,
tabCollectionStorage = tabCollectionStorage, tabCollectionStorage = tabCollectionStorage,
topSiteStorage = topSiteStorage, topSiteStorage = topSiteStorage,
@ -122,7 +122,9 @@ class DefaultBrowserToolbarControllerTest {
every { activity.components.useCases.sessionUseCases } returns sessionUseCases every { activity.components.useCases.sessionUseCases } returns sessionUseCases
every { activity.components.useCases.searchUseCases } returns searchUseCases every { activity.components.useCases.searchUseCases } returns searchUseCases
every { activity.components.core.sessionManager.selectedSession } returns currentSession every { activity.components.core.sessionManager.selectedSession } returns currentSession
every { adjustBackgroundAndNavigate.invoke(any()) } just Runs
val onComplete = slot<() -> Unit>()
every { browserAnimator.captureEngineViewAndDrawStatically(capture(onComplete)) } answers { onComplete.captured.invoke() }
} }
@Test @Test
@ -133,12 +135,11 @@ class DefaultBrowserToolbarControllerTest {
controller.handleToolbarPaste(pastedText) controller.handleToolbarPaste(pastedText)
verify { verify {
adjustBackgroundAndNavigate.invoke( val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment( sessionId = "1",
sessionId = "1", pastedText = pastedText
pastedText = pastedText
)
) )
navController.nav(R.id.browserFragment, directions)
} }
} }
@ -178,11 +179,10 @@ class DefaultBrowserToolbarControllerTest {
verify { metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.BROWSER)) } verify { metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.BROWSER)) }
verify { verify {
adjustBackgroundAndNavigate.invoke( val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
sessionId = "1" sessionId = "1"
) )
) navController.nav(R.id.browserFragment, directions)
} }
} }
@ -229,16 +229,15 @@ class DefaultBrowserToolbarControllerTest {
} }
@Test @Test
fun handleToolbarSettingsPress() { fun handleToolbarSettingsPress() = runBlocking {
val item = ToolbarMenu.Item.Settings val item = ToolbarMenu.Item.Settings
controller.handleToolbarItemInteraction(item) controller.handleToolbarItemInteraction(item)
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SETTINGS)) } verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SETTINGS)) }
verify { verify {
adjustBackgroundAndNavigate.invoke( val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment() navController.nav(R.id.browserFragment, directions)
)
} }
} }
@ -250,9 +249,8 @@ class DefaultBrowserToolbarControllerTest {
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.LIBRARY)) } verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.LIBRARY)) }
verify { verify {
adjustBackgroundAndNavigate.invoke( val directions = BrowserFragmentDirections.actionBrowserFragmentToLibraryFragment()
BrowserFragmentDirections.actionBrowserFragmentToLibraryFragment() navController.nav(R.id.browserFragment, directions)
)
} }
} }
@ -304,12 +302,11 @@ class DefaultBrowserToolbarControllerTest {
browsingModeManager = browsingModeManager, browsingModeManager = browsingModeManager,
findInPageLauncher = findInPageLauncher, findInPageLauncher = findInPageLauncher,
engineView = engineView, engineView = engineView,
adjustBackgroundAndNavigate = adjustBackgroundAndNavigate, browserAnimator = browserAnimator,
customTabSession = null, customTabSession = null,
getSupportUrl = getSupportUrl, getSupportUrl = getSupportUrl,
openInFenixIntent = openInFenixIntent, openInFenixIntent = openInFenixIntent,
scope = this, scope = this,
browserLayout = browserLayout,
swipeRefresh = swipeRefreshLayout, swipeRefresh = swipeRefreshLayout,
tabCollectionStorage = tabCollectionStorage, tabCollectionStorage = tabCollectionStorage,
topSiteStorage = topSiteStorage, topSiteStorage = topSiteStorage,
@ -500,12 +497,11 @@ class DefaultBrowserToolbarControllerTest {
browsingModeManager = browsingModeManager, browsingModeManager = browsingModeManager,
findInPageLauncher = findInPageLauncher, findInPageLauncher = findInPageLauncher,
engineView = engineView, engineView = engineView,
adjustBackgroundAndNavigate = adjustBackgroundAndNavigate, browserAnimator = browserAnimator,
customTabSession = currentSession, customTabSession = currentSession,
getSupportUrl = getSupportUrl, getSupportUrl = getSupportUrl,
openInFenixIntent = openInFenixIntent, openInFenixIntent = openInFenixIntent,
scope = scope, scope = scope,
browserLayout = browserLayout,
swipeRefresh = swipeRefreshLayout, swipeRefresh = swipeRefreshLayout,
tabCollectionStorage = tabCollectionStorage, tabCollectionStorage = tabCollectionStorage,
topSiteStorage = topSiteStorage, topSiteStorage = topSiteStorage,
@ -542,12 +538,11 @@ class DefaultBrowserToolbarControllerTest {
browsingModeManager = browsingModeManager, browsingModeManager = browsingModeManager,
findInPageLauncher = findInPageLauncher, findInPageLauncher = findInPageLauncher,
engineView = engineView, engineView = engineView,
adjustBackgroundAndNavigate = adjustBackgroundAndNavigate, browserAnimator = browserAnimator,
customTabSession = null, customTabSession = null,
getSupportUrl = getSupportUrl, getSupportUrl = getSupportUrl,
openInFenixIntent = openInFenixIntent, openInFenixIntent = openInFenixIntent,
scope = testScope, scope = testScope,
browserLayout = browserLayout,
swipeRefresh = swipeRefreshLayout, swipeRefresh = swipeRefreshLayout,
tabCollectionStorage = tabCollectionStorage, tabCollectionStorage = tabCollectionStorage,
topSiteStorage = topSiteStorage, topSiteStorage = topSiteStorage,

View File

@ -20,18 +20,17 @@ import kotlinx.coroutines.test.setMain
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.Engine
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import mozilla.components.feature.tab.collections.Tab as ComponentTab import mozilla.components.feature.tab.collections.Tab as ComponentTab
@ -61,6 +60,7 @@ class DefaultSessionControlControllerTest {
private val sessionManager: SessionManager = mockk(relaxed = true) private val sessionManager: SessionManager = mockk(relaxed = true)
private val engine: Engine = mockk(relaxed = true) private val engine: Engine = mockk(relaxed = true)
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true) private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
private lateinit var controller: DefaultSessionControlController private lateinit var controller: DefaultSessionControlController
@ -71,6 +71,7 @@ class DefaultSessionControlControllerTest {
every { activity.components.core.engine } returns engine every { activity.components.core.engine } returns engine
every { activity.components.core.sessionManager } returns sessionManager every { activity.components.core.sessionManager } returns sessionManager
every { activity.components.core.tabCollectionStorage } returns tabCollectionStorage every { activity.components.core.tabCollectionStorage } returns tabCollectionStorage
every { activity.components.useCases.tabsUseCases } returns tabsUseCases
every { store.state } returns state every { store.state } returns state
every { state.collections } returns emptyList() every { state.collections } returns emptyList()
@ -194,10 +195,24 @@ class DefaultSessionControlControllerTest {
fun handleSelectTab() { fun handleSelectTab() {
val tabView: View = mockk(relaxed = true) val tabView: View = mockk(relaxed = true)
val sessionId = "hello" val sessionId = "hello"
val directions = HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)
controller.handleSelectTab(tabView, sessionId) controller.handleSelectTab(tabView, sessionId)
verify { invokePendingDeleteJobs() } verify { invokePendingDeleteJobs() }
verify { navController.nav(R.id.homeFragment, directions) } verify { activity.openToBrowser(BrowserDirection.FromHome) }
}
@Test
fun handleSelectTopSite() {
val topSiteUrl = "mozilla.org"
controller.handleSelectTopSite(topSiteUrl)
verify { invokePendingDeleteJobs() }
verify { metrics.track(Event.TopSiteOpenInNewTab) }
verify { tabsUseCases.addTab.invoke(
topSiteUrl,
selectTab = true,
startLoading = true
) }
verify { activity.openToBrowser(BrowserDirection.FromHome) }
} }
@Test @Test