Commit e9673260 authored by Sawyer Blatz's avatar Sawyer Blatz Committed by Emily Kager
Browse files

For #167: Improves home to browser animation

parent a3d40b70
......@@ -321,7 +321,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
BrowserDirection.FromGlobal ->
NavGraphDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHome ->
HomeFragmentDirections.actionHomeFragmentToBrowserFragment(customTabSessionId)
HomeFragmentDirections.actionHomeFragmentToBrowserFragment(customTabSessionId, true)
BrowserDirection.FromSearch ->
SearchFragmentDirections.actionSearchFragmentToBrowserFragment(customTabSessionId)
BrowserDirection.FromSettings ->
......
......@@ -6,8 +6,6 @@ package org.mozilla.fenix.browser
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
......@@ -15,11 +13,9 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections
import androidx.navigation.fragment.findNavController
import com.google.android.material.appbar.AppBarLayout
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.settings.SupportUtils
import org.mozilla.fenix.theme.ThemeManager
import java.lang.ref.WeakReference
/**
* Base fragment extended by [BrowserFragment].
......@@ -98,6 +95,7 @@ import org.mozilla.fenix.theme.ThemeManager
@Suppress("TooManyFunctions", "LargeClass")
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer {
protected lateinit var browserFragmentStore: BrowserFragmentStore
private lateinit var browserAnimator: BrowserAnimator
private var _browserInteractor: BrowserToolbarViewInteractor? = null
protected val browserInteractor: BrowserToolbarViewInteractor
......@@ -164,6 +162,15 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
initializeEngineView(toolbarHeight)
browserAnimator = BrowserAnimator(
fragment = WeakReference(this),
engineView = WeakReference(engineView),
swipeRefresh = WeakReference(swipeRefresh),
arguments = arguments!!
).apply {
beginAnimationIfNecessary()
}
return getSessionById()?.also { session ->
val browserToolbarController = DefaultBrowserToolbarController(
store = browserFragmentStore,
......@@ -177,10 +184,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
browsingModeManager = (activity as HomeActivity).browsingModeManager,
sessionManager = requireComponents.core.sessionManager,
findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } },
browserLayout = view.browserLayout,
engineView = engineView,
swipeRefresh = swipeRefresh,
adjustBackgroundAndNavigate = ::adjustBackgroundAndNavigate,
browserAnimator = browserAnimator,
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
getSupportUrl = {
SupportUtils.getSumoURLForTopic(
......@@ -499,26 +505,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
view: View
): 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
override fun onSessionSelected(session: Session) {
(activity as HomeActivity).updateThemeForSession(session)
......
/* 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
}
}
......@@ -64,7 +64,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
savedInstanceState: Bundle?
): View {
val view = super.onCreateView(inflater, container, savedInstanceState)
view.browserLayout.transitionName = "$TAB_ITEM_TRANSITION_NAME${getSessionById()?.id}"
startPostponedEnterTransition()
return view
}
......
......@@ -6,16 +6,8 @@ package org.mozilla.fenix.components.toolbar
import android.app.Activity
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.core.graphics.drawable.toDrawable
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import androidx.navigation.fragment.FragmentNavigator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
......@@ -30,6 +22,7 @@ import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator
import org.mozilla.fenix.browser.BrowserFragment
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
......@@ -57,6 +50,8 @@ interface BrowserToolbarController {
fun handleTabCounterClick()
}
typealias onComplete = () -> Unit
@Suppress("LargeClass")
class DefaultBrowserToolbarController(
private val store: BrowserFragmentStore,
......@@ -66,9 +61,8 @@ class DefaultBrowserToolbarController(
private val browsingModeManager: BrowsingModeManager,
private val sessionManager: SessionManager,
private val findInPageLauncher: () -> Unit,
private val browserLayout: ViewGroup,
private val engineView: EngineView,
private val adjustBackgroundAndNavigate: (NavDirections) -> Unit,
private val browserAnimator: BrowserAnimator,
private val swipeRefresh: SwipeRefreshLayout,
private val customTabSession: Session?,
private val getSupportUrl: () -> String,
......@@ -89,12 +83,14 @@ class DefaultBrowserToolbarController(
internal var ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
override fun handleToolbarPaste(text: String) {
adjustBackgroundAndNavigate.invoke(
BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
browserAnimator.captureEngineViewAndDrawStatically {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment(
sessionId = currentSession?.id,
pastedText = text
)
)
navController.nav(R.id.browserFragment, directions)
}
}
override fun handleToolbarPasteAndGo(text: String) {
......@@ -112,9 +108,14 @@ class DefaultBrowserToolbarController(
activity.components.analytics.metrics.track(
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() {
......@@ -132,12 +133,14 @@ class DefaultBrowserToolbarController(
ToolbarMenu.Item.Forward -> sessionUseCases.goForward.invoke(currentSession)
ToolbarMenu.Item.Reload -> sessionUseCases.reload.invoke(currentSession)
ToolbarMenu.Item.Stop -> sessionUseCases.stopLoading.invoke(currentSession)
ToolbarMenu.Item.Settings -> adjustBackgroundAndNavigate.invoke(
BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
)
ToolbarMenu.Item.Library -> adjustBackgroundAndNavigate.invoke(
BrowserFragmentDirections.actionBrowserFragmentToLibraryFragment()
)
ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
navController.nav(R.id.browserFragment, directions)
}
ToolbarMenu.Item.Library -> browserAnimator.captureEngineViewAndDrawStatically {
val directions = BrowserFragmentDirections.actionBrowserFragmentToLibraryFragment()
navController.nav(R.id.browserFragment, directions)
}
is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke(
item.isChecked,
currentSession
......@@ -297,28 +300,14 @@ class DefaultBrowserToolbarController(
}
private fun animateTabAndNavigateHome() {
// We need to dynamically add the options here because if you do it in XML it overwrites
val options = NavOptions.Builder().setPopUpTo(R.id.nav_graph, false)
.setEnterAnim(R.anim.fade_in).build()
val extras = FragmentNavigator.Extras.Builder().addSharedElement(
browserLayout,
"${TAB_ITEM_TRANSITION_NAME}${currentSession?.id}"
).build()
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
)
}
browserAnimator.captureEngineViewAndDrawStatically {
if (!navController.popBackStack(R.id.homeFragment, false)) {
val directions = BrowserFragmentDirections.actionBrowserFragmentToHomeFragment()
navController.nav(
R.id.browserFragment,
directions,
null
)
}
}
}
......
......@@ -38,7 +38,6 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import androidx.transition.TransitionInflater
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
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.MediaStateMachine
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.support.ktx.android.util.dpToPx
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
......@@ -145,9 +144,6 @@ class HomeFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
.setDuration(SHARED_TRANSITION_MS)
val sessionObserver = BrowserSessionsObserver(sessionManager, singleSessionObserver) {
emitSessionChanges()
......@@ -270,6 +266,10 @@ class HomeFragment : Fragment() {
sessionControlView!!.view.layoutManager?.onRestoreInstanceState(parcelable)
}
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) {
......@@ -309,14 +309,7 @@ class HomeFragment : Fragment() {
view.toolbar_wrapper.setOnClickListener {
invokePendingDeleteJobs()
hideOnboardingIfNeeded()
val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment(
sessionId = null
)
val extras =
FragmentNavigator.Extras.Builder()
.addSharedElement(toolbar_wrapper, "toolbar_wrapper_transition")
.build()
nav(R.id.homeFragment, directions, extras)
navigateToSearch()
requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME))
}
......@@ -550,7 +543,12 @@ class HomeFragment : Fragment() {
val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment(
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() {
......@@ -897,12 +895,13 @@ class HomeFragment : Fragment() {
}
companion object {
private const val ANIMATION_DELAY = 100L
private const val NON_TAB_ITEM_NUM = 3
private const val ANIM_SCROLL_DELAY = 100L
private const val ANIM_ON_SCREEN_DELAY = 200L
private const val FADE_ANIM_DURATION = 150L
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_Y_OFFSET = -20
......
......@@ -6,7 +6,6 @@ package org.mozilla.fenix.home.sessioncontrol
import android.view.View
import androidx.navigation.NavController
import androidx.navigation.fragment.FragmentNavigator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
......@@ -341,27 +340,19 @@ class DefaultSessionControlController(
invokePendingDeleteJobs()
val session = sessionManager.findSessionById(sessionId)
sessionManager.select(session!!)
val directions = HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)
val extras =
FragmentNavigator.Extras.Builder()
.addSharedElement(
tabView,
"$TAB_ITEM_TRANSITION_NAME$sessionId"
)
.build()
navController.nav(R.id.homeFragment, directions, extras)
activity.openToBrowser(BrowserDirection.FromHome)
}
override fun handleSelectTopSite(url: String) {
invokePendingDeleteJobs()
metrics.track(Event.TopSiteOpenInNewTab)
if (url == SupportUtils.POCKET_TRENDING_URL) {
metrics.track(Event.PocketTopSiteClicked)
}
activity.components.useCases.tabsUseCases.addTab.invoke(url, true, true)
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)
if (url == SupportUtils.POCKET_TRENDING_URL) { metrics.track(Event.PocketTopSiteClicked) }
activity.components.useCases.tabsUseCases.addTab.invoke(
url = url,
selectTab = true,
startLoading = true
)
activity.openToBrowser(BrowserDirection.FromHome)
}
override fun handleShareTabs() {
......
......@@ -59,11 +59,11 @@ class SearchFragment : Fragment(), UserInteractionHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
.setDuration(
SHARED_TRANSITION_MS
)
.setDuration(SHARED_TRANSITION_MS)
requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea)
}
......@@ -346,7 +346,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
}
companion object {
private const val SHARED_TRANSITION_MS = 200L
private const val SHARED_TRANSITION_MS = 250L
private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1
}
}
......@@ -19,7 +19,7 @@ class FragmentPreDrawManager(
fragment.postponeEnterTransition()
}
fun execute(code: () -> Unit) {
fun execute(code: suspend () -> Unit) {
fragment.view?.doOnPreDraw {
fragment.viewLifecycleOwner.lifecycleScope.launch {
code()
......
......@@ -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="250" />
\ No newline at end of file
android:duration="150" />
\ No newline at end of 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="250" />
\ No newline at end of file
android:duration="150" />
\ No newline at end of file
<!-- 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>
\ No newline at end of file
......@@ -15,6 +15,7 @@
android:background="@drawable/toolbar_background_top"
android:clickable="true"
android:focusable="true"
android:transitionName="toolbar_wrapper_transition"
android:focusableInTouchMode="true"
app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed"
app:browserToolbarClearColor="?primaryText"
......
......@@ -14,9 +14,11 @@
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<mozilla.components.concept.engine.EngineView