mirror of
https://github.com/AllanWang/Frost-for-Facebook.git
synced 2024-09-19 23:21:34 +02:00
Merge pull request #1949 from AllanWang/tab-selector
This commit is contained in:
commit
bc07e6ec9d
@ -63,7 +63,9 @@ class FrostApp : Application() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
MainScope().launch { setup() }
|
MainScope().launch {
|
||||||
|
// setup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun setup() {
|
private suspend fun setup() {
|
||||||
|
@ -18,6 +18,7 @@ package com.pitchedapps.frost.compose
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
@ -27,16 +28,15 @@ import androidx.compose.material3.lightColorScheme
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
/** Main Frost compose theme. */
|
/** Main Frost compose theme. */
|
||||||
@Composable
|
@Composable
|
||||||
fun FrostTheme(
|
fun FrostTheme(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
isDarkTheme: Boolean = isSystemInDarkTheme(),
|
isDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
isDynamicColor: Boolean = true,
|
isDynamicColor: Boolean = true,
|
||||||
transparent: Boolean = true,
|
transparent: Boolean = true,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@ -57,8 +57,8 @@ fun FrostTheme(
|
|||||||
|
|
||||||
MaterialTheme(colorScheme = colorScheme) {
|
MaterialTheme(colorScheme = colorScheme) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier.fillMaxSize(),
|
||||||
color = if (transparent) Color.Transparent else MaterialTheme.colorScheme.surface,
|
// color = if (transparent) Color.Transparent else MaterialTheme.colorScheme.surface,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.compose.draggable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.layout.positionInWindow
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import com.pitchedapps.frost.ext.toDpSize
|
||||||
|
import com.pitchedapps.frost.ext.toIntOffset
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Resources:
|
||||||
|
*
|
||||||
|
* https://blog.canopas.com/android-drag-and-drop-ui-element-in-jetpack-compose-14922073b3f1
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for drag interactions.
|
||||||
|
*
|
||||||
|
* This must hold all drag and drop targets.
|
||||||
|
*
|
||||||
|
* If a composable cannot be used, any container can add the [Modifier.dragContainer] call, and have
|
||||||
|
* a [DraggingContents] positioned in the same window space (ie in a Box, or with the appropriate
|
||||||
|
* offsets to match).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T> DragContainer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
draggableState: DraggableState<T>,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier.dragContainer(draggableState)) {
|
||||||
|
content()
|
||||||
|
|
||||||
|
DraggingContents(draggableState = draggableState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drag container modifier.
|
||||||
|
*
|
||||||
|
* Containers must hold all drag and drop targets, along with [DraggingContents].
|
||||||
|
*/
|
||||||
|
fun Modifier.dragContainer(draggableState: DraggableState<*>): Modifier {
|
||||||
|
return onGloballyPositioned { draggableState.windowPosition = it.positionInWindow() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drag target modifier.
|
||||||
|
*
|
||||||
|
* This should be applied to the composable that will be dragged. The modifier will capture
|
||||||
|
* positions and hide (alpha 0) the target when dragging.
|
||||||
|
*/
|
||||||
|
fun Modifier.dragTarget(dragTargetState: DragTargetState<*>): Modifier {
|
||||||
|
return onGloballyPositioned {
|
||||||
|
dragTargetState.windowPosition = it.positionInWindow()
|
||||||
|
dragTargetState.size = it.size
|
||||||
|
}
|
||||||
|
.pointerInput(dragTargetState) {
|
||||||
|
detectDragGesturesAfterLongPress(
|
||||||
|
onDragStart = { dragTargetState.onDragStart() },
|
||||||
|
onDrag = { _, offset -> dragTargetState.onDrag(offset) },
|
||||||
|
onDragEnd = { dragTargetState.onDragEnd() },
|
||||||
|
onDragCancel = { dragTargetState.onDragEnd() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// We still need to draw to track size changes
|
||||||
|
.alpha(if (dragTargetState.isDragging) 0f else 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop target modifier.
|
||||||
|
*
|
||||||
|
* This should be applied to targets that capture drag targets. The modifier will listen to
|
||||||
|
* composable bounds.
|
||||||
|
*/
|
||||||
|
fun <T> Modifier.dropTarget(dropTargetState: DropTargetState<T>): Modifier {
|
||||||
|
return onGloballyPositioned { dropTargetState.bounds = it.boundsInWindow() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draggable content.
|
||||||
|
*
|
||||||
|
* Provides composition for all dragging targets.
|
||||||
|
*
|
||||||
|
* For composing, we take the position provided by the dragging target. We also account for the
|
||||||
|
* target size, as we want to maintain the same bounds here. As an example, the target can have
|
||||||
|
* fillMaxWidth in a grid, but would have a full parent width here without the sizing constraints.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T> DraggingContents(draggableState: DraggableState<T>) {
|
||||||
|
for (target in draggableState.targets) {
|
||||||
|
DraggingContent(draggableState = draggableState, target = target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun <T> DraggingContent(draggableState: DraggableState<T>, target: DragTargetState<T>) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier.size(target.size.toDpSize(density)).offset {
|
||||||
|
(target.dragPosition - draggableState.windowPosition).toIntOffset()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
target.dragComposable()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,268 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.compose.draggable
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
|
||||||
|
/** Callback when a drag completes on top of a target. */
|
||||||
|
fun interface OnDrop<T> {
|
||||||
|
fun onDrop(dragTarget: String, dragData: T, dropTarget: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create draggable state, which will store and create all target states. */
|
||||||
|
@Composable
|
||||||
|
fun <T> rememberDraggableState(onDrop: OnDrop<T>): DraggableState<T> {
|
||||||
|
// State must be remembered without keys, or else updates will clear draggable data
|
||||||
|
val state = remember { DraggableStateImpl(onDrop) }
|
||||||
|
|
||||||
|
LaunchedEffect(onDrop) { state.onDrop = onDrop }
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parent draggable state.
|
||||||
|
*
|
||||||
|
* Allows for drag and drop target state creation via [dragTarget] and [dragTarget].
|
||||||
|
*
|
||||||
|
* All public getters are states, and will trigger recomposition automatically.
|
||||||
|
*/
|
||||||
|
interface DraggableState<T> {
|
||||||
|
|
||||||
|
var windowPosition: Offset
|
||||||
|
|
||||||
|
val targets: Collection<DragTargetState<T>>
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberDragTarget(key: String, data: T, content: @Composable () -> Unit): DragTargetState<T>
|
||||||
|
|
||||||
|
@Composable fun rememberDropTarget(key: String): DropTargetState<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DragTargetState<T> {
|
||||||
|
/**
|
||||||
|
* Identifier for drag target.
|
||||||
|
*
|
||||||
|
* This is not necessarily unique, but only unique drag target keys may be dragged at a time. Ie,
|
||||||
|
* if a group of drag targets with the same key is dragging, only the first one will activate.
|
||||||
|
*/
|
||||||
|
val key: String
|
||||||
|
|
||||||
|
/** Data associated with this drag target. */
|
||||||
|
val data: T
|
||||||
|
|
||||||
|
/** True if target is actively being dragged. */
|
||||||
|
val isDragging: Boolean
|
||||||
|
|
||||||
|
/** Window position for target. */
|
||||||
|
var windowPosition: Offset
|
||||||
|
|
||||||
|
/** Size bounds for target. */
|
||||||
|
var size: IntSize
|
||||||
|
|
||||||
|
/** Drag position relative to window. This is only valid while [isDragging]. */
|
||||||
|
val dragPosition: Offset
|
||||||
|
|
||||||
|
/** Composable to use when dragging. */
|
||||||
|
val dragComposable: @Composable () -> Unit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin drag for target.
|
||||||
|
*
|
||||||
|
* If there is another target with the same [key] being dragged, we will ignore this request.
|
||||||
|
*
|
||||||
|
* Returns true if the request is accepted. It is the caller's responsibility to not propagate
|
||||||
|
* drag events if the request is denied.
|
||||||
|
*/
|
||||||
|
fun onDragStart()
|
||||||
|
|
||||||
|
fun onDrag(offset: Offset)
|
||||||
|
|
||||||
|
fun onDragEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DropTargetState<T> {
|
||||||
|
/**
|
||||||
|
* True if there is a drag target above this target.
|
||||||
|
*
|
||||||
|
* [hoverData] will be populated when true.
|
||||||
|
*/
|
||||||
|
val isHovered: Boolean
|
||||||
|
|
||||||
|
/** Associated [DragTargetState.data], or null when not [isHovered] */
|
||||||
|
val hoverData: T?
|
||||||
|
|
||||||
|
/** Bounds of the drop target composable */
|
||||||
|
var bounds: Rect
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DraggableStateImpl<T>(var onDrop: OnDrop<T>) : DraggableState<T> {
|
||||||
|
|
||||||
|
override var windowPosition: Offset by mutableStateOf(Offset.Zero)
|
||||||
|
|
||||||
|
val activeDragTargets = mutableStateMapOf<String, DragTargetStateImpl<T>>()
|
||||||
|
|
||||||
|
private val dropTargets = mutableStateMapOf<String, DropTargetStateImpl<T>>()
|
||||||
|
|
||||||
|
override val targets: Collection<DragTargetState<T>>
|
||||||
|
get() = activeDragTargets.values
|
||||||
|
|
||||||
|
fun cleanUpDrag(dragTarget: DragTargetStateImpl<T>) {
|
||||||
|
val dragKey = dragTarget.key
|
||||||
|
for ((dropKey, dropTarget) in dropTargets) {
|
||||||
|
if (dropTarget.hoverKey == dragKey) {
|
||||||
|
onDrop.onDrop(dragTarget = dragKey, dragData = dragTarget.data, dropTarget = dropKey)
|
||||||
|
|
||||||
|
setHover(dragKey = null, dropKey)
|
||||||
|
// Check other drag targets in case one meets drag requirements
|
||||||
|
checkForDrop(dropKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun rememberDragTarget(
|
||||||
|
key: String,
|
||||||
|
data: T,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
): DragTargetState<T> {
|
||||||
|
val target =
|
||||||
|
remember(key, data, content, this) {
|
||||||
|
DragTargetStateImpl(key = key, data = data, draggableState = this, dragComposable = content)
|
||||||
|
}
|
||||||
|
DisposableEffect(target) { onDispose { activeDragTargets.remove(key) } }
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun rememberDropTarget(key: String): DropTargetState<T> {
|
||||||
|
val target = remember(key, this) { DropTargetStateImpl(key, this) }
|
||||||
|
DisposableEffect(target) {
|
||||||
|
dropTargets[key] = target
|
||||||
|
|
||||||
|
onDispose { dropTargets.remove(key) }
|
||||||
|
}
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setHover(dragKey: String?, dropKey: String) {
|
||||||
|
val dropTarget = dropTargets[dropKey] ?: return
|
||||||
|
// Safety check; we only want to register active keys
|
||||||
|
val dragTarget = if (dragKey != null) activeDragTargets[dragKey] else null
|
||||||
|
dropTarget.hoverKey = dragTarget?.key
|
||||||
|
dropTarget.hoverData = dragTarget?.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if drag target exists and is within bounds */
|
||||||
|
private fun DropTargetStateImpl<T>.hasValidDragTarget(): Boolean {
|
||||||
|
val currentKey = hoverKey ?: return false // no target
|
||||||
|
val dragTarget = activeDragTargets[currentKey] ?: return false // target not valid
|
||||||
|
return dragTarget.within(bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if drag target fits in drop */
|
||||||
|
fun checkForDrop(dropKey: String) {
|
||||||
|
val dropTarget = dropTargets[dropKey] ?: return
|
||||||
|
val bounds = dropTarget.bounds
|
||||||
|
if (dropTarget.hasValidDragTarget()) return
|
||||||
|
|
||||||
|
// Find first target that matches
|
||||||
|
val dragKey = activeDragTargets.entries.firstOrNull { it.value.within(bounds) }?.key
|
||||||
|
setHover(dragKey = dragKey, dropKey = dropKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check drops for drag target fit */
|
||||||
|
fun checkForDrag(dragKey: String) {
|
||||||
|
val dragTarget = activeDragTargets[dragKey] ?: return
|
||||||
|
for ((dropKey, dropTarget) in dropTargets) {
|
||||||
|
// Do not override targets that are valid
|
||||||
|
if (dropTarget.hasValidDragTarget()) continue
|
||||||
|
if (dragTarget.within(dropTarget.bounds)) {
|
||||||
|
setHover(dragKey = dragKey, dropKey = dropKey)
|
||||||
|
} else if (dropTarget.hoverKey == dragKey) {
|
||||||
|
setHover(dragKey = null, dropKey = dropKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DragTargetStateImpl<*>?.within(bounds: Rect): Boolean {
|
||||||
|
if (this == null) return false
|
||||||
|
val center = dragPosition + Offset(size.width * 0.5f, size.height * 0.5f)
|
||||||
|
return bounds.contains(center)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** State for individual dragging target. */
|
||||||
|
private class DragTargetStateImpl<T>(
|
||||||
|
override val key: String,
|
||||||
|
override val data: T,
|
||||||
|
val draggableState: DraggableStateImpl<T>,
|
||||||
|
override val dragComposable: @Composable () -> Unit,
|
||||||
|
) : DragTargetState<T> {
|
||||||
|
override var isDragging by mutableStateOf(false)
|
||||||
|
override var windowPosition = Offset.Zero
|
||||||
|
override var dragPosition by mutableStateOf(Offset.Zero)
|
||||||
|
override var size: IntSize by mutableStateOf(IntSize.Zero)
|
||||||
|
|
||||||
|
override fun onDragStart() {
|
||||||
|
// Another drag target is being used; ignore
|
||||||
|
dragPosition = windowPosition
|
||||||
|
if (key in draggableState.activeDragTargets) return
|
||||||
|
draggableState.activeDragTargets[key] = this
|
||||||
|
isDragging = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrag(offset: Offset) {
|
||||||
|
if (isDragging) {
|
||||||
|
dragPosition += offset
|
||||||
|
draggableState.checkForDrag(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDragEnd() {
|
||||||
|
if (!isDragging) return
|
||||||
|
draggableState.activeDragTargets.remove(key)
|
||||||
|
draggableState.cleanUpDrag(this)
|
||||||
|
isDragging = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DropTargetStateImpl<T>(
|
||||||
|
private val key: String,
|
||||||
|
private val draggableState: DraggableStateImpl<T>
|
||||||
|
) : DropTargetState<T> {
|
||||||
|
var hoverKey: String? by mutableStateOf(null)
|
||||||
|
override var hoverData: T? by mutableStateOf(null)
|
||||||
|
override var bounds: Rect = Rect.Zero
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
draggableState.checkForDrop(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isHovered
|
||||||
|
get() = hoverKey != null
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.compose.effects
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.platform.debugInspectorInfo
|
||||||
|
import androidx.compose.ui.platform.inspectable
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State for tracking shaking animation
|
||||||
|
*
|
||||||
|
* Note: Used some other material states as reference. This however will recompose the function
|
||||||
|
* holding the state during shakes.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberShakeState(): ShakeState {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val state = remember(scope) { ShakeState(scope) }
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShakeState
|
||||||
|
internal constructor(
|
||||||
|
private val animationScope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val rotationAnimatable = Animatable(0f)
|
||||||
|
|
||||||
|
internal val rotation
|
||||||
|
get() = rotationAnimatable.value
|
||||||
|
|
||||||
|
fun shake() {
|
||||||
|
animationScope.launch {
|
||||||
|
rotationAnimatable.stop()
|
||||||
|
rotationAnimatable.animateTo(
|
||||||
|
0f,
|
||||||
|
initialVelocity = 200f,
|
||||||
|
animationSpec =
|
||||||
|
spring(
|
||||||
|
dampingRatio = 0.3f,
|
||||||
|
stiffness = 200f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.shake(state: ShakeState, enabled: Boolean = true) =
|
||||||
|
inspectable(
|
||||||
|
inspectorInfo =
|
||||||
|
debugInspectorInfo {
|
||||||
|
name = "shake"
|
||||||
|
properties["enabled"] = enabled
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Modifier.rotate(state.rotation)
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.ext
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Modifier.thenIf(condition: Boolean, action: @Composable () -> Modifier): Modifier =
|
||||||
|
if (condition) then(action()) else this
|
||||||
|
|
||||||
|
fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
|
||||||
|
|
||||||
|
fun IntSize.toDpSize(density: Density): DpSize {
|
||||||
|
return with(density) { DpSize(width.toDp(), height.toDp()) }
|
||||||
|
}
|
@ -41,6 +41,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
import com.pitchedapps.frost.ext.WebTargetId
|
import com.pitchedapps.frost.ext.WebTargetId
|
||||||
import com.pitchedapps.frost.main.MainTabItem
|
import com.pitchedapps.frost.main.MainTabItem
|
||||||
|
import com.pitchedapps.frost.tabselector.TabData
|
||||||
import compose.icons.FontAwesomeIcons
|
import compose.icons.FontAwesomeIcons
|
||||||
import compose.icons.fontawesomeicons.Solid
|
import compose.icons.fontawesomeicons.Solid
|
||||||
import compose.icons.fontawesomeicons.solid.GlobeAmericas
|
import compose.icons.fontawesomeicons.solid.GlobeAmericas
|
||||||
@ -127,6 +128,13 @@ fun FbItem.tab(context: Context, id: WebTargetId): MainTabItem =
|
|||||||
url = url,
|
url = url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun FbItem.tab(context: Context): TabData =
|
||||||
|
TabData(
|
||||||
|
key = key,
|
||||||
|
title = context.getString(titleId),
|
||||||
|
icon = icon,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
/// ** Note that this url only works if a query (?q=) is provided */
|
/// ** Note that this url only works if a query (?q=) is provided */
|
||||||
// _SEARCH("search", R.string.kau_search, GoogleMaterial.Icon.gmd_search, "search/top"),
|
// _SEARCH("search", R.string.kau_search, GoogleMaterial.Icon.gmd_search, "search/top"),
|
||||||
|
@ -23,10 +23,10 @@ import androidx.core.view.WindowCompat
|
|||||||
import com.google.common.flogger.FluentLogger
|
import com.google.common.flogger.FluentLogger
|
||||||
import com.pitchedapps.frost.R
|
import com.pitchedapps.frost.R
|
||||||
import com.pitchedapps.frost.compose.FrostTheme
|
import com.pitchedapps.frost.compose.FrostTheme
|
||||||
|
import com.pitchedapps.frost.tabselector.TabSelectorScreen
|
||||||
import com.pitchedapps.frost.web.state.FrostWebStore
|
import com.pitchedapps.frost.web.state.FrostWebStore
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import mozilla.components.lib.state.ext.observeAsState
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main activity.
|
* Main activity.
|
||||||
@ -52,11 +52,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
// tabs = tabs,
|
// tabs = tabs,
|
||||||
// )
|
// )
|
||||||
|
|
||||||
val tabs =
|
TabSelectorScreen()
|
||||||
store.observeAsState(initialValue = null) { it.homeTabs.map { it.tab } }.value
|
|
||||||
?: return@FrostTheme
|
|
||||||
|
|
||||||
MainScreenWebView(homeTabs = tabs)
|
// MainScreenWebView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package com.pitchedapps.frost.main
|
|||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
@ -42,6 +43,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.pitchedapps.frost.compose.webview.FrostWebCompose
|
import com.pitchedapps.frost.compose.webview.FrostWebCompose
|
||||||
import com.pitchedapps.frost.ext.WebTargetId
|
import com.pitchedapps.frost.ext.WebTargetId
|
||||||
@ -52,9 +54,12 @@ import kotlinx.coroutines.launch
|
|||||||
import mozilla.components.lib.state.ext.observeAsState
|
import mozilla.components.lib.state.ext.observeAsState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreenWebView(modifier: Modifier = Modifier, homeTabs: List<MainTabItem>) {
|
fun MainScreenWebView(modifier: Modifier = Modifier) {
|
||||||
val vm: MainScreenViewModel = viewModel()
|
val vm: MainScreenViewModel = viewModel()
|
||||||
|
|
||||||
|
val homeTabs =
|
||||||
|
vm.store.observeAsState(initialValue = null) { it.homeTabs.map { it.tab } }.value ?: return
|
||||||
|
|
||||||
val selectedHomeTab by vm.store.observeAsState(initialValue = null) { it.selectedHomeTab }
|
val selectedHomeTab by vm.store.observeAsState(initialValue = null) { it.selectedHomeTab }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -95,7 +100,13 @@ fun MainBottomBar(
|
|||||||
NavigationBar(modifier = modifier) {
|
NavigationBar(modifier = modifier) {
|
||||||
items.forEach { item ->
|
items.forEach { item ->
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
icon = { Icon(item.icon, contentDescription = item.title) },
|
icon = {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
imageVector = item.icon,
|
||||||
|
contentDescription = item.title,
|
||||||
|
)
|
||||||
|
},
|
||||||
selected = selectedTab == item.id,
|
selected = selectedTab == item.id,
|
||||||
onClick = { onSelect(item.id) },
|
onClick = { onSelect(item.id) },
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.tabselector
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
/** Option for main screen tabs */
|
||||||
|
@Immutable
|
||||||
|
data class TabData(
|
||||||
|
val key: String,
|
||||||
|
val title: String,
|
||||||
|
val icon: ImageVector,
|
||||||
|
)
|
@ -0,0 +1,263 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Allan Wang
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.pitchedapps.frost.tabselector
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColor
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.indication
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.ripple.LocalRippleTheme
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.pitchedapps.frost.compose.draggable.DragContainer
|
||||||
|
import com.pitchedapps.frost.compose.draggable.DraggableState
|
||||||
|
import com.pitchedapps.frost.compose.draggable.dragTarget
|
||||||
|
import com.pitchedapps.frost.compose.draggable.dropTarget
|
||||||
|
import com.pitchedapps.frost.compose.draggable.rememberDraggableState
|
||||||
|
import com.pitchedapps.frost.compose.effects.rememberShakeState
|
||||||
|
import com.pitchedapps.frost.compose.effects.shake
|
||||||
|
import com.pitchedapps.frost.facebook.FbItem
|
||||||
|
import com.pitchedapps.frost.facebook.tab
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TabSelectorScreen(modifier: Modifier = Modifier) {
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var selected: List<TabData> by remember {
|
||||||
|
mutableStateOf(FbItem.defaults().map { it.tab(context) })
|
||||||
|
}
|
||||||
|
|
||||||
|
val options: Map<String, TabData> = remember {
|
||||||
|
FbItem.values().associateBy({ it.key }, { it.tab(context) })
|
||||||
|
}
|
||||||
|
|
||||||
|
val unselected = remember(selected) { options.values - selected.toSet() }
|
||||||
|
|
||||||
|
TabSelector(
|
||||||
|
modifier = modifier,
|
||||||
|
selected = selected,
|
||||||
|
unselected = unselected,
|
||||||
|
onSelect = { selected = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TabSelector(
|
||||||
|
modifier: Modifier,
|
||||||
|
selected: List<TabData>,
|
||||||
|
unselected: List<TabData>,
|
||||||
|
onSelect: (List<TabData>) -> Unit
|
||||||
|
) {
|
||||||
|
val draggableState =
|
||||||
|
rememberDraggableState<TabData>(
|
||||||
|
onDrop = { _, dragData, dropTarget ->
|
||||||
|
onSelect(
|
||||||
|
selected.map { if (it.key == dropTarget) dragData else it },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
DragContainer(modifier = modifier, draggableState = draggableState) {
|
||||||
|
Column(modifier = Modifier.statusBarsPadding()) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
columns = GridCells.Fixed(4),
|
||||||
|
) {
|
||||||
|
items(unselected, key = { it.key }) { data ->
|
||||||
|
val dragTargetState =
|
||||||
|
draggableState.rememberDragTarget(key = data.key, data = data) {
|
||||||
|
DraggingTabItem(data = data)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shakeState = rememberShakeState()
|
||||||
|
|
||||||
|
TabItem(
|
||||||
|
modifier =
|
||||||
|
Modifier.animateItemPlacement().dragTarget(dragTargetState).shake(shakeState),
|
||||||
|
data = data,
|
||||||
|
onClick = { shakeState.shake() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TabBottomBar(
|
||||||
|
modifier = Modifier.navigationBarsPadding(),
|
||||||
|
draggableState = draggableState,
|
||||||
|
items = selected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TabBottomBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
draggableState: DraggableState<TabData>,
|
||||||
|
items: List<TabData>
|
||||||
|
) {
|
||||||
|
NavigationBar(modifier = modifier) {
|
||||||
|
items.forEach { item ->
|
||||||
|
val dropTargetState = draggableState.rememberDropTarget(item.key)
|
||||||
|
|
||||||
|
val alpha by
|
||||||
|
animateFloatAsState(
|
||||||
|
targetValue = if (!dropTargetState.isHovered) 1f else 0.3f,
|
||||||
|
label = "Nav Item Alpha",
|
||||||
|
)
|
||||||
|
|
||||||
|
NavigationBarItem(
|
||||||
|
modifier = Modifier.dropTarget(dropTargetState),
|
||||||
|
icon = {
|
||||||
|
val iconItem = dropTargetState.hoverData ?: item
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(24.dp).alpha(alpha),
|
||||||
|
imageVector = iconItem.icon,
|
||||||
|
contentDescription = iconItem.title,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selected = false,
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Our dragging tab item is fairly different from the original, so we'll just make a copy to add the
|
||||||
|
* animations.
|
||||||
|
*
|
||||||
|
* Animations are done as one shot events from a default
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DraggingTabItem(
|
||||||
|
data: TabData,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
|
||||||
|
var startState by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) { startState = false }
|
||||||
|
|
||||||
|
val transition = updateTransition(targetState = startState, label = "One Shot")
|
||||||
|
|
||||||
|
val scale by transition.animateFloat(label = "Scale") { if (it) 1f else 1.3f }
|
||||||
|
|
||||||
|
// Same color as ripple values
|
||||||
|
val color by
|
||||||
|
transition.animateColor(label = "Background") {
|
||||||
|
if (it)
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant.copy(
|
||||||
|
LocalRippleTheme.current.rippleAlpha().pressedAlpha,
|
||||||
|
)
|
||||||
|
else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.85f)
|
||||||
|
}
|
||||||
|
|
||||||
|
TabItem(
|
||||||
|
modifier =
|
||||||
|
modifier.graphicsLayer {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
},
|
||||||
|
iconBackground = color,
|
||||||
|
labelAlpha = 0f,
|
||||||
|
data = data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TabItem(
|
||||||
|
data: TabData,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
iconBackground: Color = Color.Unspecified,
|
||||||
|
labelAlpha: Float = 1f,
|
||||||
|
onClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.clickable(onClick = onClick, interactionSource = interactionSource, indication = null)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier =
|
||||||
|
Modifier.clip(CircleShape)
|
||||||
|
.background(iconBackground)
|
||||||
|
.indication(
|
||||||
|
interactionSource,
|
||||||
|
rememberRipple(color = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
)
|
||||||
|
.padding(12.dp)
|
||||||
|
.size(24.dp),
|
||||||
|
imageVector = data.icon,
|
||||||
|
contentDescription = data.title,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
// Weird offset is to accommodate the icon background, which is only used when the label is
|
||||||
|
// hidden
|
||||||
|
modifier = Modifier.offset(y = (-4).dp).alpha(labelAlpha),
|
||||||
|
text = data.title,
|
||||||
|
minLines = 2,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user