diff --git a/app/src/main/java/org/mozilla/fenix/home/collections/Collection.kt b/app/src/main/java/org/mozilla/fenix/home/collections/Collection.kt index 7be997890..6875e305e 100644 --- a/app/src/main/java/org/mozilla/fenix/home/collections/Collection.kt +++ b/app/src/main/java/org/mozilla/fenix/home/collections/Collection.kt @@ -83,7 +83,7 @@ fun Collection( .height(48.dp), shape = if (isExpanded) expandedCollectionShape else collapsedCollectionShape, backgroundColor = FirefoxTheme.colors.layer2, - elevation = 5.dp, // This needs to match the elevation of TabInCollection for matching shadows. + elevation = 5.dp, ) { Row( modifier = Modifier diff --git a/app/src/main/java/org/mozilla/fenix/home/collections/CollectionItem.kt b/app/src/main/java/org/mozilla/fenix/home/collections/CollectionItem.kt new file mode 100644 index 000000000..2c5ad9e2e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/collections/CollectionItem.kt @@ -0,0 +1,261 @@ +/* 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.home.collections + +import android.content.Context +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.DismissDirection +import androidx.compose.material.DismissDirection.EndToStart +import androidx.compose.material.DismissDirection.StartToEnd +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.SwipeToDismiss +import androidx.compose.material.rememberDismissState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import mozilla.components.browser.state.state.recover.RecoverableTab +import mozilla.components.concept.engine.Engine +import mozilla.components.feature.tab.collections.Tab +import org.mozilla.fenix.R.drawable +import org.mozilla.fenix.R.string +import org.mozilla.fenix.compose.inComposePreview +import org.mozilla.fenix.compose.list.FaviconListItem +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.toShortUrl +import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme + +/** + * Rectangular shape with only right angles used to display a middle tab. + */ +private val MIDDLE_TAB_SHAPE = RoundedCornerShape(0.dp) + +/** + * Rectangular shape with only the bottom corners rounded used to display the last tab in a collection. + */ +private val BOTTOM_TAB_SHAPE = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp) + +/** + * Display an individual [Tab] as part of a collection. + * + * @param tab [Tab] to display. + * @param isLastInCollection Whether the tab is to be shown between others or as the last one in collection. + * @param onClick Invoked when the user click on the tab. + * @param onRemove Invoked when the user removes the tab informing also if the tab was swiped to be removed. + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun CollectionItem( + tab: Tab, + isLastInCollection: Boolean, + onClick: () -> Unit, + onRemove: (Boolean) -> Unit, +) { + val dismissState = rememberDismissState() + + if (dismissState.isDismissed(StartToEnd) || dismissState.isDismissed(EndToStart)) { + onRemove(true) + } + + SwipeToDismiss( + state = dismissState, + background = { + DismissedTabBackground( + dismissDirection = dismissState.dismissDirection, + isLastInCollection = isLastInCollection, + ) + } + ) { + // We need to clip the top bounds to avoid this item drawing shadows over the above item. + // But we need to add this shadows back to have a clearer separation between tabs + // when one is being swiped away. + val clippingModifier by remember { + derivedStateOf { + try { + if (dismissState.progress.fraction != 1f) Modifier else Modifier.clipTop() + } catch (e: NoSuchElementException) { + // `androidx.compose.material.Swipeable.findBounds` couldn't find anchors. + // Happened once in testing when deleting a tab. Could not reproduce afterwards. + Modifier.clipTop() + } + } + } + + Card( + modifier = clippingModifier + .fillMaxWidth(), + shape = if (isLastInCollection) BOTTOM_TAB_SHAPE else MIDDLE_TAB_SHAPE, + backgroundColor = FirefoxTheme.colors.layer2, + elevation = 5.dp, + ) { + FaviconListItem( + label = tab.title, + description = shortenUrl(tab.url), + onClick = onClick, + url = tab.url, + iconPainter = painterResource(drawable.ic_close), + iconDescription = stringResource(string.remove_tab_from_collection), + onIconClick = { onRemove(false) }, + ) + } + } +} + +/** + * Composable used to display the background of a [Tab] shown in collections that is being swiped left or right. + * + * @param dismissDirection [DismissDirection] of the tab being swiped depending on which this composable + * will also indicate the swipe direction by placing a warning icon at the start of the swipe gesture. + * If `null` the warning icon will be shown at both ends. + * @param isLastInCollection Whether the tab is to be shown between others or as the last one in collection. + */ +@Composable +private fun DismissedTabBackground( + dismissDirection: DismissDirection?, + isLastInCollection: Boolean, +) { + Card( + modifier = Modifier.fillMaxSize(), + backgroundColor = FirefoxTheme.colors.layer3, + shape = if (isLastInCollection) BOTTOM_TAB_SHAPE else MIDDLE_TAB_SHAPE, + elevation = 0.dp, + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(drawable.ic_delete), + contentDescription = null, + modifier = Modifier + .padding(horizontal = 32.dp) + // Only show the delete icon for where the swipe starts. + .alpha( + if (dismissDirection == StartToEnd) 1f else 0f + ), + tint = FirefoxTheme.colors.iconWarning, + ) + + Icon( + painter = painterResource(drawable.ic_delete), + contentDescription = null, + modifier = Modifier + .padding(horizontal = 32.dp) + // Only show the delete icon for where the swipe starts. + .alpha( + if (dismissDirection == EndToStart) 1f else 0f + ), + tint = FirefoxTheme.colors.iconWarning, + ) + } + } +} + +/** + * Clips the Composable this applies to such that it cannot draw content / shadows outside it's top bound. + */ +private fun Modifier.clipTop() = this.then( + Modifier.drawWithContent { + val paddingPx = Constraints.Infinity.toFloat() + clipRect( + left = 0f - paddingPx, + top = 0f, + right = size.width + paddingPx, + bottom = size.height + paddingPx + ) { + this@drawWithContent.drawContent() + } + } +) + +/** + * Get a friendlier short url for [url]. + * + * @param url Full url to be shortened. + * + * @see toShortUrl + */ +@Composable +private fun shortenUrl(url: String): String { + return if (inComposePreview) { + url + } else { + url.toShortUrl(LocalContext.current.components.publicSuffixList) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun TabInCollectionPreview() { + FirefoxTheme(theme = Theme.getTheme(isPrivate = false)) { + Column { + Box(modifier = Modifier.height(56.dp)) { + DismissedTabBackground( + dismissDirection = StartToEnd, + isLastInCollection = false, + ) + } + CollectionItem( + tab = tabPreview, + isLastInCollection = false, + onClick = {}, + onRemove = {}, + ) + + Spacer(Modifier.height(10.dp)) + + Box(modifier = Modifier.height(56.dp)) { + DismissedTabBackground( + dismissDirection = EndToStart, + isLastInCollection = true, + ) + } + CollectionItem( + tab = tabPreview, + isLastInCollection = true, + onClick = {}, + onRemove = {}, + ) + } + } +} + +private val tabPreview = object : Tab { + override val id = 2L + override val title = "Mozilla-Firefox" + override val url = "https://www.mozilla.org/en-US/firefox/whats-new-in-last-version" + + override fun restore( + context: Context, + engine: Engine, + restoreSessionId: Boolean + ): RecoverableTab? = null +} diff --git a/app/src/main/java/org/mozilla/fenix/home/collections/TabInCollectionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/collections/TabInCollectionViewHolder.kt new file mode 100644 index 000000000..520d819b7 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/collections/TabInCollectionViewHolder.kt @@ -0,0 +1,94 @@ +/* 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.home.collections + +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.feature.tab.collections.Tab +import mozilla.components.feature.tab.collections.TabCollection +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.ComposeViewHolder +import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor + +/** + * [RecyclerView.ViewHolder] for displaying an individual [Tab]. + * Clients are expected to use [bindSession] to link a particular [Tab] to be displayed + * otherwise this will be an empty, 0 size View. + * + * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. + * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. + * @param interactor [CollectionInteractor] callback for user interactions. + */ +class TabInCollectionViewHolder( + composeView: ComposeView, + viewLifecycleOwner: LifecycleOwner, + private val interactor: CollectionInteractor, +) : ComposeViewHolder(composeView, viewLifecycleOwner) { + private var tabData = TabInfo() + + init { + val horizontalPadding = + composeView.resources.getDimensionPixelSize(R.dimen.home_item_horizontal_margin) + composeView.setPadding(horizontalPadding, 0, horizontalPadding, 0) + } + + @Composable + override fun Content() { + val tabInfo = remember { mutableStateOf(tabData) } + + tabInfo.value.tab?.let { tab -> + tabInfo.value.collection?.let { collection -> + + CollectionItem( + tab = tab, + isLastInCollection = tabInfo.value.isLastInCollection, + onClick = { interactor.onCollectionOpenTabClicked(tab) }, + onRemove = { wasSwiped -> + interactor.onCollectionRemoveTab( + collection = collection, + tab = tab, + wasSwiped = wasSwiped, + ) + }, + ) + } + } + } + + /** + * Dynamically replace the current [Tab] shown in this `ViewHolder`. + * + * @param collection [TabCollection] containing [tab]. + * @param tab [Tab] to display. + * @param isLastInCollection Whether [tab] is to be shown as the last item in [collection]. + */ + fun bindSession(collection: TabCollection, tab: Tab, isLastInCollection: Boolean) { + tabData = TabInfo(collection, tab, isLastInCollection) + } + + companion object { + val LAYOUT_ID = View.generateViewId() + } +} + +/** + * Wrapper over a [Tab] adding information about the collection it is part of and the position in this collection. + * + * @property collection [TabCollection] which contains this tab. + * @property tab [Tab] to display. + * @property isLastInCollection Whether the tab is to be shown between others or as the last one in collection. + */ +@Stable +private data class TabInfo( + val collection: TabCollection? = null, + val tab: Tab? = null, + val isLastInCollection: Boolean = false, +) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index 932c24646..b375abe28 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -15,12 +15,12 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite -import mozilla.components.ui.widgets.WidgetSiteItemView import org.mozilla.fenix.components.Components import org.mozilla.fenix.gleanplumb.Message import org.mozilla.fenix.home.BottomSpacerViewHolder import org.mozilla.fenix.home.TopPlaceholderViewHolder import org.mozilla.fenix.home.collections.CollectionViewHolder +import org.mozilla.fenix.home.collections.TabInCollectionViewHolder import org.mozilla.fenix.home.pocket.PocketCategoriesViewHolder import org.mozilla.fenix.home.pocket.PocketRecommendationsHeaderViewHolder import org.mozilla.fenix.home.pocket.PocketStoriesViewHolder @@ -34,7 +34,6 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHol import org.mozilla.fenix.home.sessioncontrol.viewholders.CustomizeHomeButtonViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder -import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.MessageCardViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder @@ -284,9 +283,14 @@ class SessionControlAdapter( ) CollectionViewHolder.LAYOUT_ID -> return CollectionViewHolder( composeView = ComposeView(parent.context), - viewLifecycleOwner, + viewLifecycleOwner = viewLifecycleOwner, interactor = interactor ) + TabInCollectionViewHolder.LAYOUT_ID -> return TabInCollectionViewHolder( + composeView = ComposeView(parent.context), + viewLifecycleOwner = viewLifecycleOwner, + interactor = interactor, + ) } val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) @@ -297,10 +301,6 @@ class SessionControlAdapter( viewLifecycleOwner = viewLifecycleOwner, interactor = interactor ) - TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder( - view as WidgetSiteItemView, - interactor - ) OnboardingHeaderViewHolder.LAYOUT_ID -> OnboardingHeaderViewHolder(view) OnboardingSectionHeaderViewHolder.LAYOUT_ID -> OnboardingSectionHeaderViewHolder(view) OnboardingManualSignInViewHolder.LAYOUT_ID -> OnboardingManualSignInViewHolder(view) @@ -348,6 +348,11 @@ class SessionControlAdapter( // This ViewHolder can be removed / re-added and we need it to show a fresh new composition. holder.composeView.disposeComposition() } + is TabInCollectionViewHolder -> { + // Dispose the underlying composition immediately. + // This ViewHolder can be removed / re-added and we need it to show a fresh new composition. + holder.composeView.disposeComposition() + } else -> super.onViewRecycled(holder) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index fc7c6d9d8..f5b160236 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -7,7 +7,6 @@ package org.mozilla.fenix.home.sessioncontrol import android.view.View import androidx.annotation.VisibleForTesting import androidx.lifecycle.LifecycleOwner -import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import mozilla.components.feature.tab.collections.TabCollection @@ -202,13 +201,6 @@ class SessionControlView( JumpBackInCFRDialog(view).showIfNeeded() } } - val itemTouchHelper = - ItemTouchHelper( - SwipeToDeleteCallback( - interactor - ) - ) - itemTouchHelper.attachToRecyclerView(this) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt deleted file mode 100644 index 814722a8f..000000000 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* 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.home.sessioncontrol - -import android.content.res.Resources -import android.graphics.Canvas -import android.graphics.drawable.Drawable -import androidx.appcompat.content.res.AppCompatResources -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView -import mozilla.components.support.ktx.android.content.getColorFromAttr -import org.mozilla.fenix.R -import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder - -class SwipeToDeleteCallback( - val interactor: SessionControlInteractor -) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - // We don't support drag and drop so this method will never be called - return false - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - when (viewHolder) { - is TabInCollectionViewHolder -> { - interactor.onCollectionRemoveTab(viewHolder.collection, viewHolder.tab, wasSwiped = true) - } - } - } - - override fun onChildDraw( - c: Canvas, - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - dX: Float, - dY: Float, - actionState: Int, - isCurrentlyActive: Boolean - ) { - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) - val icon = AppCompatResources.getDrawable(recyclerView.context, R.drawable.ic_delete) - icon?.setTint(recyclerView.context.getColorFromAttr(R.attr.textWarning)) - - val backgroundDrawable = when { - viewHolder is TabInCollectionViewHolder && viewHolder.isLastItem -> { - R.drawable.tab_in_collection_last_swipe_background - } - viewHolder is TabInCollectionViewHolder -> { - R.drawable.tab_in_collection_swipe_background - } - else -> R.drawable.session_background - } - - val background = AppCompatResources.getDrawable(recyclerView.context, backgroundDrawable) - background?.let { - icon?.let { - val itemView = viewHolder.itemView - val iconLeft: Int - val iconRight: Int - val margin = convertDpToPixel(MARGIN.toFloat()) - val iconWidth = icon.intrinsicWidth - val iconHeight = icon.intrinsicHeight - val cellHeight = itemView.bottom - itemView.top - val iconTop = itemView.top + (cellHeight - iconHeight) / 2 - val iconBottom = iconTop + iconHeight - - when { - dX > 0 -> { // Swiping to the right - iconLeft = itemView.left + margin - iconRight = itemView.left + margin + iconWidth - background.setBounds( - itemView.left, itemView.top, - (itemView.left + dX).toInt() + BACKGROUND_CORNER_OFFSET, - itemView.bottom - ) - icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) - draw(background, icon, c) - } - dX < 0 -> { // Swiping to the left - iconLeft = itemView.right - margin - iconWidth - iconRight = itemView.right - margin - background.setBounds( - (itemView.right + dX).toInt() - BACKGROUND_CORNER_OFFSET, - itemView.top, itemView.right, itemView.bottom - ) - icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) - draw(background, icon, c) - } - else -> { // View not swiped - background.setBounds(0, 0, 0, 0) - icon.setBounds(0, 0, 0, 0) - } - } - } - } - } - - override fun getSwipeDirs( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder - ): Int { - return if (recyclerView.hasWindowFocus() && - viewHolder is TabInCollectionViewHolder - ) { - super.getSwipeDirs(recyclerView, viewHolder) - } else 0 - } - - companion object { - const val BACKGROUND_CORNER_OFFSET = 40 - const val MARGIN = 32 - private const val DENSITY_CONVERSION = 160f - - private fun draw( - background: Drawable, - icon: Drawable, - c: Canvas - ) { - background.draw(c) - icon.draw(c) - } - - private fun convertDpToPixel(dp: Float): Int { - val metrics = Resources.getSystem().displayMetrics - val px = dp * (metrics.densityDpi / DENSITY_CONVERSION) - return Math.round(px) - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt deleted file mode 100644 index 17ad83f28..000000000 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* 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.home.sessioncontrol.viewholders - -import android.os.Build -import android.os.Build.VERSION.SDK_INT -import androidx.appcompat.content.res.AppCompatResources -import mozilla.components.browser.icons.BrowserIcons -import mozilla.components.feature.tab.collections.TabCollection -import mozilla.components.lib.publicsuffixlist.PublicSuffixList -import mozilla.components.support.ktx.android.content.getColorFromAttr -import mozilla.components.support.ktx.android.content.res.resolveAttribute -import mozilla.components.ui.widgets.WidgetSiteItemView -import org.mozilla.fenix.R -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.loadIntoView -import org.mozilla.fenix.ext.toShortUrl -import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor -import org.mozilla.fenix.utils.view.ViewHolder -import mozilla.components.feature.tab.collections.Tab as ComponentTab - -class TabInCollectionViewHolder( - private val view: WidgetSiteItemView, - val interactor: CollectionInteractor, - private val icons: BrowserIcons = view.context.components.core.icons, - private val publicSuffixList: PublicSuffixList = view.context.components.publicSuffixList -) : ViewHolder(view) { - - lateinit var collection: TabCollection - private set - lateinit var tab: ComponentTab - private set - var isLastItem = false - private set - - init { - if (SDK_INT >= Build.VERSION_CODES.M) { - view.foreground = AppCompatResources.getDrawable( - view.context, - view.context.theme.resolveAttribute(R.attr.selectableItemBackground) - ) - } - - // This needs to match the elevation of the CollectionViewHolder for the shadow - view.elevation = view.resources.getDimension(R.dimen.home_item_elevation) - - view.setOnClickListener { - interactor.onCollectionOpenTabClicked(tab) - } - - view.setSecondaryButton( - icon = R.drawable.ic_close, - contentDescription = R.string.remove_tab_from_collection - ) { - interactor.onCollectionRemoveTab(collection, tab, wasSwiped = false) - } - } - - fun bindSession(collection: TabCollection, tab: ComponentTab, isLastTab: Boolean) { - this.collection = collection - this.tab = tab - this.isLastItem = isLastTab - updateTabUI() - } - - private fun updateTabUI() { - view.setText( - label = tab.title, - caption = tab.url.toShortUrl(publicSuffixList) - ) - - icons.loadIntoView(view.iconView, tab.url) - - // If last item and we want to change UI for it - val context = view.context - if (isLastItem) { - view.background = AppCompatResources.getDrawable(context, R.drawable.rounded_bottom_corners) - } else { - view.setBackgroundColor(context.getColorFromAttr(R.attr.layer2)) - } - } - - companion object { - const val LAYOUT_ID = R.layout.site_list_item - } -} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt index 41041350c..49825d270 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt @@ -17,7 +17,6 @@ import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.getDrawableWithTint import mozilla.components.support.ktx.android.util.dpToPx import org.mozilla.fenix.R -import org.mozilla.fenix.home.sessioncontrol.SwipeToDeleteCallback /** * A callback for consumers to know when a [RecyclerView.ViewHolder] is about to be touched. @@ -95,7 +94,7 @@ class TouchCallback( val iconLeft: Int val iconRight: Int val margin = - SwipeToDeleteCallback.MARGIN.dpToPx(recyclerView.resources.displayMetrics) + MARGIN.dpToPx(recyclerView.resources.displayMetrics) val iconWidth = icon.intrinsicWidth val iconHeight = icon.intrinsicHeight val cellHeight = itemView.bottom - itemView.top @@ -108,7 +107,7 @@ class TouchCallback( iconRight = itemView.left + margin + iconWidth background.setBounds( itemView.left, itemView.top, - (itemView.left + dX).toInt() + SwipeToDeleteCallback.BACKGROUND_CORNER_OFFSET, + (itemView.left + dX).toInt() + BACKGROUND_CORNER_OFFSET, itemView.bottom ) icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) @@ -118,7 +117,7 @@ class TouchCallback( iconLeft = itemView.right - margin - iconWidth iconRight = itemView.right - margin background.setBounds( - (itemView.right + dX).toInt() - SwipeToDeleteCallback.BACKGROUND_CORNER_OFFSET, + (itemView.right + dX).toInt() - BACKGROUND_CORNER_OFFSET, itemView.top, itemView.right, itemView.bottom ) icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) @@ -139,4 +138,9 @@ class TouchCallback( background.draw(c) icon.draw(c) } + + companion object { + const val BACKGROUND_CORNER_OFFSET = 40 + const val MARGIN = 32 + } } diff --git a/app/src/main/res/drawable/session_background.xml b/app/src/main/res/drawable/session_background.xml deleted file mode 100644 index a53dd3768..000000000 --- a/app/src/main/res/drawable/session_background.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/tab_in_collection_last_swipe_background.xml b/app/src/main/res/drawable/tab_in_collection_last_swipe_background.xml deleted file mode 100644 index 983fbdc22..000000000 --- a/app/src/main/res/drawable/tab_in_collection_last_swipe_background.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/tab_in_collection_swipe_background.xml b/app/src/main/res/drawable/tab_in_collection_swipe_background.xml deleted file mode 100644 index fcc47f735..000000000 --- a/app/src/main/res/drawable/tab_in_collection_swipe_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index cb2a676cb..f0333bebf 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -76,7 +76,6 @@ 60dp - 5dp 16dp diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 53a7d694c..d676235bf 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -458,7 +458,6 @@ UndocumentedPublicClass:SupportUtils.kt$SupportUtils UndocumentedPublicClass:SupportUtils.kt$SupportUtils$MozillaPage UndocumentedPublicClass:SupportUtils.kt$SupportUtils$SumoTopic - UndocumentedPublicClass:SwipeToDeleteCallback.kt$SwipeToDeleteCallback : SimpleCallback UndocumentedPublicClass:SwitchWithDescription.kt$SwitchWithDescription : ConstraintLayout UndocumentedPublicClass:Tab.kt$Tab UndocumentedPublicClass:TabCollectionStorage.kt$TabCollectionStorage : Observable