diff --git a/app/build.gradle b/app/build.gradle index da5d7bfdf8..1c8c4be885 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt index 9951804135..f4ac27ffe9 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt @@ -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.") } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TrayViewHolders.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TrayViewHolders.kt index 405f21fde1..8bb03f408a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayViewHolders.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayViewHolders.kt @@ -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, 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 + + override fun bind( + adapter: RecyclerView.Adapter, + 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() { + 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_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.normal_browser_tray_list + } +} + +class PrivateBrowserTabViewHolder( + containerView: View, + interactor: TabsTrayInteractor +) : BrowserTabViewHolder(containerView, interactor) { + companion object { + const val LAYOUT_ID = R.layout.private_browser_tray_list } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BaseBrowserTrayList.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BaseBrowserTrayList.kt index 653d756bef..fdc33ad4e1 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BaseBrowserTrayList.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BaseBrowserTrayList.kt @@ -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, diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt index 9d64dbb3e9..455ea319ce 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt @@ -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 = ObserverRegistry() -) : TabsAdapter(delegate) { +) : TabsAdapter(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? = 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())) + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt index bf6f42b9e4..8fffdb9137 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt @@ -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 } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt index c0bced6016..e8cbcb0716 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt @@ -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( delegate: Observable = ObserverRegistry() -) : RecyclerView.Adapter(), TabsTray, Observable by delegate { - private var tabs: Tabs? = null +) : RecyclerView.Adapter(), TabsTray, Observable 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 diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsDetailsLookup.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsDetailsLookup.kt new file mode 100644 index 0000000000..219f314a36 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsDetailsLookup.kt @@ -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() { + + override fun getItemDetails(event: MotionEvent): ItemDetails? { + val view = recyclerView.findChildViewUnder(event.x, event.y) + if (view != null) { + val viewHolder = recyclerView.getChildViewHolder(view) as TabTrayViewHolder + return viewHolder.getItemDetails() + } + return null + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt index 5d08324331..63ff46b7cf 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt @@ -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() { + 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) diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 3e117e06cc..40c8517a65 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -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}"