For #21894: Move Tabs Tray to compose: Individual tab viewholders: ListViewHolder.
parent
017d1c4d1c
commit
5f1448460e
@ -0,0 +1,71 @@
|
||||
/* 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.compose.tabstray
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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 com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
import org.mozilla.fenix.theme.Theme
|
||||
|
||||
/**
|
||||
* Controller buttons for the media (play/pause) state for the given [tab].
|
||||
*
|
||||
* @param tab [TabSessionState] which the image should be shown.
|
||||
* @param onMediaIconClicked handles the click event when tab has media session like play/pause.
|
||||
* @param modifier [Modifier] to be applied to the layout.
|
||||
*/
|
||||
@Composable
|
||||
fun MediaImage(
|
||||
tab: TabSessionState,
|
||||
onMediaIconClicked: ((TabSessionState) -> Unit),
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val (icon, contentDescription) = when (tab.mediaSessionState?.playbackState) {
|
||||
PlaybackState.PAUSED -> {
|
||||
R.drawable.media_state_play to R.string.mozac_feature_media_notification_action_play
|
||||
}
|
||||
PlaybackState.PLAYING -> {
|
||||
R.drawable.media_state_pause to R.string.mozac_feature_media_notification_action_pause
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
val drawable = AppCompatResources.getDrawable(LocalContext.current, icon)
|
||||
// Follow up ticket https://github.com/mozilla-mobile/fenix/issues/25774
|
||||
Image(
|
||||
painter = rememberDrawablePainter(drawable = drawable),
|
||||
contentDescription = stringResource(contentDescription),
|
||||
modifier = modifier.clickable { onMediaIconClicked(tab) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
private fun ImagePreview() {
|
||||
FirefoxTheme(theme = Theme.getTheme(isPrivate = false)) {
|
||||
MediaImage(
|
||||
tab = createTab(url = "https://mozilla.com"),
|
||||
onMediaIconClicked = {},
|
||||
modifier = Modifier
|
||||
.height(100.dp)
|
||||
.width(200.dp)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
/* 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.compose.tabstray
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.ThumbnailCard
|
||||
import org.mozilla.fenix.ext.toShortUrl
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
import org.mozilla.fenix.theme.Theme
|
||||
|
||||
/**
|
||||
* List item used to display a tab that supports clicks,
|
||||
* long clicks, multiselection, and media controls.
|
||||
*
|
||||
* @param tab The given tab to be render as view a list item.
|
||||
* @param isSelected Indicates if the item should be render as selected.
|
||||
* @param multiSelectionEnabled Indicates if the item should be render with multi selection options,
|
||||
* enabled.
|
||||
* @param multiSelectionSelected Indicates if the item should be render as multi selection selected
|
||||
* option.
|
||||
* @param onCloseClick Callback to handle the click event of the close button.
|
||||
* @param onMediaClick Callback to handle when the media item is clicked.
|
||||
* @param onClick Callback to handle when item is clicked.
|
||||
* @param onLongClick Callback to handle when item is long clicked.
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@Suppress("MagicNumber")
|
||||
fun TabListItem(
|
||||
tab: TabSessionState,
|
||||
isSelected: Boolean = false,
|
||||
multiSelectionEnabled: Boolean = false,
|
||||
multiSelectionSelected: Boolean = false,
|
||||
onCloseClick: (tab: TabSessionState) -> Unit,
|
||||
onMediaClick: (tab: TabSessionState) -> Unit,
|
||||
onClick: (tab: TabSessionState) -> Unit,
|
||||
onLongClick: (tab: TabSessionState) -> Unit,
|
||||
) {
|
||||
|
||||
val contentBackgroundColor = if (isSelected) {
|
||||
FirefoxTheme.colors.layerAccentNonOpaque
|
||||
} else {
|
||||
FirefoxTheme.colors.layer1
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(contentBackgroundColor)
|
||||
.combinedClickable(
|
||||
onLongClick = { onLongClick(tab) },
|
||||
onClick = { onClick(tab) }
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Thumbnail(
|
||||
tab = tab,
|
||||
multiSelectionEnabled = multiSelectionEnabled,
|
||||
isSelected = multiSelectionSelected,
|
||||
onMediaIconClicked = { onMediaClick(it) }
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.weight(weight = 1f)
|
||||
) {
|
||||
Text(
|
||||
text = tab.content.title,
|
||||
fontSize = 16.sp,
|
||||
maxLines = 2,
|
||||
color = FirefoxTheme.colors.textPrimary,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = tab.content.url.toShortUrl(),
|
||||
fontSize = 12.sp,
|
||||
color = FirefoxTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
|
||||
if (!multiSelectionEnabled) {
|
||||
IconButton(
|
||||
onClick = { onCloseClick(tab) },
|
||||
modifier = Modifier.size(size = 24.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.mozac_ic_close),
|
||||
contentDescription = stringResource(
|
||||
id = R.string.close_tab_title,
|
||||
tab.content.title
|
||||
),
|
||||
tint = FirefoxTheme.colors.iconPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Thumbnail(
|
||||
tab: TabSessionState,
|
||||
multiSelectionEnabled: Boolean,
|
||||
isSelected: Boolean,
|
||||
onMediaIconClicked: ((TabSessionState) -> Unit)
|
||||
) {
|
||||
Box {
|
||||
ThumbnailCard(
|
||||
url = tab.content.url,
|
||||
key = tab.id,
|
||||
modifier = Modifier.size(width = 92.dp, height = 72.dp),
|
||||
contentDescription = stringResource(id = R.string.mozac_browser_tabstray_open_tab),
|
||||
)
|
||||
|
||||
if (isSelected) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.size(size = 40.dp)
|
||||
.align(alignment = Alignment.Center),
|
||||
shape = CircleShape,
|
||||
backgroundColor = FirefoxTheme.colors.layerAccent,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.mozac_ic_check),
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.padding(all = 8.dp),
|
||||
contentDescription = null,
|
||||
tint = colorResource(id = R.color.mozac_ui_icons_fill)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!multiSelectionEnabled) {
|
||||
MediaImage(
|
||||
tab = tab,
|
||||
onMediaIconClicked = onMediaIconClicked,
|
||||
modifier = Modifier.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
private fun TabListItemPreview() {
|
||||
FirefoxTheme(theme = Theme.getTheme(isPrivate = false)) {
|
||||
TabListItem(
|
||||
tab = createTab(url = "www.mozilla.com", title = "Mozilla"),
|
||||
onCloseClick = {},
|
||||
onMediaClick = {},
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
private fun SelectedTabListItemPreview() {
|
||||
FirefoxTheme(theme = Theme.getTheme(isPrivate = false)) {
|
||||
TabListItem(
|
||||
tab = createTab(url = "www.mozilla.com", title = "Mozilla"),
|
||||
onCloseClick = {},
|
||||
onMediaClick = {},
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
multiSelectionEnabled = true,
|
||||
multiSelectionSelected = true,
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/* 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.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewTreeLifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import androidx.savedstate.ViewTreeSavedStateRegistryOwner
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.tabstray.SelectableTabViewHolder
|
||||
import org.mozilla.fenix.compose.ComposeViewHolder
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* [RecyclerView.ViewHolder] used for Jetpack Compose UI content .
|
||||
*
|
||||
* @param composeView [ComposeView] which will be populated with Jetpack Compose UI content.
|
||||
* @param viewLifecycleOwner [LifecycleOwner] life cycle owner for the view.
|
||||
*/
|
||||
abstract class ComposeAbstractTabViewHolder(
|
||||
private val composeView: ComposeView,
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : SelectableTabViewHolder(composeView) {
|
||||
|
||||
/**
|
||||
* Composable that contains the content for a specific [ComposeViewHolder] implementation.
|
||||
*/
|
||||
@Composable
|
||||
abstract fun Content(tab: TabSessionState)
|
||||
|
||||
/**
|
||||
* Binds the a composable to the [composeView].
|
||||
*/
|
||||
fun bind(tab: TabSessionState) {
|
||||
composeView.setContent {
|
||||
FirefoxTheme {
|
||||
Content(tab)
|
||||
}
|
||||
}
|
||||
|
||||
ViewTreeLifecycleOwner.set(composeView, viewLifecycleOwner)
|
||||
ViewTreeSavedStateRegistryOwner.set(
|
||||
composeView,
|
||||
viewLifecycleOwner as SavedStateRegistryOwner
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
/* 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.compose
|
||||
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.tabstray.TabsTray
|
||||
import mozilla.components.browser.tabstray.TabsTrayStyling
|
||||
import mozilla.components.lib.state.ext.observeAsComposableState
|
||||
import org.mozilla.fenix.compose.tabstray.TabListItem
|
||||
import org.mozilla.fenix.selection.SelectionHolder
|
||||
import org.mozilla.fenix.tabstray.TabsTrayState
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
|
||||
|
||||
/**
|
||||
* A Compose ViewHolder implementation for "tab" items with list layout.
|
||||
*
|
||||
* @param interactor [BrowserTrayInteractor] handling tabs interactions in a tab tray.
|
||||
* @param tabsTrayStore [TabsTrayStore] containing the complete state of tabs tray and methods to update that.
|
||||
* @param selectionHolder [SelectionHolder]<[TabSessionState]> for helping with selecting
|
||||
* any number of displayed [TabSessionState]s.
|
||||
* @param composeItemView that displays a "tab".
|
||||
* @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting.
|
||||
* @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to.
|
||||
*/
|
||||
class ComposeListViewHolder(
|
||||
private val interactor: BrowserTrayInteractor,
|
||||
private val tabsTrayStore: TabsTrayStore,
|
||||
private val selectionHolder: SelectionHolder<TabSessionState>? = null,
|
||||
composeItemView: ComposeView,
|
||||
private val featureName: String,
|
||||
viewLifecycleOwner: LifecycleOwner,
|
||||
) : ComposeAbstractTabViewHolder(composeItemView, viewLifecycleOwner) {
|
||||
|
||||
private var delegate: TabsTray.Delegate? = null
|
||||
|
||||
override var tab: TabSessionState? = null
|
||||
private val isMultiSelectionSelected = MutableStateFlow(false)
|
||||
private val isSelectedTab = MutableStateFlow(false)
|
||||
|
||||
override fun bind(
|
||||
tab: TabSessionState,
|
||||
isSelected: Boolean,
|
||||
styling: TabsTrayStyling,
|
||||
delegate: TabsTray.Delegate
|
||||
) {
|
||||
this.tab = tab
|
||||
this.delegate = delegate
|
||||
isSelectedTab.value = isSelected
|
||||
bind(tab)
|
||||
}
|
||||
|
||||
override fun updateSelectedTabIndicator(showAsSelected: Boolean) {
|
||||
isSelectedTab.value = showAsSelected
|
||||
}
|
||||
|
||||
override fun showTabIsMultiSelectEnabled(selectedMaskView: View?, isSelected: Boolean) {
|
||||
isMultiSelectionSelected.value = isSelected
|
||||
}
|
||||
|
||||
private fun onCloseClicked(tab: TabSessionState) {
|
||||
delegate?.onTabClosed(tab, featureName)
|
||||
}
|
||||
|
||||
private fun onClick(tab: TabSessionState) {
|
||||
val holder = selectionHolder
|
||||
if (holder != null) {
|
||||
interactor.onMultiSelectClicked(tab, holder, featureName)
|
||||
} else {
|
||||
interactor.onTabSelected(tab, featureName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLongClick(tab: TabSessionState) {
|
||||
val holder = selectionHolder ?: return
|
||||
interactor.onLongClicked(tab, holder)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(tab: TabSessionState) {
|
||||
val multiSelectionEnabled = tabsTrayStore.observeAsComposableState {
|
||||
state ->
|
||||
state.mode is TabsTrayState.Mode.Select
|
||||
}.value ?: false
|
||||
val isSelectedTabState by isSelectedTab.collectAsState()
|
||||
val multiSelectionSelected by isMultiSelectionSelected.collectAsState()
|
||||
|
||||
TabListItem(
|
||||
tab = tab,
|
||||
isSelected = isSelectedTabState,
|
||||
multiSelectionEnabled = multiSelectionEnabled,
|
||||
multiSelectionSelected = multiSelectionSelected,
|
||||
onCloseClick = ::onCloseClicked,
|
||||
onMediaClick = interactor::onMediaClicked,
|
||||
onClick = ::onClick,
|
||||
onLongClick = ::onLongClick,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val LAYOUT_ID = View.generateViewId()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue