Close #18443: Use recyclerview-selection for multi-select mode in tray

Add multi-select mode to the BrowserTabsAdapter. It has the
functionality to:
 - Enable multi-select mode on long-press.
 - Enable multi-select mode when changed by an external function.
 - Only works for normal tabs (as we currently have it).

Co-authored-by: "codrut.topliceanu" <codrut.topliceanu@softvision.ro>
upstream-sync
Jonathan Almeida 3 years ago committed by Jonathan Almeida
parent 5d0af34537
commit 9ce4adaa32

@ -533,6 +533,7 @@ dependencies {
implementation Deps.androidx_navigation_fragment
implementation Deps.androidx_navigation_ui
implementation Deps.androidx_recyclerview
implementation Deps.androidx_recyclerview_selection
implementation Deps.androidx_lifecycle_livedata
implementation Deps.androidx_lifecycle_runtime
implementation Deps.androidx_lifecycle_viewmodel

@ -9,8 +9,6 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.tabstray.BrowserTabViewHolder.Companion.LAYOUT_ID_NORMAL_TAB
import org.mozilla.fenix.tabstray.BrowserTabViewHolder.Companion.LAYOUT_ID_PRIVATE_TAB
import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
@ -27,8 +25,14 @@ class TrayPagerAdapter(
val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
LAYOUT_ID_NORMAL_TAB -> BrowserTabViewHolder(itemView, interactor)
LAYOUT_ID_PRIVATE_TAB -> BrowserTabViewHolder(itemView, interactor)
NormalBrowserTabViewHolder.LAYOUT_ID -> NormalBrowserTabViewHolder(
itemView,
interactor
)
PrivateBrowserTabViewHolder.LAYOUT_ID -> PrivateBrowserTabViewHolder(
itemView,
interactor
)
else -> throw IllegalStateException("Unknown viewType.")
}
}
@ -45,8 +49,8 @@ class TrayPagerAdapter(
override fun getItemViewType(position: Int): Int {
return when (position) {
POSITION_NORMAL_TABS -> LAYOUT_ID_NORMAL_TAB
POSITION_PRIVATE_TABS -> LAYOUT_ID_PRIVATE_TAB
POSITION_NORMAL_TABS -> NormalBrowserTabViewHolder.LAYOUT_ID
POSITION_PRIVATE_TABS -> PrivateBrowserTabViewHolder.LAYOUT_ID
else -> throw IllegalStateException("Unknown position.")
}
}

@ -5,15 +5,22 @@
package org.mozilla.fenix.tabstray
import android.view.View
import androidx.annotation.CallSuper
import androidx.recyclerview.selection.SelectionPredicates
import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.selection.StableIdKeyProvider
import androidx.recyclerview.selection.StorageStrategy
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.browser.BaseBrowserTrayList
import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
import org.mozilla.fenix.tabstray.browser.TabsDetailsLookup
/**
* Base [RecyclerView.ViewHolder] for [TrayPagerAdapter] items.
*/
sealed class TrayViewHolder constructor(
abstract class TrayViewHolder constructor(
override val containerView: View
) : RecyclerView.ViewHolder(containerView), LayoutContainer {
@ -23,17 +30,18 @@ sealed class TrayViewHolder constructor(
)
}
class BrowserTabViewHolder(
abstract class BrowserTabViewHolder(
containerView: View,
interactor: TabsTrayInteractor
) : TrayViewHolder(containerView) {
private val trayList: BaseBrowserTrayList = itemView.findViewById(R.id.tray_list_item)
protected val trayList: BaseBrowserTrayList = itemView.findViewById(R.id.tray_list_item)
init {
trayList.interactor = interactor
}
@CallSuper
override fun bind(
adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>,
layoutManager: RecyclerView.LayoutManager
@ -41,9 +49,50 @@ class BrowserTabViewHolder(
trayList.layoutManager = layoutManager
trayList.adapter = adapter
}
}
class NormalBrowserTabViewHolder(
containerView: View,
interactor: TabsTrayInteractor
) : BrowserTabViewHolder(containerView, interactor) {
private lateinit var selectionTracker: SelectionTracker<Long>
override fun bind(
adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>,
layoutManager: RecyclerView.LayoutManager
) {
super.bind(adapter, layoutManager)
selectionTracker = SelectionTracker.Builder(
"mySelection",
trayList,
StableIdKeyProvider(trayList),
TabsDetailsLookup(trayList),
StorageStrategy.createLongStorage()
).withSelectionPredicate(
SelectionPredicates.createSelectAnything()
).build()
(adapter as BrowserTabsAdapter).tracker = selectionTracker
selectionTracker.addObserver(object : SelectionTracker.SelectionObserver<Long>() {
override fun onItemStateChanged(key: Long, selected: Boolean) {
// TODO Do nothing for now; remove in a future patch if needed.
}
})
}
companion object {
const val LAYOUT_ID = R.layout.normal_browser_tray_list
}
}
class PrivateBrowserTabViewHolder(
containerView: View,
interactor: TabsTrayInteractor
) : BrowserTabViewHolder(containerView, interactor) {
companion object {
const val LAYOUT_ID_NORMAL_TAB = R.layout.normal_browser_tray_list
const val LAYOUT_ID_PRIVATE_TAB = R.layout.private_browser_tray_list
const val LAYOUT_ID = R.layout.private_browser_tray_list
}
}

@ -53,7 +53,7 @@ abstract class BaseBrowserTrayList @JvmOverloads constructor(
private val tabsFeature by lazy {
ViewBoundFeatureWrapper(
feature = TabsFeature(
adapter as TabsAdapter,
adapter as BrowserTabsAdapter,
context.components.core.store,
selectTabUseCase,
removeTabUseCase,

@ -6,6 +6,7 @@ package org.mozilla.fenix.tabstray.browser
import android.content.Context
import android.view.ViewGroup
import androidx.recyclerview.selection.SelectionTracker
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.GridLayoutManager
import kotlinx.android.synthetic.main.tab_tray_item.view.*
@ -27,7 +28,7 @@ class BrowserTabsAdapter(
private val interactor: BrowserTrayInteractor,
private val layoutManager: (() -> GridLayoutManager)? = null,
delegate: Observable<TabsTray.Observer> = ObserverRegistry()
) : TabsAdapter(delegate) {
) : TabsAdapter<TabViewHolder>(delegate) {
/**
* The layout types for the tabs.
@ -37,8 +38,17 @@ class BrowserTabsAdapter(
GRID
}
/**
* Tracks the selected tabs in multi-select mode.
*/
var tracker: SelectionTracker<Long>? = null
private val imageLoader = ThumbnailLoader(context.components.core.thumbnailStorage)
init {
setHasStableIds(true)
}
override fun getItemViewType(position: Int): Int {
return if (context.settings().gridTabView) {
ViewType.GRID.ordinal
@ -54,19 +64,12 @@ class BrowserTabsAdapter(
}
}
override fun getItemId(position: Int) = position.toLong()
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
holder.tab?.let { tab ->
if (!tab.private) {
holder.itemView.setOnLongClickListener {
interactor.onMultiSelect(true)
true
}
} else {
holder.itemView.setOnLongClickListener(null)
}
holder.itemView.setOnClickListener {
interactor.onOpenTab(tab)
}
@ -74,6 +77,10 @@ class BrowserTabsAdapter(
holder.itemView.mozac_browser_tabstray_close.setOnClickListener {
interactor.onCloseTab(tab)
}
tracker?.let {
holder.showTabIsMultiSelectEnabled(it.isSelected(position.toLong()))
}
}
}
}

@ -9,7 +9,8 @@ import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.tabstray.TabsTrayInteractor
/**
* For interacting with UI that extends from [BaseBrowserTrayList] and other browser tab tray views.
* For interacting with UI that is specifically for [BaseBrowserTrayList] and other browser
* tab tray views.
*/
interface BrowserTrayInteractor {
@ -24,9 +25,9 @@ interface BrowserTrayInteractor {
fun onCloseTab(tab: Tab)
/**
* Enable or disable multi-select mode.
* If multi-select mode is enabled or disabled.
*/
fun onMultiSelect(enabled: Boolean)
fun isMultiSelectMode(): Boolean
}
/**
@ -54,9 +55,10 @@ class DefaultBrowserTrayInteractor(
}
/**
* See [BrowserTrayInteractor.onMultiSelect].
* See [BrowserTrayInteractor.isMultiSelectMode].
*/
override fun onMultiSelect(enabled: Boolean) {
// TODO https://github.com/mozilla-mobile/fenix/issues/18443
override fun isMultiSelectMode(): Boolean {
// Needs https://github.com/mozilla-mobile/fenix/issues/18513 to change this value
return false
}
}

@ -18,14 +18,12 @@ import mozilla.components.support.base.observer.ObserverRegistry
// for Android UI APIs.
//
// TODO Let's upstream this to AC with tests.
abstract class TabsAdapter(
abstract class TabsAdapter<T : TabViewHolder>(
delegate: Observable<TabsTray.Observer> = ObserverRegistry()
) : RecyclerView.Adapter<TabViewHolder>(), TabsTray, Observable<TabsTray.Observer> by delegate {
private var tabs: Tabs? = null
) : RecyclerView.Adapter<T>(), TabsTray, Observable<TabsTray.Observer> by delegate {
var styling: TabsTrayStyling = TabsTrayStyling()
override fun getItemCount(): Int = tabs?.list?.size ?: 0
protected var tabs: Tabs? = null
protected var styling: TabsTrayStyling = TabsTrayStyling()
@CallSuper
override fun updateTabs(tabs: Tabs) {
@ -35,12 +33,14 @@ abstract class TabsAdapter(
}
@CallSuper
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
override fun onBindViewHolder(holder: T, position: Int) {
val tabs = tabs ?: return
holder.bind(tabs.list[position], isTabSelected(tabs, position), styling, this)
}
override fun getItemCount(): Int = tabs?.list?.size ?: 0
final override fun isTabSelected(tabs: Tabs, position: Int): Boolean =
tabs.selectedIndex == position

@ -0,0 +1,28 @@
/* 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.tabstray.browser
import android.view.MotionEvent
import androidx.recyclerview.selection.ItemDetailsLookup
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.tabtray.TabTrayViewHolder
/**
* An [ItemDetailsLookup] for retrieving the [ItemDetails] of a [TabTrayViewHolder].
*/
class TabsDetailsLookup(
private val recyclerView: RecyclerView
) : ItemDetailsLookup<Long>() {
override fun getItemDetails(event: MotionEvent): ItemDetails<Long>? {
val view = recyclerView.findChildViewUnder(event.x, event.y)
if (view != null) {
val viewHolder = recyclerView.getChildViewHolder(view) as TabTrayViewHolder
return viewHolder.getItemDetails()
}
return null
}
}

@ -4,6 +4,7 @@
package org.mozilla.fenix.tabtray
import android.view.MotionEvent
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
@ -12,7 +13,12 @@ import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageButton
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.selection.ItemDetailsLookup
import kotlinx.android.synthetic.main.checkbox_item.view.*
import kotlinx.android.synthetic.main.tab_tray_grid_item.view.*
import kotlinx.android.synthetic.main.tab_tray_grid_item.view.mozac_browser_tabstray_close
import kotlinx.android.synthetic.main.tab_tray_item.view.*
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabViewHolder
@ -35,6 +41,7 @@ import org.mozilla.fenix.ext.removeTouchDelegate
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import kotlin.math.max
/**
@ -44,7 +51,8 @@ class TabTrayViewHolder(
itemView: View,
private val imageLoader: ImageLoader,
private val store: BrowserStore = itemView.context.components.core.store,
private val metrics: MetricController = itemView.context.components.analytics.metrics
private val metrics: MetricController = itemView.context.components.analytics.metrics,
private val browserTrayInteractor: BrowserTrayInteractor? = null
) : TabViewHolder(itemView) {
private val faviconView: ImageView? =
@ -196,6 +204,20 @@ class TabTrayViewHolder(
)
}
fun getItemDetails() = object : ItemDetailsLookup.ItemDetails<Long>() {
override fun getPosition(): Int = bindingAdapterPosition
override fun getSelectionKey(): Long = itemId
override fun inSelectionHotspot(e: MotionEvent): Boolean {
return browserTrayInteractor?.isMultiSelectMode() == true
}
}
fun showTabIsMultiSelectEnabled(isSelected: Boolean) {
itemView.selected_mask.isVisible = isSelected
itemView.mozac_browser_tabstray_close.isVisible =
browserTrayInteractor?.isMultiSelectMode() == false
}
private fun updateCloseButtonDescription(title: String) {
closeView.contentDescription =
closeView.context.getString(R.string.close_tab_title, title)

@ -29,6 +29,7 @@ object Versions {
const val androidx_fragment = "1.2.5"
const val androidx_navigation = "2.3.3"
const val androidx_recyclerview = "1.2.0-beta01"
const val androidx_recyclerview_selection = "1.0.0"
const val androidx_core = "1.3.2"
const val androidx_paging = "2.1.2"
const val androidx_transition = "1.4.0"
@ -185,6 +186,7 @@ object Deps {
const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment-ktx:${Versions.androidx_navigation}"
const val androidx_navigation_ui = "androidx.navigation:navigation-ui:${Versions.androidx_navigation}"
const val androidx_recyclerview = "androidx.recyclerview:recyclerview:${Versions.androidx_recyclerview}"
const val androidx_recyclerview_selection = "androidx.recyclerview:recyclerview-selection:${Versions.androidx_recyclerview_selection}"
const val androidx_core = "androidx.core:core:${Versions.androidx_core}"
const val androidx_core_ktx = "androidx.core:core-ktx:${Versions.androidx_core}"
const val androidx_transition = "androidx.transition:transition:${Versions.androidx_transition}"

Loading…
Cancel
Save