[fenix] For https://github.com/mozilla-mobile/fenix/issues/24333 - Replace the xml based CollectionViewHolder with a composable
parent
d92adcff12
commit
7185eee3df
@ -0,0 +1,226 @@
|
||||
/* 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.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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 mozilla.components.feature.tab.collections.TabCollection
|
||||
import org.mozilla.fenix.R.drawable
|
||||
import org.mozilla.fenix.R.string
|
||||
import org.mozilla.fenix.compose.list.ExpandableListHeader
|
||||
import org.mozilla.fenix.ext.getIconColor
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
import org.mozilla.fenix.theme.Theme
|
||||
|
||||
/**
|
||||
* Rectangular shape with all corners rounded used to display a collapsed collection.
|
||||
*/
|
||||
private val collapsedCollectionShape = RoundedCornerShape(8.dp)
|
||||
|
||||
/**
|
||||
* Rectangular shape with only the top corners rounded used to display an expanded collection with other views
|
||||
* placed immediately below this which can be shown immediately next to it, with no visible separation.
|
||||
*/
|
||||
private val expandedCollectionShape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)
|
||||
|
||||
/**
|
||||
* Displays an individual [TabCollection].
|
||||
*
|
||||
* @param collection [TabCollection] to display.
|
||||
* @param expanded Whether the collection is expanded to show it's containing tabs or not.
|
||||
* @param menuItems List of [CollectionMenuItem] to be shown in a menu.
|
||||
* @param onToggleCollectionExpanded Invoked when the user clicks on the collection.
|
||||
* @param onCollectionShareTabsClicked Invoked when the user clicks to share the collection.
|
||||
* @param onCollectionMenuOpened Invoked when the user clicks to open a menu for the collection.
|
||||
*/
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
fun Collection(
|
||||
collection: TabCollection,
|
||||
expanded: Boolean,
|
||||
menuItems: List<CollectionMenuItem>,
|
||||
onToggleCollectionExpanded: (TabCollection, Boolean) -> Unit,
|
||||
onCollectionShareTabsClicked: (TabCollection) -> Unit,
|
||||
onCollectionMenuOpened: () -> Unit,
|
||||
) {
|
||||
var isMenuExpanded by remember(collection) { mutableStateOf(false) }
|
||||
val isExpanded by remember(collection) { mutableStateOf(expanded) }
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.semantics(mergeDescendants = true) {}
|
||||
.clickable { onToggleCollectionExpanded(collection, !isExpanded) }
|
||||
.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.
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(drawable.ic_tab_collection),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 8.dp // (24.dp - 16.dp) hardcoded in ExpandableListHeader
|
||||
),
|
||||
tint = Paint().apply {
|
||||
color = Color(collection.getIconColor(LocalContext.current))
|
||||
blendMode = BlendMode.SrcIn
|
||||
}.color,
|
||||
)
|
||||
|
||||
ExpandableListHeader(
|
||||
headerText = collection.title,
|
||||
expanded = isExpanded,
|
||||
) {
|
||||
if (isExpanded) {
|
||||
Row {
|
||||
IconButton(
|
||||
onClick = { onCollectionShareTabsClicked(collection) }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(drawable.ic_share),
|
||||
contentDescription = stringResource(string.share_button_content_description),
|
||||
tint = FirefoxTheme.colors.iconPrimary,
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
isMenuExpanded = !isMenuExpanded
|
||||
onCollectionMenuOpened()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(drawable.ic_menu),
|
||||
contentDescription = stringResource(
|
||||
string.collection_menu_button_content_description
|
||||
),
|
||||
tint = FirefoxTheme.colors.iconPrimary,
|
||||
)
|
||||
|
||||
CollectionMenu(
|
||||
showMenu = isMenuExpanded,
|
||||
menuItems = menuItems,
|
||||
onDismissRequest = { isMenuExpanded = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun CollectionDarkPreview() {
|
||||
FirefoxTheme(Theme.Dark) {
|
||||
Collection(
|
||||
collection = collectionPreview,
|
||||
expanded = false,
|
||||
menuItems = emptyList(),
|
||||
onToggleCollectionExpanded = { _, _ -> },
|
||||
onCollectionShareTabsClicked = {},
|
||||
onCollectionMenuOpened = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun CollectionDarkExpandedPreview() {
|
||||
FirefoxTheme(Theme.Dark) {
|
||||
Collection(
|
||||
collection = collectionPreview,
|
||||
expanded = true,
|
||||
menuItems = emptyList(),
|
||||
onToggleCollectionExpanded = { _, _ -> },
|
||||
onCollectionShareTabsClicked = {},
|
||||
onCollectionMenuOpened = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
private fun CollectionLightPreview() {
|
||||
FirefoxTheme(Theme.Light) {
|
||||
Collection(
|
||||
collection = collectionPreview,
|
||||
expanded = false,
|
||||
menuItems = emptyList(),
|
||||
onToggleCollectionExpanded = { _, _ -> },
|
||||
onCollectionShareTabsClicked = {},
|
||||
onCollectionMenuOpened = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
private fun CollectionLightExpandedPreview() {
|
||||
FirefoxTheme(Theme.Light) {
|
||||
Collection(
|
||||
collection = collectionPreview,
|
||||
expanded = true,
|
||||
menuItems = emptyList(),
|
||||
onToggleCollectionExpanded = { _, _ -> },
|
||||
onCollectionShareTabsClicked = {},
|
||||
onCollectionMenuOpened = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val collectionPreview = object : TabCollection {
|
||||
override val id: Long = 1L
|
||||
override val tabs: List<Tab> = emptyList()
|
||||
override val title: String = "Collection 1"
|
||||
|
||||
override fun restore(
|
||||
context: Context,
|
||||
engine: Engine,
|
||||
restoreSessionId: Boolean,
|
||||
): List<RecoverableTab> = emptyList()
|
||||
|
||||
override fun restoreSubset(
|
||||
context: Context,
|
||||
engine: Engine,
|
||||
tabs: List<Tab>,
|
||||
restoreSessionId: Boolean,
|
||||
): List<RecoverableTab> = emptyList()
|
||||
}
|
@ -0,0 +1,210 @@
|
||||
/* 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.background
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.state.recover.RecoverableTab
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.feature.tab.collections.Tab
|
||||
import mozilla.components.feature.tab.collections.TabCollection
|
||||
import org.mozilla.fenix.R.string
|
||||
import org.mozilla.fenix.compose.inComposePreview
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
import org.mozilla.fenix.theme.Theme
|
||||
|
||||
/**
|
||||
* Menu shown for a [org.mozilla.fenix.home.collections.Collection].
|
||||
*
|
||||
* @see [DropdownMenu]
|
||||
*
|
||||
* @param showMenu Whether this is currently open and visible to the user.
|
||||
* @param menuItems List of options shown.
|
||||
* @param onDismissRequest Called when the user chooses a menu option or requests to dismiss the menu.
|
||||
*/
|
||||
@Composable
|
||||
fun CollectionMenu(
|
||||
showMenu: Boolean,
|
||||
menuItems: List<CollectionMenuItem>,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
DisposableEffect(LocalConfiguration.current.orientation) {
|
||||
onDispose { onDismissRequest() }
|
||||
}
|
||||
|
||||
// DropdownMenu uses the medium shape from MaterialTheme.
|
||||
// Override it's corner radius to be the same 8.dp as in mozac_browser_menu_corner_radius
|
||||
MaterialTheme(shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(8.dp))) {
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { onDismissRequest() },
|
||||
modifier = Modifier
|
||||
.background(color = FirefoxTheme.colors.layer2),
|
||||
) {
|
||||
for (item in menuItems) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
item.onClick()
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = item.title,
|
||||
color = item.color,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A menu item for collections.
|
||||
*
|
||||
* @property title The menu item title.
|
||||
* @property color The color that should be set for the title.
|
||||
* @property onClick Invoked when the user clicks on the menu item.
|
||||
*/
|
||||
@Immutable
|
||||
data class CollectionMenuItem(
|
||||
val title: String,
|
||||
val color: Color,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
||||
/**
|
||||
* Constructs and returns the default list of menu options for a [TabCollection].
|
||||
*
|
||||
* @param collection [TabCollection] for which the menu will be shown.
|
||||
* Might serve as an argument for the callbacks for when the user interacts with certain menu options.
|
||||
* @param onOpenTabsTapped Invoked when the user chooses to open the tabs from [collection].
|
||||
* @param onRenameCollectionTapped Invoked when the user chooses to rename the [collection].
|
||||
* @param onAddTabTapped Invoked when the user chooses to add tabs to [collection].
|
||||
* @param onDeleteCollectionTapped Invoked when the user chooses to delete [collection].
|
||||
*/
|
||||
@Composable
|
||||
fun getMenuItems(
|
||||
collection: TabCollection,
|
||||
onOpenTabsTapped: (TabCollection) -> Unit,
|
||||
onRenameCollectionTapped: (TabCollection) -> Unit,
|
||||
onAddTabTapped: (TabCollection) -> Unit,
|
||||
onDeleteCollectionTapped: (TabCollection) -> Unit,
|
||||
): List<CollectionMenuItem> {
|
||||
return listOfNotNull(
|
||||
CollectionMenuItem(
|
||||
title = stringResource(string.collection_open_tabs),
|
||||
color = FirefoxTheme.colors.textPrimary
|
||||
) {
|
||||
onOpenTabsTapped(collection)
|
||||
},
|
||||
CollectionMenuItem(
|
||||
title = stringResource(string.collection_rename),
|
||||
color = FirefoxTheme.colors.textPrimary
|
||||
) {
|
||||
onRenameCollectionTapped(collection)
|
||||
},
|
||||
|
||||
if (hasOpenTabs()) {
|
||||
CollectionMenuItem(
|
||||
title = stringResource(string.add_tab),
|
||||
color = FirefoxTheme.colors.textPrimary
|
||||
) {
|
||||
onAddTabTapped(collection)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
|
||||
CollectionMenuItem(
|
||||
title = stringResource(string.collection_delete),
|
||||
color = FirefoxTheme.colors.textWarning
|
||||
) {
|
||||
onDeleteCollectionTapped(collection)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun hasOpenTabs() = when (inComposePreview) {
|
||||
true -> true
|
||||
false -> LocalContext.current.components.core.store.state.normalTabs.isNotEmpty()
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun CollectionMenuDarkPreview() {
|
||||
FirefoxTheme(Theme.Dark) {
|
||||
CollectionMenu(
|
||||
showMenu = true,
|
||||
menuItems = getMenuItems(
|
||||
collection = collectionPreview,
|
||||
onOpenTabsTapped = {},
|
||||
onRenameCollectionTapped = {},
|
||||
onAddTabTapped = {},
|
||||
onDeleteCollectionTapped = {}
|
||||
),
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
private fun CollectionMenuLightPreview() {
|
||||
FirefoxTheme(Theme.Light) {
|
||||
CollectionMenu(
|
||||
showMenu = true,
|
||||
menuItems = getMenuItems(
|
||||
collection = collectionPreview,
|
||||
onOpenTabsTapped = {},
|
||||
onRenameCollectionTapped = {},
|
||||
onAddTabTapped = {},
|
||||
onDeleteCollectionTapped = {}
|
||||
),
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
private val collectionPreview = object : TabCollection {
|
||||
override val id: Long = 1L
|
||||
override val tabs: List<Tab> = emptyList()
|
||||
override val title: String = "Collection 1"
|
||||
|
||||
override fun restore(
|
||||
context: Context,
|
||||
engine: Engine,
|
||||
restoreSessionId: Boolean,
|
||||
): List<RecoverableTab> = emptyList()
|
||||
|
||||
override fun restoreSubset(
|
||||
context: Context,
|
||||
engine: Engine,
|
||||
tabs: List<Tab>,
|
||||
restoreSessionId: Boolean,
|
||||
): List<RecoverableTab> = emptyList()
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- 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/. -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/item_collection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginHorizontal="@dimen/home_item_horizontal_margin"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="@drawable/card_list_row_background"
|
||||
android:clickable="true"
|
||||
android:clipToPadding="false"
|
||||
android:elevation="@dimen/home_item_elevation"
|
||||
android:focusable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/collection_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_tab_collection"
|
||||
app:tint="@null" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/collection_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="start"
|
||||
android:maxLines="1"
|
||||
android:minLines="1"
|
||||
android:textAppearance="@style/Header14TextStyle"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="@id/collection_icon"
|
||||
app:layout_constraintEnd_toStartOf="@id/chevron"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@+id/collection_icon"
|
||||
app:layout_constraintTop_toTopOf="@id/collection_icon"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
<ImageView
|
||||
android:id="@+id/chevron"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:srcCompat="@drawable/ic_chevron"
|
||||
android:contentDescription="@string/tab_menu"
|
||||
app:layout_constraintBottom_toBottomOf="@id/collection_icon"
|
||||
app:layout_constraintEnd_toStartOf="@+id/collection_share_button"
|
||||
app:layout_constraintStart_toEndOf="@+id/collection_title"
|
||||
app:layout_constraintTop_toTopOf="@id/collection_icon" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/collection_share_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/share_button_content_description"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/collection_icon"
|
||||
app:layout_constraintEnd_toStartOf="@id/collection_overflow_button"
|
||||
app:layout_constraintTop_toTopOf="@id/collection_icon"
|
||||
app:srcCompat="@drawable/ic_share"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/collection_overflow_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/collection_menu_button_content_description"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/collection_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/collection_icon"
|
||||
app:srcCompat="@drawable/ic_menu"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<View
|
||||
android:id="@+id/selected_border"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/session_border"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in New Issue