Skip to content
This repository has been archived by the owner on Jan 12, 2023. It is now read-only.

Commit

Permalink
For #5741: Add option to close sessions selectively.
Browse files Browse the repository at this point in the history
Refactor TabsAdapter to implement ListAdapter.
Refactor TabViewHolder.
Remove state observing and pass initial tab list:
The tab list and the selected tab do not change while fragment is displayed.
  • Loading branch information
mcarare authored and mergify[bot] committed Dec 7, 2021
1 parent 923f533 commit 3ab7190
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 141 deletions.
119 changes: 70 additions & 49 deletions app/src/main/java/org/mozilla/focus/session/ui/TabSheetFragment.kt
Expand Up @@ -14,54 +14,67 @@ import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.ext.flowScoped
import org.mozilla.focus.GleanMetrics.TabCount
import org.mozilla.focus.R
import org.mozilla.focus.databinding.FragmentSessionssheetBinding
import org.mozilla.focus.ext.components
import org.mozilla.focus.ext.requireComponents
import org.mozilla.focus.state.AppAction
import org.mozilla.focus.utils.OneShotOnPreDrawListener
import kotlin.math.pow
import kotlin.math.sqrt

class TabSheetFragment : Fragment(), View.OnClickListener {

private lateinit var backgroundView: View
private lateinit var cardView: View
class TabSheetFragment : Fragment() {
private var isAnimating: Boolean = false

private var scope: CoroutineScope? = null

private lateinit var store: BrowserStore
private var scope: CoroutineScope? = null
private lateinit var binding: FragmentSessionssheetBinding

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentSessionssheetBinding.inflate(layoutInflater, container, false)
store = requireComponents.store

binding.apply {
background.setOnClickListener {
if (isAnimating) {
// Ignore touched while we are animating
return@setOnClickListener
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_sessionssheet, container, false)

backgroundView = view.findViewById(R.id.background)
backgroundView.setOnClickListener(this)

cardView = view.findViewById(R.id.card)
OneShotOnPreDrawListener(cardView) {
playAnimation(false)
true
}

store = view.context.components.store

val sessionsAdapter = TabsAdapter(this, store.state.privateTabs)
animateAndDismiss()
val openedTabs = store.state.tabs.size
TabCount.sessionListClosed.record(TabCount.SessionListClosedExtra(openedTabs))
}

scope = store.flowScoped(owner = this) { flow ->
sessionsAdapter.onFlow(flow)
}
OneShotOnPreDrawListener(binding.card) {
playAnimation(false)
true
}

view.findViewById<RecyclerView>(R.id.sessions).let {
it.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
it.adapter = sessionsAdapter
val sessionsAdapter = TabsAdapter(
tabList = store.state.privateTabs,
isCurrentSession = { tab -> isCurrentSession(tab) },
selectSession = { tab -> selectSession(tab) },
closeSession = { tab -> closeSession(tab) }
)

sessions.apply {
adapter = sessionsAdapter
layoutManager = LinearLayoutManager(context)
setHasFixedSize(true)
}
}

return view
return binding.root
}

override fun onDestroyView() {
Expand All @@ -73,13 +86,14 @@ class TabSheetFragment : Fragment(), View.OnClickListener {
private fun playAnimation(reverse: Boolean): Animator {
isAnimating = true

val cardView = binding.card
val offset = resources.getDimensionPixelSize(R.dimen.tab_sheet_end_margin) / 2
val cx = cardView.measuredWidth - offset
val cy = cardView.measuredHeight - offset

// The final radius is the diagonal of the card view -> sqrt(w^2 + h^2)
val fullRadius = Math.sqrt(
Math.pow(cardView.width.toDouble(), 2.0) + Math.pow(cardView.height.toDouble(), 2.0)
val fullRadius = sqrt(
cardView.width.toDouble().pow(2.0) + cardView.height.toDouble().pow(2.0)
).toFloat()

val sheetAnimator = ViewAnimationUtils.createCircularReveal(
Expand All @@ -101,16 +115,16 @@ class TabSheetFragment : Fragment(), View.OnClickListener {
start()
}

backgroundView.alpha = if (reverse) 1f else 0f
backgroundView.animate()
binding.background.alpha = if (reverse) 1f else 0f
binding.background.animate()
.alpha(if (reverse) 0f else 1f)
.setDuration(ANIMATION_DURATION.toLong())
.start()

return sheetAnimator
}

internal fun animateAndDismiss(): Animator {
private fun animateAndDismiss(): Animator {
val animator = playAnimation(true)

animator.addListener(object : AnimatorListenerAdapter() {
Expand All @@ -133,27 +147,34 @@ class TabSheetFragment : Fragment(), View.OnClickListener {
return true
}

override fun onClick(view: View) {
if (isAnimating) {
// Ignore touched while we are animating
return
}
private fun isCurrentSession(tab: TabSessionState): Boolean {
return requireComponents.store.state.selectedTabId == tab.id
}

when (view.id) {
R.id.background -> {
animateAndDismiss()
private fun selectSession(tab: TabSessionState) {
animateAndDismiss().addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
requireComponents.tabsUseCases.selectTab.invoke(tab.id)

val openedTabs = store.state.tabs.size
TabCount.sessionListClosed.record(TabCount.SessionListClosedExtra(openedTabs))
val openedTabs = requireComponents.store.state.tabs.size
TabCount.sessionListItemTapped.record(TabCount.SessionListItemTappedExtra(openedTabs))
}
})
}

else -> throw IllegalStateException("Unhandled view in onClick()")
}
private fun closeSession(tab: TabSessionState) {
animateAndDismiss().addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
requireComponents.tabsUseCases.removeTab.invoke(tab.id, selectParentIfExists = false)

val openedTabs = requireComponents.store.state.tabs.size
TabCount.sessionListItemTapped.record(TabCount.SessionListItemTappedExtra(openedTabs))
}
})
}

companion object {
const val FRAGMENT_TAG = "tab_sheet"

private const val ANIMATION_DURATION = 200
}
}
72 changes: 24 additions & 48 deletions app/src/main/java/org/mozilla/focus/session/ui/TabViewHolder.kt
Expand Up @@ -4,72 +4,48 @@

package org.mozilla.focus.session.ui

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.state.TabSessionState
import org.mozilla.focus.GleanMetrics.TabCount
import org.mozilla.focus.R
import org.mozilla.focus.databinding.ItemSessionBinding
import org.mozilla.focus.ext.beautifyUrl
import org.mozilla.focus.ext.components
import org.mozilla.focus.ext.requireComponents
import java.lang.ref.WeakReference

class TabViewHolder internal constructor(
private val fragment: TabSheetFragment,
private val textView: TextView
) : RecyclerView.ViewHolder(textView), View.OnClickListener {
companion object {
internal const val LAYOUT_ID = R.layout.item_session
}
class TabViewHolder(
private val binding: ItemSessionBinding,
) : RecyclerView.ViewHolder(binding.root) {

private var tabReference: WeakReference<TabSessionState> = WeakReference<TabSessionState>(null)

init {
textView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_link, 0, 0, 0)
textView.setOnClickListener(this)
}

fun bind(tab: TabSessionState) {
fun bind(
tab: TabSessionState,
isCurrentSession: Boolean,
selectSession: (TabSessionState) -> Unit,
closeSession: (TabSessionState) -> Unit
) {
this.tabReference = WeakReference(tab)

updateTitle(tab)

val isSelected = fragment.requireComponents.store.state.selectedTabId == tab.id

updateTextBackgroundColor(isSelected)
}

private fun updateTextBackgroundColor(isCurrentSession: Boolean) {
val drawable = if (isCurrentSession) {
R.drawable.background_list_item_current_session
} else {
R.drawable.background_list_item_session
}
textView.setBackgroundResource(drawable)
}

private fun updateTitle(tab: TabSessionState) {
textView.text =
if (tab.content.title.isEmpty()) tab.content.url.beautifyUrl()
else tab.content.title
}

override fun onClick(view: View) {
val tab = tabReference.get() ?: return
selectSession(tab)
}

private fun selectSession(tab: TabSessionState) {
fragment.animateAndDismiss().addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
fragment.components?.tabsUseCases?.selectTab?.invoke(tab.id)
val title = tab.content.title.ifEmpty { tab.content.url.beautifyUrl() }

val openedTabs = fragment.requireComponents.store.state.tabs.size
TabCount.sessionListItemTapped.record(TabCount.SessionListItemTappedExtra(openedTabs))
binding.sessionItem.setBackgroundResource(drawable)
binding.sessionTitle.apply {
setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_link, 0, 0, 0)
text = title
setOnClickListener {
val clickedTab = tabReference.get() ?: return@setOnClickListener
selectSession(clickedTab)
}
})
}

binding.closeButton.setOnClickListener {
val clickedTab = tabReference.get() ?: return@setOnClickListener
closeSession(clickedTab)
}
}
}
52 changes: 20 additions & 32 deletions app/src/main/java/org/mozilla/focus/session/ui/TabsAdapter.kt
Expand Up @@ -6,47 +6,35 @@ package org.mozilla.focus.session.ui

import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.focus.databinding.ItemSessionBinding

/**
* Adapter implementation to show a list of active tabs.
*/
class TabsAdapter internal constructor(
private val fragment: TabSheetFragment,
private var tabs: List<TabSessionState> = emptyList()
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)

return TabViewHolder(
fragment,
inflater.inflate(TabViewHolder.LAYOUT_ID, parent, false) as TextView
)
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as TabViewHolder).bind(tabs[position])
private val tabList: List<TabSessionState>,
private val isCurrentSession: (TabSessionState) -> Boolean,
private val selectSession: (TabSessionState) -> Unit,
private val closeSession: (TabSessionState) -> Unit,
) : RecyclerView.Adapter<TabViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder {
val binding =
ItemSessionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return TabViewHolder(binding)
}

override fun getItemCount(): Int {
return tabs.size
}

suspend fun onFlow(flow: Flow<BrowserState>) {
flow.ifAnyChanged { state -> arrayOf(state.privateTabs.size, state.selectedTabId) }
.collect { state -> onUpdate(state.privateTabs) }
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
val currentItem = tabList[position]
holder.bind(
currentItem,
isCurrentSession = isCurrentSession.invoke(currentItem),
selectSession = selectSession,
closeSession = closeSession,
)
}

private fun onUpdate(tabs: List<TabSessionState>) {
this.tabs = tabs
notifyDataSetChanged()
}
override fun getItemCount() = tabList.size
}

0 comments on commit 3ab7190

Please sign in to comment.