更新登录UI
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 onebone <me@onebone.me>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||||
|
* OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aiosman.riderpro.ui.composables.toolbar
|
||||||
|
|
||||||
|
@RequiresOptIn(
|
||||||
|
message = "This is an experimental API of compose-collapsing-toolbar. Any declarations with " +
|
||||||
|
"the annotation might be removed or changed in some way without any notice.",
|
||||||
|
level = RequiresOptIn.Level.WARNING
|
||||||
|
)
|
||||||
|
@Target(
|
||||||
|
AnnotationTarget.FUNCTION,
|
||||||
|
AnnotationTarget.PROPERTY,
|
||||||
|
AnnotationTarget.CLASS
|
||||||
|
)
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class ExperimentalToolbarApi
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 onebone <me@onebone.me>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||||
|
* OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aiosman.riderpro.ui.composables.toolbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.Measurable
|
||||||
|
import androidx.compose.ui.layout.MeasurePolicy
|
||||||
|
import androidx.compose.ui.layout.MeasureResult
|
||||||
|
import androidx.compose.ui.layout.MeasureScope
|
||||||
|
import androidx.compose.ui.layout.ParentDataModifier
|
||||||
|
import androidx.compose.ui.layout.Placeable
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"Use AppBarContainer for naming consistency",
|
||||||
|
replaceWith = ReplaceWith(
|
||||||
|
"AppBarContainer(modifier, scrollStrategy, collapsingToolbarState, content)",
|
||||||
|
"me.onebone.toolbar"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun AppbarContainer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
scrollStrategy: ScrollStrategy,
|
||||||
|
collapsingToolbarState: CollapsingToolbarState,
|
||||||
|
content: @Composable AppbarContainerScope.() -> Unit
|
||||||
|
) {
|
||||||
|
AppBarContainer(
|
||||||
|
modifier = modifier,
|
||||||
|
scrollStrategy = scrollStrategy,
|
||||||
|
collapsingToolbarState = collapsingToolbarState,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
"AppBarContainer is replaced with CollapsingToolbarScaffold",
|
||||||
|
replaceWith = ReplaceWith(
|
||||||
|
"CollapsingToolbarScaffold",
|
||||||
|
"me.onebone.toolbar"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun AppBarContainer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
scrollStrategy: ScrollStrategy,
|
||||||
|
/** The state of a connected collapsing toolbar */
|
||||||
|
collapsingToolbarState: CollapsingToolbarState,
|
||||||
|
content: @Composable AppbarContainerScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val offsetY = remember { mutableStateOf(0) }
|
||||||
|
val flingBehavior = ScrollableDefaults.flingBehavior()
|
||||||
|
|
||||||
|
val (scope, measurePolicy) = remember(scrollStrategy, collapsingToolbarState) {
|
||||||
|
AppbarContainerScopeImpl(scrollStrategy.create(offsetY, collapsingToolbarState, flingBehavior)) to
|
||||||
|
AppbarMeasurePolicy(scrollStrategy, collapsingToolbarState, offsetY)
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout(
|
||||||
|
content = { scope.content() },
|
||||||
|
measurePolicy = measurePolicy,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppbarContainerScope {
|
||||||
|
fun Modifier.appBarBody(): Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AppbarContainerScopeImpl(
|
||||||
|
private val nestedScrollConnection: NestedScrollConnection
|
||||||
|
): AppbarContainerScope {
|
||||||
|
override fun Modifier.appBarBody(): Modifier {
|
||||||
|
return this
|
||||||
|
.then(AppBarBodyMarkerModifier)
|
||||||
|
.nestedScroll(nestedScrollConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object AppBarBodyMarkerModifier: ParentDataModifier {
|
||||||
|
override fun Density.modifyParentData(parentData: Any?): Any {
|
||||||
|
return AppBarBodyMarker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object AppBarBodyMarker
|
||||||
|
|
||||||
|
private class AppbarMeasurePolicy(
|
||||||
|
private val scrollStrategy: ScrollStrategy,
|
||||||
|
private val toolbarState: CollapsingToolbarState,
|
||||||
|
private val offsetY: State<Int>
|
||||||
|
): MeasurePolicy {
|
||||||
|
override fun MeasureScope.measure(
|
||||||
|
measurables: List<Measurable>,
|
||||||
|
constraints: Constraints
|
||||||
|
): MeasureResult {
|
||||||
|
var width = 0
|
||||||
|
var height = 0
|
||||||
|
|
||||||
|
var toolbarPlaceable: Placeable? = null
|
||||||
|
|
||||||
|
val nonToolbars = measurables.filter {
|
||||||
|
val data = it.parentData
|
||||||
|
if(data != AppBarBodyMarker) {
|
||||||
|
if(toolbarPlaceable != null)
|
||||||
|
throw IllegalStateException("There cannot exist multiple toolbars under single parent")
|
||||||
|
|
||||||
|
val placeable = it.measure(constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
minHeight = 0
|
||||||
|
))
|
||||||
|
width = max(width, placeable.width)
|
||||||
|
height = max(height, placeable.height)
|
||||||
|
|
||||||
|
toolbarPlaceable = placeable
|
||||||
|
|
||||||
|
false
|
||||||
|
}else{
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val placeables = nonToolbars.map { measurable ->
|
||||||
|
val childConstraints = if(scrollStrategy == ScrollStrategy.ExitUntilCollapsed) {
|
||||||
|
constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
minHeight = 0,
|
||||||
|
maxHeight = max(0, constraints.maxHeight - toolbarState.minHeight)
|
||||||
|
)
|
||||||
|
}else{
|
||||||
|
constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
minHeight = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val placeable = measurable.measure(childConstraints)
|
||||||
|
|
||||||
|
width = max(width, placeable.width)
|
||||||
|
height = max(height, placeable.height)
|
||||||
|
|
||||||
|
placeable
|
||||||
|
}
|
||||||
|
|
||||||
|
height += (toolbarPlaceable?.height ?: 0)
|
||||||
|
|
||||||
|
return layout(
|
||||||
|
width.coerceIn(constraints.minWidth, constraints.maxWidth),
|
||||||
|
height.coerceIn(constraints.minHeight, constraints.maxHeight)
|
||||||
|
) {
|
||||||
|
toolbarPlaceable?.place(x = 0, y = offsetY.value)
|
||||||
|
|
||||||
|
placeables.forEach { placeable ->
|
||||||
|
placeable.place(
|
||||||
|
x = 0,
|
||||||
|
y = offsetY.value + (toolbarPlaceable?.height ?: 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 onebone <me@onebone.me>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||||
|
* OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aiosman.riderpro.ui.composables.toolbar
|
||||||
|
|
||||||
|
import androidx.annotation.FloatRange
|
||||||
|
import androidx.compose.animation.core.AnimationState
|
||||||
|
import androidx.compose.animation.core.animateTo
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.MutatePriority
|
||||||
|
import androidx.compose.foundation.gestures.FlingBehavior
|
||||||
|
import androidx.compose.foundation.gestures.ScrollScope
|
||||||
|
import androidx.compose.foundation.gestures.ScrollableState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
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.clipToBounds
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.Measurable
|
||||||
|
import androidx.compose.ui.layout.MeasurePolicy
|
||||||
|
import androidx.compose.ui.layout.MeasureResult
|
||||||
|
import androidx.compose.ui.layout.MeasureScope
|
||||||
|
import androidx.compose.ui.layout.ParentDataModifier
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class CollapsingToolbarState(
|
||||||
|
initial: Int = Int.MAX_VALUE
|
||||||
|
): ScrollableState {
|
||||||
|
/**
|
||||||
|
* [height] indicates current height of the toolbar.
|
||||||
|
*/
|
||||||
|
var height: Int by mutableStateOf(initial)
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [minHeight] indicates the minimum height of the collapsing toolbar. The toolbar
|
||||||
|
* may collapse its height to [minHeight] but not smaller. This size is determined by
|
||||||
|
* the smallest child.
|
||||||
|
*/
|
||||||
|
var minHeight: Int
|
||||||
|
get() = minHeightState
|
||||||
|
internal set(value) {
|
||||||
|
minHeightState = value
|
||||||
|
|
||||||
|
if(height < value) {
|
||||||
|
height = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [maxHeight] indicates the maximum height of the collapsing toolbar. The toolbar
|
||||||
|
* may expand its height to [maxHeight] but not larger. This size is determined by
|
||||||
|
* the largest child.
|
||||||
|
*/
|
||||||
|
var maxHeight: Int
|
||||||
|
get() = maxHeightState
|
||||||
|
internal set(value) {
|
||||||
|
maxHeightState = value
|
||||||
|
|
||||||
|
if(value < height) {
|
||||||
|
height = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var maxHeightState by mutableStateOf(Int.MAX_VALUE)
|
||||||
|
private var minHeightState by mutableStateOf(0)
|
||||||
|
|
||||||
|
val progress: Float
|
||||||
|
@FloatRange(from = 0.0, to = 1.0)
|
||||||
|
get() =
|
||||||
|
if(minHeight == maxHeight) {
|
||||||
|
0f
|
||||||
|
}else{
|
||||||
|
((height - minHeight).toFloat() / (maxHeight - minHeight)).coerceIn(0f, 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val scrollableState = ScrollableState { value ->
|
||||||
|
val consume = if(value < 0) {
|
||||||
|
max(minHeight.toFloat() - height, value)
|
||||||
|
}else{
|
||||||
|
min(maxHeight.toFloat() - height, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
val current = consume + deferredConsumption
|
||||||
|
val currentInt = current.toInt()
|
||||||
|
|
||||||
|
if(current.absoluteValue > 0) {
|
||||||
|
height += currentInt
|
||||||
|
deferredConsumption = current - currentInt
|
||||||
|
}
|
||||||
|
|
||||||
|
consume
|
||||||
|
}
|
||||||
|
|
||||||
|
private var deferredConsumption: Float = 0f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return consumed scroll value is returned
|
||||||
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
message = "feedScroll() is deprecated, use dispatchRawDelta() instead.",
|
||||||
|
replaceWith = ReplaceWith("dispatchRawDelta(value)")
|
||||||
|
)
|
||||||
|
fun feedScroll(value: Float): Float = dispatchRawDelta(value)
|
||||||
|
|
||||||
|
@ExperimentalToolbarApi
|
||||||
|
suspend fun expand(duration: Int = 200) {
|
||||||
|
val anim = AnimationState(height.toFloat())
|
||||||
|
|
||||||
|
scroll {
|
||||||
|
var prev = anim.value
|
||||||
|
anim.animateTo(maxHeight.toFloat(), tween(duration)) {
|
||||||
|
scrollBy(value - prev)
|
||||||
|
prev = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalToolbarApi
|
||||||
|
suspend fun collapse(duration: Int = 200) {
|
||||||
|
val anim = AnimationState(height.toFloat())
|
||||||
|
|
||||||
|
scroll {
|
||||||
|
var prev = anim.value
|
||||||
|
anim.animateTo(minHeight.toFloat(), tween(duration)) {
|
||||||
|
scrollBy(value - prev)
|
||||||
|
prev = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Remaining velocity after fling
|
||||||
|
*/
|
||||||
|
suspend fun fling(flingBehavior: FlingBehavior, velocity: Float): Float {
|
||||||
|
var left = velocity
|
||||||
|
scroll {
|
||||||
|
with(flingBehavior) {
|
||||||
|
left = performFling(left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isScrollInProgress: Boolean
|
||||||
|
get() = scrollableState.isScrollInProgress
|
||||||
|
|
||||||
|
override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta)
|
||||||
|
|
||||||
|
override suspend fun scroll(
|
||||||
|
scrollPriority: MutatePriority,
|
||||||
|
block: suspend ScrollScope.() -> Unit
|
||||||
|
) = scrollableState.scroll(scrollPriority, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberCollapsingToolbarState(
|
||||||
|
initial: Int = Int.MAX_VALUE
|
||||||
|
): CollapsingToolbarState {
|
||||||
|
return remember {
|
||||||
|
CollapsingToolbarState(
|
||||||
|
initial = initial
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CollapsingToolbar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
clipToBounds: Boolean = true,
|
||||||
|
collapsingToolbarState: CollapsingToolbarState,
|
||||||
|
content: @Composable CollapsingToolbarScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val measurePolicy = remember(collapsingToolbarState) {
|
||||||
|
CollapsingToolbarMeasurePolicy(collapsingToolbarState)
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout(
|
||||||
|
content = { CollapsingToolbarScopeInstance.content() },
|
||||||
|
measurePolicy = measurePolicy,
|
||||||
|
modifier = modifier.then(
|
||||||
|
if (clipToBounds) {
|
||||||
|
Modifier.clipToBounds()
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CollapsingToolbarMeasurePolicy(
|
||||||
|
private val collapsingToolbarState: CollapsingToolbarState
|
||||||
|
): MeasurePolicy {
|
||||||
|
override fun MeasureScope.measure(
|
||||||
|
measurables: List<Measurable>,
|
||||||
|
constraints: Constraints
|
||||||
|
): MeasureResult {
|
||||||
|
val placeables = measurables.map {
|
||||||
|
it.measure(
|
||||||
|
constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
minHeight = 0,
|
||||||
|
maxHeight = Constraints.Infinity
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val placeStrategy = measurables.map { it.parentData }
|
||||||
|
|
||||||
|
val minHeight = placeables.minOfOrNull { it.height }
|
||||||
|
?.coerceIn(constraints.minHeight, constraints.maxHeight) ?: 0
|
||||||
|
|
||||||
|
val maxHeight = placeables.maxOfOrNull { it.height }
|
||||||
|
?.coerceIn(constraints.minHeight, constraints.maxHeight) ?: 0
|
||||||
|
|
||||||
|
val maxWidth = placeables.maxOfOrNull{ it.width }
|
||||||
|
?.coerceIn(constraints.minWidth, constraints.maxWidth) ?: 0
|
||||||
|
|
||||||
|
collapsingToolbarState.also {
|
||||||
|
it.minHeight = minHeight
|
||||||
|
it.maxHeight = maxHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
val height = collapsingToolbarState.height
|
||||||
|
return layout(maxWidth, height) {
|
||||||
|
val progress = collapsingToolbarState.progress
|
||||||
|
|
||||||
|
placeables.forEachIndexed { i, placeable ->
|
||||||
|
val strategy = placeStrategy[i]
|
||||||
|
if(strategy is CollapsingToolbarData) {
|
||||||
|
strategy.progressListener?.onProgressUpdate(progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
when(strategy) {
|
||||||
|
is CollapsingToolbarRoadData -> {
|
||||||
|
val collapsed = strategy.whenCollapsed
|
||||||
|
val expanded = strategy.whenExpanded
|
||||||
|
|
||||||
|
val collapsedOffset = collapsed.align(
|
||||||
|
size = IntSize(placeable.width, placeable.height),
|
||||||
|
space = IntSize(maxWidth, height),
|
||||||
|
layoutDirection = layoutDirection
|
||||||
|
)
|
||||||
|
|
||||||
|
val expandedOffset = expanded.align(
|
||||||
|
size = IntSize(placeable.width, placeable.height),
|
||||||
|
space = IntSize(maxWidth, height),
|
||||||
|
layoutDirection = layoutDirection
|
||||||
|
)
|
||||||
|
|
||||||
|
val offset = collapsedOffset + (expandedOffset - collapsedOffset) * progress
|
||||||
|
|
||||||
|
placeable.place(offset.x, offset.y)
|
||||||
|
}
|
||||||
|
is CollapsingToolbarParallaxData ->
|
||||||
|
placeable.placeRelative(
|
||||||
|
x = 0,
|
||||||
|
y = -((maxHeight - minHeight) * (1 - progress) * strategy.ratio).roundToInt()
|
||||||
|
)
|
||||||
|
else -> placeable.placeRelative(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CollapsingToolbarScope {
|
||||||
|
fun Modifier.progress(listener: ProgressListener): Modifier
|
||||||
|
|
||||||
|
fun Modifier.road(whenCollapsed: Alignment, whenExpanded: Alignment): Modifier
|
||||||
|
|
||||||
|
fun Modifier.parallax(ratio: Float = 0.2f): Modifier
|
||||||
|
|
||||||
|
fun Modifier.pin(): Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object CollapsingToolbarScopeInstance: CollapsingToolbarScope {
|
||||||
|
override fun Modifier.progress(listener: ProgressListener): Modifier {
|
||||||
|
return this.then(ProgressUpdateListenerModifier(listener))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun Modifier.road(whenCollapsed: Alignment, whenExpanded: Alignment): Modifier {
|
||||||
|
return this.then(RoadModifier(whenCollapsed, whenExpanded))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun Modifier.parallax(ratio: Float): Modifier {
|
||||||
|
return this.then(ParallaxModifier(ratio))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun Modifier.pin(): Modifier {
|
||||||
|
return this.then(PinModifier())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class RoadModifier(
|
||||||
|
private val whenCollapsed: Alignment,
|
||||||
|
private val whenExpanded: Alignment
|
||||||
|
): ParentDataModifier {
|
||||||
|
override fun Density.modifyParentData(parentData: Any?): Any {
|
||||||
|
return CollapsingToolbarRoadData(
|
||||||
|
this@RoadModifier.whenCollapsed, this@RoadModifier.whenExpanded,
|
||||||
|
(parentData as? CollapsingToolbarData)?.progressListener
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ParallaxModifier(
|
||||||
|
private val ratio: Float
|
||||||
|
): ParentDataModifier {
|
||||||
|
override fun Density.modifyParentData(parentData: Any?): Any {
|
||||||
|
return CollapsingToolbarParallaxData(ratio, (parentData as? CollapsingToolbarData)?.progressListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class PinModifier: ParentDataModifier {
|
||||||
|
override fun Density.modifyParentData(parentData: Any?): Any {
|
||||||
|
return CollapsingToolbarPinData((parentData as? CollapsingToolbarData)?.progressListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ProgressUpdateListenerModifier(
|
||||||
|
private val listener: ProgressListener
|
||||||
|
): ParentDataModifier {
|
||||||
|
override fun Density.modifyParentData(parentData: Any?): Any {
|
||||||
|
return CollapsingToolbarProgressData(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interface ProgressListener {
|
||||||
|
fun onProgressUpdate(value: Float)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class CollapsingToolbarData(
|
||||||
|
var progressListener: ProgressListener?
|
||||||
|
)
|
||||||
|
|
||||||
|
internal class CollapsingToolbarProgressData(
|
||||||
|
progressListener: ProgressListener?
|
||||||
|
): CollapsingToolbarData(progressListener)
|
||||||
|
|
||||||
|
internal class CollapsingToolbarRoadData(
|
||||||
|
var whenCollapsed: Alignment,
|
||||||
|
var whenExpanded: Alignment,
|
||||||
|
progressListener: ProgressListener? = null
|
||||||
|
): CollapsingToolbarData(progressListener)
|
||||||
|
|
||||||
|
internal class CollapsingToolbarPinData(
|
||||||
|
progressListener: ProgressListener? = null
|
||||||
|
): CollapsingToolbarData(progressListener)
|
||||||
|
|
||||||
|
internal class CollapsingToolbarParallaxData(
|
||||||
|
var ratio: Float,
|
||||||
|
progressListener: ProgressListener? = null
|
||||||
|
): CollapsingToolbarData(progressListener)
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 onebone <me@onebone.me>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||||
|
* OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aiosman.riderpro.ui.composables.toolbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.ParentDataModifier
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class CollapsingToolbarScaffoldState(
|
||||||
|
val toolbarState: CollapsingToolbarState,
|
||||||
|
initialOffsetY: Int = 0
|
||||||
|
) {
|
||||||
|
val offsetY: Int
|
||||||
|
get() = offsetYState.value
|
||||||
|
|
||||||
|
internal val offsetYState = mutableStateOf(initialOffsetY)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CollapsingToolbarScaffoldStateSaver: Saver<CollapsingToolbarScaffoldState, List<Any>> {
|
||||||
|
override fun restore(value: List<Any>): CollapsingToolbarScaffoldState =
|
||||||
|
CollapsingToolbarScaffoldState(
|
||||||
|
CollapsingToolbarState(value[0] as Int),
|
||||||
|
value[1] as Int
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun SaverScope.save(value: CollapsingToolbarScaffoldState): List<Any> =
|
||||||
|
listOf(
|
||||||
|
value.toolbarState.height,
|
||||||
|
value.offsetY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberCollapsingToolbarScaffoldState(
|
||||||
|
toolbarState: CollapsingToolbarState = rememberCollapsingToolbarState()
|
||||||
|
): CollapsingToolbarScaffoldState {
|
||||||
|
return rememberSaveable(toolbarState, saver = CollapsingToolbarScaffoldStateSaver()) {
|
||||||
|
CollapsingToolbarScaffoldState(toolbarState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CollapsingToolbarScaffoldScope {
|
||||||
|
@ExperimentalToolbarApi
|
||||||
|
fun Modifier.align(alignment: Alignment): Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CollapsingToolbarScaffold(
|
||||||
|
modifier: Modifier,
|
||||||
|
state: CollapsingToolbarScaffoldState,
|
||||||
|
scrollStrategy: ScrollStrategy,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
toolbarModifier: Modifier = Modifier,
|
||||||
|
toolbarClipToBounds: Boolean = true,
|
||||||
|
toolbar: @Composable CollapsingToolbarScope.() -> Unit,
|
||||||
|
body: @Composable CollapsingToolbarScaffoldScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val flingBehavior = ScrollableDefaults.flingBehavior()
|
||||||
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
|
|
||||||
|
val nestedScrollConnection = remember(scrollStrategy, state) {
|
||||||
|
scrollStrategy.create(state.offsetYState, state.toolbarState, flingBehavior)
|
||||||
|
}
|
||||||
|
|
||||||
|
val toolbarState = state.toolbarState
|
||||||
|
|
||||||
|
Layout(
|
||||||
|
content = {
|
||||||
|
CollapsingToolbar(
|
||||||
|
modifier = toolbarModifier,
|
||||||
|
clipToBounds = toolbarClipToBounds,
|
||||||
|
collapsingToolbarState = toolbarState,
|
||||||
|
) {
|
||||||
|
toolbar()
|
||||||
|
}
|
||||||
|
|
||||||
|
CollapsingToolbarScaffoldScopeInstance.body()
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
.then(
|
||||||
|
if (enabled) {
|
||||||
|
Modifier.nestedScroll(nestedScrollConnection)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) { measurables, constraints ->
|
||||||
|
check(measurables.size >= 2) {
|
||||||
|
"the number of children should be at least 2: toolbar, (at least one) body"
|
||||||
|
}
|
||||||
|
|
||||||
|
val toolbarConstraints = constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
minHeight = 0
|
||||||
|
)
|
||||||
|
val bodyConstraints = constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
minHeight = 0,
|
||||||
|
maxHeight = when (scrollStrategy) {
|
||||||
|
ScrollStrategy.ExitUntilCollapsed ->
|
||||||
|
(constraints.maxHeight - toolbarState.minHeight).coerceAtLeast(0)
|
||||||
|
|
||||||
|
ScrollStrategy.EnterAlways, ScrollStrategy.EnterAlwaysCollapsed ->
|
||||||
|
constraints.maxHeight
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val toolbarPlaceable = measurables[0].measure(toolbarConstraints)
|
||||||
|
|
||||||
|
val bodyMeasurables = measurables.subList(1, measurables.size)
|
||||||
|
val childrenAlignments = bodyMeasurables.map {
|
||||||
|
(it.parentData as? ScaffoldParentData)?.alignment
|
||||||
|
}
|
||||||
|
val bodyPlaceables = bodyMeasurables.map {
|
||||||
|
it.measure(bodyConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
val toolbarHeight = toolbarPlaceable.height
|
||||||
|
|
||||||
|
val width = max(
|
||||||
|
toolbarPlaceable.width,
|
||||||
|
bodyPlaceables.maxOfOrNull { it.width } ?: 0
|
||||||
|
).coerceIn(constraints.minWidth, constraints.maxWidth)
|
||||||
|
val height = max(
|
||||||
|
toolbarHeight,
|
||||||
|
bodyPlaceables.maxOfOrNull { it.height } ?: 0
|
||||||
|
).coerceIn(constraints.minHeight, constraints.maxHeight)
|
||||||
|
|
||||||
|
layout(width, height) {
|
||||||
|
bodyPlaceables.forEachIndexed { index, placeable ->
|
||||||
|
val alignment = childrenAlignments[index]
|
||||||
|
|
||||||
|
if (alignment == null) {
|
||||||
|
placeable.placeRelative(0, toolbarHeight + state.offsetY)
|
||||||
|
} else {
|
||||||
|
val offset = alignment.align(
|
||||||
|
size = IntSize(placeable.width, placeable.height),
|
||||||
|
space = IntSize(width, height),
|
||||||
|
layoutDirection = layoutDirection
|
||||||
|
)
|
||||||
|
placeable.place(offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toolbarPlaceable.placeRelative(0, state.offsetY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object CollapsingToolbarScaffoldScopeInstance: CollapsingToolbarScaffoldScope {
|
||||||
|
@ExperimentalToolbarApi
|
||||||
|
override fun Modifier.align(alignment: Alignment): Modifier =
|
||||||
|
this.then(ScaffoldChildAlignmentModifier(alignment))
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScaffoldChildAlignmentModifier(
|
||||||
|
private val alignment: Alignment
|
||||||
|
) : ParentDataModifier {
|
||||||
|
override fun Density.modifyParentData(parentData: Any?): Any {
|
||||||
|
return (parentData as? ScaffoldParentData) ?: ScaffoldParentData(alignment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ScaffoldParentData(
|
||||||
|
var alignment: Alignment? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 onebone <me@onebone.me>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||||
|
* OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aiosman.riderpro.ui.composables.toolbar
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class FabPlacement(
|
||||||
|
val left: Int,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.aiosman.riderpro.ui.composables.toolbar
|
||||||
|
|
||||||
|
enum class FabPosition {
|
||||||
|
Center,
|
||||||
|
End
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 onebone <me@onebone.me>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||||
|
* OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aiosman.riderpro.ui.composables.toolbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.FlingBehavior
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
|
|
||||||
|
enum class ScrollStrategy {
|
||||||
|
EnterAlways {
|
||||||
|
override fun create(
|
||||||
|
offsetY: MutableState<Int>,
|
||||||
|
toolbarState: CollapsingToolbarState,
|
||||||
|
flingBehavior: FlingBehavior
|
||||||
|
): NestedScrollConnection =
|
||||||
|
EnterAlwaysNestedScrollConnection(offsetY, toolbarState, flingBehavior)
|
||||||
|
},
|
||||||
|
EnterAlwaysCollapsed {
|
||||||
|
override fun create(
|
||||||
|
offsetY: MutableState<Int>,
|
||||||
|
toolbarState: CollapsingToolbarState,
|
||||||
|
flingBehavior: FlingBehavior
|
||||||
|
): NestedScrollConnection =
|
||||||
|
EnterAlwaysCollapsedNestedScrollConnection(offsetY, toolbarState, flingBehavior)
|
||||||
|
},
|
||||||
|
ExitUntilCollapsed {
|
||||||
|
override fun create(
|
||||||
|
offsetY: MutableState<Int>,
|
||||||
|
toolbarState: CollapsingToolbarState,
|
||||||
|
flingBehavior: FlingBehavior
|
||||||
|
): NestedScrollConnection =
|
||||||
|
ExitUntilCollapsedNestedScrollConnection(toolbarState, flingBehavior)
|
||||||
|
};
|
||||||
|
|
||||||
|
internal abstract fun create(
|
||||||
|
offsetY: MutableState<Int>,
|
||||||
|
toolbarState: CollapsingToolbarState,
|
||||||
|
flingBehavior: FlingBehavior
|
||||||
|
): NestedScrollConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScrollDelegate(
|
||||||
|
private val offsetY: MutableState<Int>
|
||||||
|
) {
|
||||||
|
private var scrollToBeConsumed: Float = 0f
|
||||||
|
|
||||||
|
fun doScroll(delta: Float) {
|
||||||
|
val scroll = scrollToBeConsumed + delta
|
||||||
|
val scrollInt = scroll.toInt()
|
||||||
|
|
||||||
|
scrollToBeConsumed = scroll - scrollInt
|
||||||
|
|
||||||
|
offsetY.value += scrollInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class EnterAlwaysNestedScrollConnection(
|
||||||
|
private val offsetY: MutableState<Int>,
|
||||||
|
private val toolbarState: CollapsingToolbarState,
|
||||||
|
private val flingBehavior: FlingBehavior
|
||||||
|
): NestedScrollConnection {
|
||||||
|
private val scrollDelegate = ScrollDelegate(offsetY)
|
||||||
|
//private val tracker = RelativeVelocityTracker(CurrentTimeProviderImpl())
|
||||||
|
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
val dy = available.y
|
||||||
|
|
||||||
|
val toolbar = toolbarState.height.toFloat()
|
||||||
|
val offset = offsetY.value.toFloat()
|
||||||
|
|
||||||
|
// -toolbarHeight <= offsetY + dy <= 0
|
||||||
|
val consume = if(dy < 0) {
|
||||||
|
val toolbarConsumption = toolbarState.dispatchRawDelta(dy)
|
||||||
|
val remaining = dy - toolbarConsumption
|
||||||
|
val offsetConsumption = remaining.coerceAtLeast(-toolbar - offset)
|
||||||
|
scrollDelegate.doScroll(offsetConsumption)
|
||||||
|
|
||||||
|
toolbarConsumption + offsetConsumption
|
||||||
|
}else{
|
||||||
|
val offsetConsumption = dy.coerceAtMost(-offset)
|
||||||
|
scrollDelegate.doScroll(offsetConsumption)
|
||||||
|
|
||||||
|
val toolbarConsumption = toolbarState.dispatchRawDelta(dy - offsetConsumption)
|
||||||
|
|
||||||
|
offsetConsumption + toolbarConsumption
|
||||||
|
}
|
||||||
|
|
||||||
|
return Offset(0f, consume)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
|
val left = if(available.y > 0) {
|
||||||
|
toolbarState.fling(flingBehavior, available.y)
|
||||||
|
}else{
|
||||||
|
// If velocity < 0, the main content should have a remaining scroll space
|
||||||
|
// so the scroll resumes to the onPreScroll(..., Fling) phase. Hence we do
|
||||||
|
// not need to process it at onPostFling() manually.
|
||||||
|
available.y
|
||||||
|
}
|
||||||
|
|
||||||
|
return Velocity(x = 0f, y = available.y - left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class EnterAlwaysCollapsedNestedScrollConnection(
|
||||||
|
private val offsetY: MutableState<Int>,
|
||||||
|
private val toolbarState: CollapsingToolbarState,
|
||||||
|
private val flingBehavior: FlingBehavior
|
||||||
|
): NestedScrollConnection {
|
||||||
|
private val scrollDelegate = ScrollDelegate(offsetY)
|
||||||
|
//private val tracker = RelativeVelocityTracker(CurrentTimeProviderImpl())
|
||||||
|
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
val dy = available.y
|
||||||
|
|
||||||
|
val consumed = if(dy > 0) { // expanding: offset -> body -> toolbar
|
||||||
|
val offsetConsumption = dy.coerceAtMost(-offsetY.value.toFloat())
|
||||||
|
scrollDelegate.doScroll(offsetConsumption)
|
||||||
|
|
||||||
|
offsetConsumption
|
||||||
|
}else{ // collapsing: toolbar -> offset -> body
|
||||||
|
val toolbarConsumption = toolbarState.dispatchRawDelta(dy)
|
||||||
|
val offsetConsumption = (dy - toolbarConsumption).coerceAtLeast(-toolbarState.height.toFloat() - offsetY.value)
|
||||||
|
|
||||||
|
scrollDelegate.doScroll(offsetConsumption)
|
||||||
|
|
||||||
|
toolbarConsumption + offsetConsumption
|
||||||
|
}
|
||||||
|
|
||||||
|
return Offset(0f, consumed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
val dy = available.y
|
||||||
|
|
||||||
|
return if(dy > 0) {
|
||||||
|
Offset(0f, toolbarState.dispatchRawDelta(dy))
|
||||||
|
}else{
|
||||||
|
Offset(0f, 0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
|
val dy = available.y
|
||||||
|
|
||||||
|
val left = if(dy > 0) {
|
||||||
|
// onPostFling() has positive available scroll value only called if the main scroll
|
||||||
|
// has leftover scroll, i.e. the scroll of the main content has done. So we just process
|
||||||
|
// fling if the available value is positive.
|
||||||
|
toolbarState.fling(flingBehavior, dy)
|
||||||
|
}else{
|
||||||
|
dy
|
||||||
|
}
|
||||||
|
|
||||||
|
return Velocity(x = 0f, y = available.y - left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ExitUntilCollapsedNestedScrollConnection(
|
||||||
|
private val toolbarState: CollapsingToolbarState,
|
||||||
|
private val flingBehavior: FlingBehavior
|
||||||
|
): NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
val dy = available.y
|
||||||
|
|
||||||
|
val consume = if(dy < 0) { // collapsing: toolbar -> body
|
||||||
|
toolbarState.dispatchRawDelta(dy)
|
||||||
|
}else{
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
|
||||||
|
return Offset(0f, consume)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
val dy = available.y
|
||||||
|
|
||||||
|
val consume = if(dy > 0) { // expanding: body -> toolbar
|
||||||
|
toolbarState.dispatchRawDelta(dy)
|
||||||
|
}else{
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
|
||||||
|
return Offset(0f, consume)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
|
val left = if(available.y < 0) {
|
||||||
|
toolbarState.fling(flingBehavior, available.y)
|
||||||
|
}else{
|
||||||
|
available.y
|
||||||
|
}
|
||||||
|
|
||||||
|
return Velocity(x = 0f, y = available.y - left)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
|
val velocity = available.y
|
||||||
|
|
||||||
|
val left = if(velocity > 0) {
|
||||||
|
toolbarState.fling(flingBehavior, velocity)
|
||||||
|
}else{
|
||||||
|
velocity
|
||||||
|
}
|
||||||
|
|
||||||
|
return Velocity(x = 0f, y = available.y - left)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.aiosman.riderpro.ui.composables.toolbar
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@ExperimentalToolbarApi
|
||||||
|
@Composable
|
||||||
|
fun ToolbarWithFabScaffold(
|
||||||
|
modifier: Modifier,
|
||||||
|
state: CollapsingToolbarScaffoldState,
|
||||||
|
scrollStrategy: ScrollStrategy,
|
||||||
|
toolbarModifier: Modifier = Modifier,
|
||||||
|
toolbarClipToBounds: Boolean = true,
|
||||||
|
toolbar: @Composable CollapsingToolbarScope.() -> Unit,
|
||||||
|
fab: @Composable () -> Unit,
|
||||||
|
fabPosition: FabPosition = FabPosition.End,
|
||||||
|
body: @Composable CollapsingToolbarScaffoldScope.() -> Unit
|
||||||
|
) {
|
||||||
|
SubcomposeLayout(
|
||||||
|
modifier = modifier
|
||||||
|
) { constraints ->
|
||||||
|
|
||||||
|
val toolbarScaffoldConstraints = constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
minHeight = 0,
|
||||||
|
maxHeight = constraints.maxHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
val toolbarScaffoldPlaceables = subcompose(ToolbarWithFabScaffoldContent.ToolbarScaffold) {
|
||||||
|
CollapsingToolbarScaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
state = state,
|
||||||
|
scrollStrategy = scrollStrategy,
|
||||||
|
toolbarModifier = toolbarModifier,
|
||||||
|
toolbarClipToBounds = toolbarClipToBounds,
|
||||||
|
toolbar = toolbar,
|
||||||
|
body = body
|
||||||
|
)
|
||||||
|
}.map { it.measure(toolbarScaffoldConstraints) }
|
||||||
|
|
||||||
|
val fabConstraints = constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
minHeight = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
val fabPlaceables = subcompose(
|
||||||
|
ToolbarWithFabScaffoldContent.Fab,
|
||||||
|
fab
|
||||||
|
).mapNotNull { measurable ->
|
||||||
|
measurable.measure(fabConstraints).takeIf { it.height != 0 && it.width != 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
val fabPlacement = if (fabPlaceables.isNotEmpty()) {
|
||||||
|
val fabWidth = fabPlaceables.maxOfOrNull { it.width } ?: 0
|
||||||
|
val fabHeight = fabPlaceables.maxOfOrNull { it.height } ?: 0
|
||||||
|
// FAB distance from the left of the layout, taking into account LTR / RTL
|
||||||
|
val fabLeftOffset = if (fabPosition == FabPosition.End) {
|
||||||
|
if (layoutDirection == LayoutDirection.Ltr) {
|
||||||
|
constraints.maxWidth - 16.dp.roundToPx() - fabWidth
|
||||||
|
} else {
|
||||||
|
16.dp.roundToPx()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(constraints.maxWidth - fabWidth) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
FabPlacement(
|
||||||
|
left = fabLeftOffset,
|
||||||
|
width = fabWidth,
|
||||||
|
height = fabHeight
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val fabOffsetFromBottom = fabPlacement?.let {
|
||||||
|
it.height + 16.dp.roundToPx()
|
||||||
|
}
|
||||||
|
|
||||||
|
val width = constraints.maxWidth
|
||||||
|
val height = constraints.maxHeight
|
||||||
|
|
||||||
|
layout(width, height) {
|
||||||
|
toolbarScaffoldPlaceables.forEach {
|
||||||
|
it.place(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fabPlacement?.let { placement ->
|
||||||
|
fabPlaceables.forEach {
|
||||||
|
it.place(placement.left, height - fabOffsetFromBottom!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class ToolbarWithFabScaffoldContent {
|
||||||
|
ToolbarScaffold, Fab
|
||||||
|
}
|
||||||
@@ -6,8 +6,6 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -20,7 +18,6 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -41,11 +38,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Edit
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -59,64 +54,41 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.PathEffect
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.Velocity
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.riderpro.LocalNavController
|
||||||
import com.aiosman.riderpro.R
|
import com.aiosman.riderpro.R
|
||||||
import com.aiosman.riderpro.entity.AccountProfileEntity
|
import com.aiosman.riderpro.entity.AccountProfileEntity
|
||||||
import com.aiosman.riderpro.entity.MomentEntity
|
import com.aiosman.riderpro.entity.MomentEntity
|
||||||
import com.aiosman.riderpro.exp.formatPostTime2
|
|
||||||
import com.aiosman.riderpro.ui.NavigationRoute
|
import com.aiosman.riderpro.ui.NavigationRoute
|
||||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||||
import com.aiosman.riderpro.ui.composables.DropdownMenu
|
import com.aiosman.riderpro.ui.composables.DropdownMenu
|
||||||
import com.aiosman.riderpro.ui.composables.MenuItem
|
import com.aiosman.riderpro.ui.composables.MenuItem
|
||||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.EmptyMomentPostUnit
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.GalleryItem
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.MomentPostUnit
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.OtherProfileAction
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.SelfProfileAction
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserContentPageIndicator
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserItem
|
||||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.riderpro.ui.navigateToPost
|
|
||||||
import com.aiosman.riderpro.ui.post.NewPostViewModel
|
import com.aiosman.riderpro.ui.post.NewPostViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ProfileWrap(
|
|
||||||
|
|
||||||
) {
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
MyProfileViewModel.loadProfile()
|
|
||||||
}
|
|
||||||
Profile(
|
|
||||||
onUpdateBanner = { uri, context ->
|
|
||||||
MyProfileViewModel.updateUserProfileBanner(uri, context)
|
|
||||||
},
|
|
||||||
onLogout = {
|
|
||||||
MyProfileViewModel.viewModelScope.launch {
|
|
||||||
MyProfileViewModel.logout()
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
profile = MyProfileViewModel.profile,
|
|
||||||
sharedFlow = MyProfileViewModel.sharedFlow
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Profile(
|
fun Profile(
|
||||||
@@ -188,14 +160,6 @@ fun Profile(
|
|||||||
|
|
||||||
return Offset(x = 0f, y = consumedHeader / speedFactor)
|
return Offset(x = 0f, y = consumedHeader / speedFactor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// override fun onPostScroll(
|
|
||||||
// consumed: Offset,
|
|
||||||
// available: Offset,
|
|
||||||
// source: NestedScrollSource
|
|
||||||
// ): Offset {
|
|
||||||
// return Offset.Zero
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,8 +357,6 @@ fun Profile(
|
|||||||
.alpha(1 - alpha)
|
.alpha(1 - alpha)
|
||||||
.background(Color(0xfff8f8f8))
|
.background(Color(0xfff8f8f8))
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
|
|
||||||
|
|
||||||
) {
|
) {
|
||||||
StatusBarSpacer()
|
StatusBarSpacer()
|
||||||
|
|
||||||
@@ -425,57 +387,7 @@ fun Profile(
|
|||||||
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Row(
|
UserContentPageIndicator(pagerState)
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(32.dp))
|
|
||||||
.background(if (pagerState.currentPage == 0) Color(0xFFFFFFFF) else Color.Transparent)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
// switch to gallery
|
|
||||||
scope.launch {
|
|
||||||
pagerState.scrollToPage(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.gallery),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
color = Color.Black,
|
|
||||||
modifier = Modifier.padding(8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(32.dp))
|
|
||||||
.background(if (pagerState.currentPage == 1) Color(0xFFFFFFFF) else Color.Transparent)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
// switch to moments
|
|
||||||
scope.launch {
|
|
||||||
pagerState.scrollToPage(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.moment),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
color = Color.Black,
|
|
||||||
modifier = Modifier.padding(8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
@@ -530,30 +442,7 @@ fun Profile(
|
|||||||
}
|
}
|
||||||
items(moments.itemCount) { idx ->
|
items(moments.itemCount) { idx ->
|
||||||
val moment = moments[idx] ?: return@items
|
val moment = moments[idx] ?: return@items
|
||||||
Box(
|
GalleryItem(moment)
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(
|
|
||||||
if (idx % 3 == 0) 1.5f else 1f
|
|
||||||
)
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.noRippleClickable {
|
|
||||||
navController.navigateToPost(
|
|
||||||
moment.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
) {
|
|
||||||
CustomAsyncImage(
|
|
||||||
LocalContext.current,
|
|
||||||
moment.images[0].thumbnail,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize(),
|
|
||||||
contentDescription = "",
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
items(2) {
|
items(2) {
|
||||||
Spacer(modifier = Modifier.height(120.dp))
|
Spacer(modifier = Modifier.height(120.dp))
|
||||||
@@ -563,14 +452,15 @@ fun Profile(
|
|||||||
|
|
||||||
1 -> {
|
1 -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxHeight(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = scrollState
|
state = scrollState
|
||||||
) {
|
) {
|
||||||
item {
|
|
||||||
if (moments.itemCount == 0) {
|
if (moments.itemCount == 0) {
|
||||||
|
item {
|
||||||
EmptyMomentPostUnit()
|
EmptyMomentPostUnit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
for (idx in 0 until moments.itemCount) {
|
for (idx in 0 until moments.itemCount) {
|
||||||
val moment = moments[idx]
|
val moment = moments[idx]
|
||||||
@@ -590,449 +480,4 @@ fun Profile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UserItem(accountProfileEntity: AccountProfileEntity) {
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// 头像
|
|
||||||
CustomAsyncImage(
|
|
||||||
LocalContext.current,
|
|
||||||
accountProfileEntity.avatar,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.size(48.dp),
|
|
||||||
contentDescription = "",
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(32.dp))
|
|
||||||
//个人统计
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.noRippleClickable {
|
|
||||||
navController.navigate(
|
|
||||||
NavigationRoute.FollowerList.route.replace(
|
|
||||||
"{id}",
|
|
||||||
accountProfileEntity.id.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = accountProfileEntity.followerCount.toString(),
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
fontSize = 16.sp
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.followers_upper),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.noRippleClickable {
|
|
||||||
navController.navigate(
|
|
||||||
NavigationRoute.FollowingList.route.replace(
|
|
||||||
"{id}",
|
|
||||||
accountProfileEntity.id.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = accountProfileEntity.followingCount.toString(),
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
fontSize = 16.sp
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.following_upper),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
// 昵称
|
|
||||||
Text(
|
|
||||||
text = accountProfileEntity.nickName,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
// 个人简介
|
|
||||||
Text(
|
|
||||||
text = accountProfileEntity.bio,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = Color.Gray
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SelfProfileAction(
|
|
||||||
onEditProfile: () -> Unit
|
|
||||||
) {
|
|
||||||
// 按钮
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(32.dp))
|
|
||||||
.background(Color(0xffebebeb))
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
onEditProfile()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Edit,
|
|
||||||
contentDescription = "",
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.edit_profile),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
color = Color.Black,
|
|
||||||
modifier = Modifier.padding(8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun OtherProfileAction(
|
|
||||||
profile: AccountProfileEntity,
|
|
||||||
onFollow: (() -> Unit)? = null,
|
|
||||||
onChat: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(32.dp))
|
|
||||||
.background(if (profile.isFollowing) Color(0xffebebeb) else Color(0xffda3833))
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
onFollow?.invoke()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Add,
|
|
||||||
contentDescription = "",
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
tint = if (profile.isFollowing) Color.Black else Color.White
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(if (profile.isFollowing) R.string.following_upper else R.string.follow_upper),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
color = if (profile.isFollowing) Color.Black else Color.White,
|
|
||||||
modifier = Modifier.padding(8.dp),
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(32.dp))
|
|
||||||
.background(Color(0xffebebeb))
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
onChat?.invoke()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_comment),
|
|
||||||
contentDescription = "",
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.chat_upper),
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.W600,
|
|
||||||
color = Color.Black,
|
|
||||||
modifier = Modifier.padding(8.dp),
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 按钮
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun EmptyMomentPostUnit() {
|
|
||||||
TimeGroup(stringResource(R.string.empty_my_post_title))
|
|
||||||
ProfileEmptyMomentCard()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ProfileEmptyMomentCard(
|
|
||||||
|
|
||||||
) {
|
|
||||||
var columnHeight by remember { mutableStateOf(0) }
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 24.dp, top = 18.dp, end = 24.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Canvas(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(with(LocalDensity.current) { columnHeight.toDp() })
|
|
||||||
.width(14.dp)
|
|
||||||
) {
|
|
||||||
drawLine(
|
|
||||||
color = Color(0xff899DA9),
|
|
||||||
start = Offset(0f, 0f),
|
|
||||||
end = Offset(0f, size.height),
|
|
||||||
strokeWidth = 4f,
|
|
||||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.onGloballyPositioned { coordinates ->
|
|
||||||
columnHeight = coordinates.size.height
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.empty_my_post_content), fontSize = 16.sp)
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.aspectRatio(3f / 2f)
|
|
||||||
.background(Color.White)
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color(0xFFF5F5F5))
|
|
||||||
.noRippleClickable {
|
|
||||||
NewPostViewModel.asNewPost()
|
|
||||||
navController.navigate(NavigationRoute.NewPost.route)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Add,
|
|
||||||
tint = Color(0xFFD8D8D8),
|
|
||||||
contentDescription = "New post",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(32.dp)
|
|
||||||
.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MomentPostUnit(momentEntity: MomentEntity) {
|
|
||||||
TimeGroup(momentEntity.time.formatPostTime2())
|
|
||||||
ProfileMomentCard(
|
|
||||||
momentEntity.momentTextContent,
|
|
||||||
momentEntity.images[0].thumbnail,
|
|
||||||
momentEntity.likeCount.toString(),
|
|
||||||
momentEntity.commentCount.toString(),
|
|
||||||
momentEntity = momentEntity
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TimeGroup(time: String = "2024.06.08 12:23") {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 24.dp, top = 40.dp, end = 24.dp),
|
|
||||||
horizontalArrangement = Arrangement.Start,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(16.dp)
|
|
||||||
.width(14.dp),
|
|
||||||
painter = painterResource(id = R.drawable.rider_pro_moment_time_flag),
|
|
||||||
contentDescription = ""
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Text(
|
|
||||||
text = time,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = Color.Black,
|
|
||||||
style = TextStyle(fontWeight = FontWeight.W600)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ProfileMomentCard(
|
|
||||||
content: String,
|
|
||||||
imageUrl: String,
|
|
||||||
like: String,
|
|
||||||
comment: String,
|
|
||||||
momentEntity: MomentEntity
|
|
||||||
) {
|
|
||||||
var columnHeight by remember { mutableStateOf(0) }
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 24.dp, top = 18.dp, end = 24.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Canvas(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(with(LocalDensity.current) { columnHeight.toDp() })
|
|
||||||
.width(14.dp)
|
|
||||||
) {
|
|
||||||
drawLine(
|
|
||||||
color = Color(0xff899DA9),
|
|
||||||
start = Offset(0f, 0f),
|
|
||||||
end = Offset(0f, size.height),
|
|
||||||
strokeWidth = 4f,
|
|
||||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(Color.White)
|
|
||||||
.weight(1f)
|
|
||||||
.onGloballyPositioned { coordinates ->
|
|
||||||
columnHeight = coordinates.size.height
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
if (content.isNotEmpty()) {
|
|
||||||
MomentCardTopContent(content)
|
|
||||||
}
|
|
||||||
MomentCardPicture(imageUrl, momentEntity = momentEntity)
|
|
||||||
MomentCardOperation(like, comment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MomentCardTopContent(content: String) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.Start,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(top = 16.dp, bottom = 0.dp, start = 16.dp, end = 16.dp),
|
|
||||||
text = content, fontSize = 16.sp, color = Color.Black
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) {
|
|
||||||
val navController = LocalNavController.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
CustomAsyncImage(
|
|
||||||
context,
|
|
||||||
imageUrl,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.aspectRatio(3f / 2f)
|
|
||||||
.padding(top = 16.dp)
|
|
||||||
.noRippleClickable {
|
|
||||||
navController.navigateToPost(
|
|
||||||
id = momentEntity.id,
|
|
||||||
highlightCommentId = 0,
|
|
||||||
initImagePagerIndex = 0
|
|
||||||
)
|
|
||||||
},
|
|
||||||
contentDescription = "",
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MomentCardOperation(like: String, comment: String) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(48.dp),
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// Spacer(modifier = Modifier.weight(1f))
|
|
||||||
MomentCardOperationItem(
|
|
||||||
drawable = R.drawable.rider_pro_like,
|
|
||||||
number = like,
|
|
||||||
modifier = Modifier.padding(end = 32.dp)
|
|
||||||
)
|
|
||||||
MomentCardOperationItem(
|
|
||||||
drawable = R.drawable.rider_pro_moment_comment,
|
|
||||||
number = comment,
|
|
||||||
modifier = Modifier.padding(end = 32.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MomentCardOperationItem(@DrawableRes drawable: Int, number: String, modifier: Modifier) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 8.dp),
|
|
||||||
painter = painterResource(id = drawable), contentDescription = ""
|
|
||||||
)
|
|
||||||
Text(text = number)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
package com.aiosman.riderpro.ui.index.tabs.profile
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
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.layout.size
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import com.aiosman.riderpro.LocalNavController
|
||||||
|
import com.aiosman.riderpro.R
|
||||||
|
import com.aiosman.riderpro.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.riderpro.entity.MomentEntity
|
||||||
|
import com.aiosman.riderpro.ui.NavigationRoute
|
||||||
|
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.riderpro.ui.composables.DropdownMenu
|
||||||
|
import com.aiosman.riderpro.ui.composables.MenuItem
|
||||||
|
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
||||||
|
import com.aiosman.riderpro.ui.composables.toolbar.CollapsingToolbarScaffold
|
||||||
|
import com.aiosman.riderpro.ui.composables.toolbar.ScrollStrategy
|
||||||
|
import com.aiosman.riderpro.ui.composables.toolbar.rememberCollapsingToolbarScaffoldState
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.EmptyMomentPostUnit
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.GalleryItem
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.MomentPostUnit
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.OtherProfileAction
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.SelfProfileAction
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserContentPageIndicator
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserItem
|
||||||
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.riderpro.ui.post.NewPostViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ProfileV3(
|
||||||
|
onUpdateBanner: ((Uri, Context) -> Unit)? = null,
|
||||||
|
profile: AccountProfileEntity? = null,
|
||||||
|
onLogout: () -> Unit = {},
|
||||||
|
onFollowClick: () -> Unit = {},
|
||||||
|
onChatClick: () -> Unit = {},
|
||||||
|
sharedFlow: SharedFlow<PagingData<MomentEntity>> = MutableStateFlow<PagingData<MomentEntity>>(
|
||||||
|
PagingData.empty()
|
||||||
|
).asStateFlow(),
|
||||||
|
isSelf: Boolean = true
|
||||||
|
) {
|
||||||
|
val state = rememberCollapsingToolbarScaffoldState()
|
||||||
|
val pagerState = rememberPagerState(pageCount = { 2 })
|
||||||
|
var enabled by remember { mutableStateOf(true) }
|
||||||
|
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
var bannerHeight = 400
|
||||||
|
val pickBannerImageLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = result.data?.data
|
||||||
|
uri?.let {
|
||||||
|
onUpdateBanner?.invoke(it, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val moments = sharedFlow.collectAsLazyPagingItems()
|
||||||
|
|
||||||
|
Box {
|
||||||
|
CollapsingToolbarScaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = state,
|
||||||
|
scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
|
||||||
|
enabled = enabled,
|
||||||
|
toolbar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
// 保持在最低高度和当前高度之间
|
||||||
|
.background(Color(0xfff8f8f8))
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
|
||||||
|
|
||||||
|
) {
|
||||||
|
StatusBarSpacer()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
LocalContext.current,
|
||||||
|
profile?.avatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = profile?.nickName ?: "",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
// Column(
|
||||||
|
// modifier = Modifier.fillMaxWidth()
|
||||||
|
//
|
||||||
|
// ) {
|
||||||
|
// StatusBarSpacer()
|
||||||
|
// Box(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .background(Color.White)
|
||||||
|
// .fillMaxWidth()
|
||||||
|
// .height(64.dp)
|
||||||
|
// ) {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.parallax(0.5f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(600.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
// change alpha of Image as the toolbar expands
|
||||||
|
alpha = state.toolbarState.progress
|
||||||
|
},
|
||||||
|
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// banner
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(bannerHeight.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.noRippleClickable {
|
||||||
|
Intent(Intent.ACTION_PICK).apply {
|
||||||
|
type = "image/*"
|
||||||
|
pickBannerImageLauncher.launch(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.shadow(
|
||||||
|
elevation = 4.dp,
|
||||||
|
shape = RoundedCornerShape(
|
||||||
|
bottomStart = 32.dp,
|
||||||
|
bottomEnd = 32.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val banner = profile?.banner
|
||||||
|
|
||||||
|
if (banner != null) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
LocalContext.current,
|
||||||
|
banner,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_moment_demo_2),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isSelf) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(
|
||||||
|
top = statusBarPaddingValues.calculateTopPadding(),
|
||||||
|
start = 8.dp,
|
||||||
|
end = 8.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.shadow(
|
||||||
|
elevation = 20.dp
|
||||||
|
)
|
||||||
|
.background(Color.White.copy(alpha = 0.7f))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.noRippleClickable {
|
||||||
|
expanded = true
|
||||||
|
},
|
||||||
|
tint = Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
width = 250,
|
||||||
|
menuItems = listOf(
|
||||||
|
MenuItem(
|
||||||
|
stringResource(R.string.logout),
|
||||||
|
R.mipmap.rider_pro_logout
|
||||||
|
) {
|
||||||
|
expanded = false
|
||||||
|
scope.launch {
|
||||||
|
onLogout()
|
||||||
|
navController.navigate(NavigationRoute.Login.route) {
|
||||||
|
popUpTo(NavigationRoute.Index.route) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MenuItem(
|
||||||
|
stringResource(R.string.change_password),
|
||||||
|
R.mipmap.rider_pro_change_password
|
||||||
|
) {
|
||||||
|
expanded = false
|
||||||
|
scope.launch {
|
||||||
|
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MenuItem(
|
||||||
|
stringResource(R.string.favourites),
|
||||||
|
R.drawable.rider_pro_favourite
|
||||||
|
) {
|
||||||
|
expanded = false
|
||||||
|
scope.launch {
|
||||||
|
navController.navigate(NavigationRoute.FavouriteList.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// user info
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
// 个人信息
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
profile?.let {
|
||||||
|
UserItem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
profile?.let {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
if (isSelf) {
|
||||||
|
SelfProfileAction {
|
||||||
|
navController.navigate(NavigationRoute.AccountEdit.route)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
OtherProfileAction(
|
||||||
|
it,
|
||||||
|
onFollow = {
|
||||||
|
onFollowClick()
|
||||||
|
},
|
||||||
|
onChat = {
|
||||||
|
onChatClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collapsed bar
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
UserContentPageIndicator(
|
||||||
|
pagerState = pagerState,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
) { idx ->
|
||||||
|
when (idx) {
|
||||||
|
0 ->
|
||||||
|
LazyVerticalStaggeredGrid(
|
||||||
|
columns = StaggeredGridCells.Fixed(2),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
|
8.dp
|
||||||
|
),
|
||||||
|
verticalItemSpacing = 8.dp,
|
||||||
|
contentPadding = PaddingValues(8.dp)
|
||||||
|
) {
|
||||||
|
if (isSelf) {
|
||||||
|
items(1) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(0.75f)
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.background(Color.White)
|
||||||
|
.padding(8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
NewPostViewModel.asNewPost()
|
||||||
|
navController.navigate(NavigationRoute.NewPost.route)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.background(Color(0xfff5f5f5))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(moments.itemCount) { idx ->
|
||||||
|
val moment = moments[idx] ?: return@items
|
||||||
|
GalleryItem(moment,idx)
|
||||||
|
}
|
||||||
|
items(2) {
|
||||||
|
Spacer(modifier = Modifier.height(120.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
1 ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
if (moments.itemCount == 0) {
|
||||||
|
item {
|
||||||
|
EmptyMomentPostUnit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
for (idx in 0 until moments.itemCount) {
|
||||||
|
val moment = moments[idx]
|
||||||
|
moment?.let {
|
||||||
|
MomentPostUnit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(120.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.aiosman.riderpro.ui.index.tabs.profile
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProfileWrap(
|
||||||
|
|
||||||
|
) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
MyProfileViewModel.loadProfile()
|
||||||
|
}
|
||||||
|
// ProfileV2(
|
||||||
|
// onUpdateBanner = { uri, context ->
|
||||||
|
// MyProfileViewModel.updateUserProfileBanner(uri, context)
|
||||||
|
// },
|
||||||
|
// onLogout = {
|
||||||
|
// MyProfileViewModel.viewModelScope.launch {
|
||||||
|
// MyProfileViewModel.logout()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// },
|
||||||
|
// profile = MyProfileViewModel.profile,
|
||||||
|
// sharedFlow = MyProfileViewModel.sharedFlow
|
||||||
|
// )
|
||||||
|
ProfileV3(
|
||||||
|
onUpdateBanner = { uri, context ->
|
||||||
|
MyProfileViewModel.updateUserProfileBanner(uri, context)
|
||||||
|
},
|
||||||
|
onLogout = {
|
||||||
|
MyProfileViewModel.viewModelScope.launch {
|
||||||
|
MyProfileViewModel.logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
profile = MyProfileViewModel.profile,
|
||||||
|
sharedFlow = MyProfileViewModel.sharedFlow
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,481 @@
|
|||||||
|
package com.aiosman.riderpro.ui.index.tabs.profile
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
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.layout.size
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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.draw.shadow
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import com.aiosman.riderpro.LocalNavController
|
||||||
|
import com.aiosman.riderpro.R
|
||||||
|
import com.aiosman.riderpro.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.riderpro.entity.MomentEntity
|
||||||
|
import com.aiosman.riderpro.ui.NavigationRoute
|
||||||
|
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.riderpro.ui.composables.DropdownMenu
|
||||||
|
import com.aiosman.riderpro.ui.composables.MenuItem
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.EmptyMomentPostUnit
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.GalleryItem
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.MomentPostUnit
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.OtherProfileAction
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.SelfProfileAction
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserContentPageIndicator
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserItem
|
||||||
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.riderpro.ui.post.NewPostViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ProfileV2(
|
||||||
|
onUpdateBanner: ((Uri, Context) -> Unit)? = null,
|
||||||
|
profile: AccountProfileEntity? = null,
|
||||||
|
onLogout: () -> Unit = {},
|
||||||
|
onFollowClick: () -> Unit = {},
|
||||||
|
onChatClick: () -> Unit = {},
|
||||||
|
sharedFlow: SharedFlow<PagingData<MomentEntity>> = MutableStateFlow<PagingData<MomentEntity>>(
|
||||||
|
PagingData.empty()
|
||||||
|
).asStateFlow(),
|
||||||
|
isSelf: Boolean = true
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.background(Color(0xffFFFFFF))
|
||||||
|
) {
|
||||||
|
var parentScrollThreshold by remember { mutableStateOf(0) }
|
||||||
|
var remainScrollThreshold by remember { mutableStateOf(0) }
|
||||||
|
val bannerHeight = 500
|
||||||
|
var scrollState = rememberLazyListState()
|
||||||
|
var gridScrollState = rememberLazyStaggeredGridState()
|
||||||
|
var pagerState = rememberPagerState(pageCount = { 2 })
|
||||||
|
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
|
val context = LocalContext.current
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val moments = sharedFlow.collectAsLazyPagingItems()
|
||||||
|
val rootScrollState = rememberScrollState()
|
||||||
|
val pickBannerImageLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri = result.data?.data
|
||||||
|
uri?.let {
|
||||||
|
onUpdateBanner?.invoke(it, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val parentScrollConnection = remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
Log.d("ProfileV2", "onPreScroll: $available")
|
||||||
|
val delta = available.y.toInt()
|
||||||
|
if (delta < 0 && rootScrollState.value < parentScrollThreshold - remainScrollThreshold) {
|
||||||
|
val scrollAmount = minOf(
|
||||||
|
-delta,
|
||||||
|
parentScrollThreshold - remainScrollThreshold - rootScrollState.value
|
||||||
|
)
|
||||||
|
scope.launch {
|
||||||
|
Log.d("ProfileV2", "scrollBy: $scrollAmount")
|
||||||
|
rootScrollState.scrollBy(scrollAmount.toFloat())
|
||||||
|
}
|
||||||
|
return Offset(0f, -scrollAmount.toFloat())
|
||||||
|
}
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.nestedScroll(parentScrollConnection)
|
||||||
|
.verticalScroll(
|
||||||
|
state = rootScrollState
|
||||||
|
)
|
||||||
|
.background(Color(0xfff8f8f8))
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned {
|
||||||
|
parentScrollThreshold = it.size.height
|
||||||
|
}
|
||||||
|
|
||||||
|
) {
|
||||||
|
// banner
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(bannerHeight.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.noRippleClickable {
|
||||||
|
Intent(Intent.ACTION_PICK).apply {
|
||||||
|
type = "image/*"
|
||||||
|
pickBannerImageLauncher.launch(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.shadow(
|
||||||
|
elevation = 4.dp,
|
||||||
|
shape = RoundedCornerShape(
|
||||||
|
bottomStart = 32.dp,
|
||||||
|
bottomEnd = 32.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val banner = profile?.banner
|
||||||
|
|
||||||
|
if (banner != null) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
LocalContext.current,
|
||||||
|
banner,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_moment_demo_2),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isSelf) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(
|
||||||
|
top = statusBarPaddingValues.calculateTopPadding(),
|
||||||
|
start = 8.dp,
|
||||||
|
end = 8.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.shadow(
|
||||||
|
elevation = 20.dp
|
||||||
|
)
|
||||||
|
.background(Color.White.copy(alpha = 0.7f))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_more_horizon),
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.noRippleClickable {
|
||||||
|
expanded = true
|
||||||
|
},
|
||||||
|
tint = Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
width = 250,
|
||||||
|
menuItems = listOf(
|
||||||
|
MenuItem(
|
||||||
|
stringResource(R.string.logout),
|
||||||
|
R.mipmap.rider_pro_logout
|
||||||
|
) {
|
||||||
|
expanded = false
|
||||||
|
scope.launch {
|
||||||
|
onLogout()
|
||||||
|
navController.navigate(NavigationRoute.Login.route) {
|
||||||
|
popUpTo(NavigationRoute.Index.route) {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MenuItem(
|
||||||
|
stringResource(R.string.change_password),
|
||||||
|
R.mipmap.rider_pro_change_password
|
||||||
|
) {
|
||||||
|
expanded = false
|
||||||
|
scope.launch {
|
||||||
|
navController.navigate(NavigationRoute.ChangePasswordScreen.route)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MenuItem(
|
||||||
|
stringResource(R.string.favourites),
|
||||||
|
R.drawable.rider_pro_favourite
|
||||||
|
) {
|
||||||
|
expanded = false
|
||||||
|
scope.launch {
|
||||||
|
navController.navigate(NavigationRoute.FavouriteList.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
// user info
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
// 个人信息
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
profile?.let {
|
||||||
|
UserItem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
profile?.let {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
if (isSelf) {
|
||||||
|
SelfProfileAction {
|
||||||
|
navController.navigate(NavigationRoute.AccountEdit.route)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
OtherProfileAction(
|
||||||
|
it,
|
||||||
|
onFollow = {
|
||||||
|
onFollowClick()
|
||||||
|
},
|
||||||
|
onChat = {
|
||||||
|
onChatClick()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collapsed bar
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
val miniBarAlpha =
|
||||||
|
if (rootScrollState.value >= parentScrollThreshold - remainScrollThreshold) {
|
||||||
|
1f
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.alpha(miniBarAlpha)
|
||||||
|
// 保持在最低高度和当前高度之间
|
||||||
|
.background(Color(0xfff8f8f8))
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.onGloballyPositioned {
|
||||||
|
remainScrollThreshold = it.size.height
|
||||||
|
}
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
LocalContext.current,
|
||||||
|
profile?.avatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
text = profile?.nickName ?: "",
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 页面指示器
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
UserContentPageIndicator(pagerState)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(900.dp)
|
||||||
|
) { page ->
|
||||||
|
when (page) {
|
||||||
|
0 -> {
|
||||||
|
LazyVerticalStaggeredGrid(
|
||||||
|
columns = StaggeredGridCells.Fixed(2),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
|
8.dp
|
||||||
|
),
|
||||||
|
verticalItemSpacing = 8.dp,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = gridScrollState,
|
||||||
|
contentPadding = PaddingValues(8.dp)
|
||||||
|
) {
|
||||||
|
items(1) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(0.75f)
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.background(Color.White)
|
||||||
|
.padding(8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
NewPostViewModel.asNewPost()
|
||||||
|
navController.navigate(NavigationRoute.NewPost.route)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.background(Color(0xfff5f5f5))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(moments.itemCount) { idx ->
|
||||||
|
val moment = moments[idx] ?: return@items
|
||||||
|
GalleryItem(moment)
|
||||||
|
}
|
||||||
|
items(2) {
|
||||||
|
Spacer(modifier = Modifier.height(120.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxHeight(),
|
||||||
|
state = scrollState
|
||||||
|
) {
|
||||||
|
if (moments.itemCount == 0) {
|
||||||
|
item {
|
||||||
|
EmptyMomentPostUnit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
for (idx in 0 until moments.itemCount) {
|
||||||
|
val moment = moments[idx]
|
||||||
|
moment?.let {
|
||||||
|
MomentPostUnit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(120.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.aiosman.riderpro.ui.index.tabs.profile.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.aiosman.riderpro.LocalNavController
|
||||||
|
import com.aiosman.riderpro.entity.MomentEntity
|
||||||
|
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.riderpro.ui.navigateToPost
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GalleryItem(
|
||||||
|
moment: MomentEntity,
|
||||||
|
idx: Int = 0
|
||||||
|
){
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(
|
||||||
|
if (idx % 3 == 0) 1.5f else 1f
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateToPost(
|
||||||
|
moment.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
LocalContext.current,
|
||||||
|
moment.images[0].thumbnail,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
package com.aiosman.riderpro.ui.index.tabs.profile.composable
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.aspectRatio
|
||||||
|
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.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
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.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.riderpro.LocalNavController
|
||||||
|
import com.aiosman.riderpro.R
|
||||||
|
import com.aiosman.riderpro.entity.MomentEntity
|
||||||
|
import com.aiosman.riderpro.exp.formatPostTime2
|
||||||
|
import com.aiosman.riderpro.ui.NavigationRoute
|
||||||
|
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
|
import com.aiosman.riderpro.ui.navigateToPost
|
||||||
|
import com.aiosman.riderpro.ui.post.NewPostViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmptyMomentPostUnit() {
|
||||||
|
TimeGroup(stringResource(R.string.empty_my_post_title))
|
||||||
|
ProfileEmptyMomentCard()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProfileEmptyMomentCard(
|
||||||
|
|
||||||
|
) {
|
||||||
|
var columnHeight by remember { mutableStateOf(0) }
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 24.dp, top = 18.dp, end = 24.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(with(LocalDensity.current) { columnHeight.toDp() })
|
||||||
|
.width(14.dp)
|
||||||
|
) {
|
||||||
|
drawLine(
|
||||||
|
color = Color(0xff899DA9),
|
||||||
|
start = Offset(0f, 0f),
|
||||||
|
end = Offset(0f, size.height),
|
||||||
|
strokeWidth = 4f,
|
||||||
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
columnHeight = coordinates.size.height
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.empty_my_post_content), fontSize = 16.sp)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(3f / 2f)
|
||||||
|
.background(Color.White)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF5F5F5))
|
||||||
|
.noRippleClickable {
|
||||||
|
NewPostViewModel.asNewPost()
|
||||||
|
navController.navigate(NavigationRoute.NewPost.route)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
tint = Color(0xFFD8D8D8),
|
||||||
|
contentDescription = "New post",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MomentPostUnit(momentEntity: MomentEntity) {
|
||||||
|
TimeGroup(momentEntity.time.formatPostTime2())
|
||||||
|
ProfileMomentCard(
|
||||||
|
momentEntity.momentTextContent,
|
||||||
|
momentEntity.images[0].thumbnail,
|
||||||
|
momentEntity.likeCount.toString(),
|
||||||
|
momentEntity.commentCount.toString(),
|
||||||
|
momentEntity = momentEntity
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TimeGroup(time: String = "2024.06.08 12:23") {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 24.dp, top = 40.dp, end = 24.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(16.dp)
|
||||||
|
.width(14.dp),
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_moment_time_flag),
|
||||||
|
contentDescription = ""
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = time,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = Color.Black,
|
||||||
|
style = TextStyle(fontWeight = FontWeight.W600)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProfileMomentCard(
|
||||||
|
content: String,
|
||||||
|
imageUrl: String,
|
||||||
|
like: String,
|
||||||
|
comment: String,
|
||||||
|
momentEntity: MomentEntity
|
||||||
|
) {
|
||||||
|
var columnHeight by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 24.dp, top = 18.dp, end = 24.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(with(LocalDensity.current) { columnHeight.toDp() })
|
||||||
|
.width(14.dp)
|
||||||
|
) {
|
||||||
|
drawLine(
|
||||||
|
color = Color(0xff899DA9),
|
||||||
|
start = Offset(0f, 0f),
|
||||||
|
end = Offset(0f, size.height),
|
||||||
|
strokeWidth = 4f,
|
||||||
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(Color.White)
|
||||||
|
.weight(1f)
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
columnHeight = coordinates.size.height
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (content.isNotEmpty()) {
|
||||||
|
MomentCardTopContent(content)
|
||||||
|
}
|
||||||
|
MomentCardPicture(imageUrl, momentEntity = momentEntity)
|
||||||
|
MomentCardOperation(like, comment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MomentCardTopContent(content: String) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(top = 16.dp, bottom = 0.dp, start = 16.dp, end = 16.dp),
|
||||||
|
text = content, fontSize = 16.sp, color = Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
CustomAsyncImage(
|
||||||
|
context,
|
||||||
|
imageUrl,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.aspectRatio(3f / 2f)
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigateToPost(
|
||||||
|
id = momentEntity.id,
|
||||||
|
highlightCommentId = 0,
|
||||||
|
initImagePagerIndex = 0
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MomentCardOperation(like: String, comment: String) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
MomentCardOperationItem(
|
||||||
|
drawable = R.drawable.rider_pro_like,
|
||||||
|
number = like,
|
||||||
|
modifier = Modifier.padding(end = 32.dp)
|
||||||
|
)
|
||||||
|
MomentCardOperationItem(
|
||||||
|
drawable = R.drawable.rider_pro_moment_comment,
|
||||||
|
number = comment,
|
||||||
|
modifier = Modifier.padding(end = 32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MomentCardOperationItem(@DrawableRes drawable: Int, number: String, modifier: Modifier) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.padding(start = 16.dp, end = 8.dp),
|
||||||
|
painter = painterResource(id = drawable), contentDescription = ""
|
||||||
|
)
|
||||||
|
Text(text = number)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.aiosman.riderpro.ui.index.tabs.profile.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.riderpro.R
|
||||||
|
import com.aiosman.riderpro.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OtherProfileAction(
|
||||||
|
profile: AccountProfileEntity,
|
||||||
|
onFollow: (() -> Unit)? = null,
|
||||||
|
onChat: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(32.dp))
|
||||||
|
.background(if (profile.isFollowing) Color(0xffebebeb) else Color(0xffda3833))
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
onFollow?.invoke()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
tint = if (profile.isFollowing) Color.Black else Color.White
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(if (profile.isFollowing) R.string.following_upper else R.string.follow_upper),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = if (profile.isFollowing) Color.Black else Color.White,
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(32.dp))
|
||||||
|
.background(Color(0xffebebeb))
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
onChat?.invoke()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.rider_pro_comment),
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.chat_upper),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = Color.Black,
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按钮
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.aiosman.riderpro.ui.index.tabs.profile.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.riderpro.R
|
||||||
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelfProfileAction(
|
||||||
|
onEditProfile: () -> Unit
|
||||||
|
) {
|
||||||
|
// 按钮
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(32.dp))
|
||||||
|
.background(Color(0xffebebeb))
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
onEditProfile()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Edit,
|
||||||
|
contentDescription = "",
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.edit_profile),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = Color.Black,
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.aiosman.riderpro.ui.index.tabs.profile.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.riderpro.R
|
||||||
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun UserContentPageIndicator(
|
||||||
|
pagerState: PagerState
|
||||||
|
){
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(32.dp))
|
||||||
|
.background(if (pagerState.currentPage == 0) Color(0xFFFFFFFF) else Color.Transparent)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
// switch to gallery
|
||||||
|
scope.launch {
|
||||||
|
pagerState.scrollToPage(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.gallery),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = Color.Black,
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(32.dp))
|
||||||
|
.background(if (pagerState.currentPage == 1) Color(0xFFFFFFFF) else Color.Transparent)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
.noRippleClickable {
|
||||||
|
// switch to moments
|
||||||
|
scope.launch {
|
||||||
|
pagerState.scrollToPage(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.moment),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
color = Color.Black,
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.aiosman.riderpro.ui.index.tabs.profile.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.aiosman.riderpro.LocalNavController
|
||||||
|
import com.aiosman.riderpro.R
|
||||||
|
import com.aiosman.riderpro.entity.AccountProfileEntity
|
||||||
|
import com.aiosman.riderpro.ui.NavigationRoute
|
||||||
|
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||||
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UserItem(accountProfileEntity: AccountProfileEntity) {
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// 头像
|
||||||
|
CustomAsyncImage(
|
||||||
|
LocalContext.current,
|
||||||
|
accountProfileEntity.avatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.size(48.dp),
|
||||||
|
contentDescription = "",
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(32.dp))
|
||||||
|
//个人统计
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigate(
|
||||||
|
NavigationRoute.FollowerList.route.replace(
|
||||||
|
"{id}",
|
||||||
|
accountProfileEntity.id.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = accountProfileEntity.followerCount.toString(),
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.followers_upper),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.noRippleClickable {
|
||||||
|
navController.navigate(
|
||||||
|
NavigationRoute.FollowingList.route.replace(
|
||||||
|
"{id}",
|
||||||
|
accountProfileEntity.id.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = accountProfileEntity.followingCount.toString(),
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.following_upper),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
// 昵称
|
||||||
|
Text(
|
||||||
|
text = accountProfileEntity.nickName,
|
||||||
|
fontWeight = FontWeight.W600,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
// 个人简介
|
||||||
|
Text(
|
||||||
|
text = accountProfileEntity.bio,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -17,8 +16,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -28,7 +25,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -44,7 +40,6 @@ import com.aiosman.riderpro.ui.NavigationRoute
|
|||||||
import com.aiosman.riderpro.ui.composables.ActionButton
|
import com.aiosman.riderpro.ui.composables.ActionButton
|
||||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||||
import com.aiosman.riderpro.utils.GoogleLogin
|
import com.aiosman.riderpro.utils.GoogleLogin
|
||||||
import com.google.accompanist.systemuicontroller.SystemUiController
|
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import com.aiosman.riderpro.LocalNavController
|
import com.aiosman.riderpro.LocalNavController
|
||||||
import com.aiosman.riderpro.exp.viewModelFactory
|
import com.aiosman.riderpro.exp.viewModelFactory
|
||||||
import com.aiosman.riderpro.ui.index.tabs.profile.Profile
|
import com.aiosman.riderpro.ui.index.tabs.profile.Profile
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.ProfileV2
|
||||||
|
import com.aiosman.riderpro.ui.index.tabs.profile.ProfileV3
|
||||||
import com.aiosman.riderpro.ui.navigateToChat
|
import com.aiosman.riderpro.ui.navigateToChat
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -17,7 +19,7 @@ fun AccountProfileV2(id: String){
|
|||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
model.loadProfile(id)
|
model.loadProfile(id)
|
||||||
}
|
}
|
||||||
Profile(
|
ProfileV3(
|
||||||
sharedFlow = model.momentsFlow,
|
sharedFlow = model.momentsFlow,
|
||||||
profile = model.profile,
|
profile = model.profile,
|
||||||
isSelf = false,
|
isSelf = false,
|
||||||
|
|||||||
Reference in New Issue
Block a user