2
0
mirror of https://github.com/fork-maintainers/iceraven-browser synced 2024-11-03 23:15:31 +00:00

[fenix] Close https://github.com/mozilla-mobile/fenix/issues/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>
This commit is contained in:
Jonathan Almeida 2021-03-25 12:59:31 -04:00 committed by Jonathan Almeida
parent 83e350ed4c
commit 0e3def9e83
10 changed files with 151 additions and 36 deletions

View File

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

View File

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

View File

@ -5,15 +5,22 @@
package org.mozilla.fenix.tabstray package org.mozilla.fenix.tabstray
import android.view.View 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 androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.browser.BaseBrowserTrayList 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. * Base [RecyclerView.ViewHolder] for [TrayPagerAdapter] items.
*/ */
sealed class TrayViewHolder constructor( abstract class TrayViewHolder constructor(
override val containerView: View override val containerView: View
) : RecyclerView.ViewHolder(containerView), LayoutContainer { ) : RecyclerView.ViewHolder(containerView), LayoutContainer {
@ -23,17 +30,18 @@ sealed class TrayViewHolder constructor(
) )
} }
class BrowserTabViewHolder( abstract class BrowserTabViewHolder(
containerView: View, containerView: View,
interactor: TabsTrayInteractor interactor: TabsTrayInteractor
) : TrayViewHolder(containerView) { ) : 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 { init {
trayList.interactor = interactor trayList.interactor = interactor
} }
@CallSuper
override fun bind( override fun bind(
adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>, adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>,
layoutManager: RecyclerView.LayoutManager layoutManager: RecyclerView.LayoutManager
@ -41,9 +49,50 @@ class BrowserTabViewHolder(
trayList.layoutManager = layoutManager trayList.layoutManager = layoutManager
trayList.adapter = adapter 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 { companion object {
const val LAYOUT_ID_NORMAL_TAB = R.layout.normal_browser_tray_list const val LAYOUT_ID = R.layout.normal_browser_tray_list
const val LAYOUT_ID_PRIVATE_TAB = R.layout.private_browser_tray_list }
}
class PrivateBrowserTabViewHolder(
containerView: View,
interactor: TabsTrayInteractor
) : BrowserTabViewHolder(containerView, interactor) {
companion object {
const val LAYOUT_ID = R.layout.private_browser_tray_list
} }
} }

View File

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

View File

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

View File

@ -9,7 +9,8 @@ import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.tabstray.TabsTrayInteractor 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 { interface BrowserTrayInteractor {
@ -24,9 +25,9 @@ interface BrowserTrayInteractor {
fun onCloseTab(tab: Tab) 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) { override fun isMultiSelectMode(): Boolean {
// TODO https://github.com/mozilla-mobile/fenix/issues/18443 // Needs https://github.com/mozilla-mobile/fenix/issues/18513 to change this value
return false
} }
} }

View File

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

View File

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

View File

@ -4,6 +4,7 @@
package org.mozilla.fenix.tabtray package org.mozilla.fenix.tabtray
import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
@ -12,7 +13,12 @@ import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageButton import androidx.appcompat.widget.AppCompatImageButton
import androidx.core.content.ContextCompat 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.*
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.selector.findTabOrCustomTab
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabViewHolder 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.settings
import org.mozilla.fenix.ext.showAndEnable import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import kotlin.math.max import kotlin.math.max
/** /**
@ -44,7 +51,8 @@ class TabTrayViewHolder(
itemView: View, itemView: View,
private val imageLoader: ImageLoader, private val imageLoader: ImageLoader,
private val store: BrowserStore = itemView.context.components.core.store, 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) { ) : TabViewHolder(itemView) {
private val faviconView: ImageView? = 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) { private fun updateCloseButtonDescription(title: String) {
closeView.contentDescription = closeView.contentDescription =
closeView.context.getString(R.string.close_tab_title, title) closeView.context.getString(R.string.close_tab_title, title)

View File

@ -29,6 +29,7 @@ object Versions {
const val androidx_fragment = "1.2.5" const val androidx_fragment = "1.2.5"
const val androidx_navigation = "2.3.3" const val androidx_navigation = "2.3.3"
const val androidx_recyclerview = "1.2.0-beta01" 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_core = "1.3.2"
const val androidx_paging = "2.1.2" const val androidx_paging = "2.1.2"
const val androidx_transition = "1.4.0" 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_fragment = "androidx.navigation:navigation-fragment-ktx:${Versions.androidx_navigation}"
const val androidx_navigation_ui = "androidx.navigation:navigation-ui:${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 = "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 = "androidx.core:core:${Versions.androidx_core}"
const val androidx_core_ktx = "androidx.core:core-ktx:${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}" const val androidx_transition = "androidx.transition:transition:${Versions.androidx_transition}"