更新登录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 androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
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.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
|
||||
@@ -41,11 +38,9 @@ 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.material.icons.filled.Edit
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -59,64 +54,41 @@ 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.graphics.PathEffect
|
||||
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.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.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.exp.formatPostTime2
|
||||
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.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.navigateToPost
|
||||
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
|
||||
|
||||
@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)
|
||||
@Composable
|
||||
fun Profile(
|
||||
@@ -188,14 +160,6 @@ fun Profile(
|
||||
|
||||
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)
|
||||
.background(Color(0xfff8f8f8))
|
||||
.padding(horizontal = 16.dp)
|
||||
|
||||
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
|
||||
@@ -425,57 +387,7 @@ fun Profile(
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
UserContentPageIndicator(pagerState)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
@@ -530,30 +442,7 @@ fun Profile(
|
||||
}
|
||||
items(moments.itemCount) { idx ->
|
||||
val moment = moments[idx] ?: return@items
|
||||
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,
|
||||
)
|
||||
}
|
||||
GalleryItem(moment)
|
||||
}
|
||||
items(2) {
|
||||
Spacer(modifier = Modifier.height(120.dp))
|
||||
@@ -563,14 +452,15 @@ fun Profile(
|
||||
|
||||
1 -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = scrollState
|
||||
) {
|
||||
item {
|
||||
if (moments.itemCount == 0) {
|
||||
if (moments.itemCount == 0) {
|
||||
item {
|
||||
EmptyMomentPostUnit()
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
for (idx in 0 until moments.itemCount) {
|
||||
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.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.padding
|
||||
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.runtime.Composable
|
||||
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.Color
|
||||
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.font.FontWeight
|
||||
@@ -44,7 +40,6 @@ import com.aiosman.riderpro.ui.NavigationRoute
|
||||
import com.aiosman.riderpro.ui.composables.ActionButton
|
||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||
import com.aiosman.riderpro.utils.GoogleLogin
|
||||
import com.google.accompanist.systemuicontroller.SystemUiController
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.exp.viewModelFactory
|
||||
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
|
||||
|
||||
@Composable
|
||||
@@ -17,7 +19,7 @@ fun AccountProfileV2(id: String){
|
||||
LaunchedEffect(Unit) {
|
||||
model.loadProfile(id)
|
||||
}
|
||||
Profile(
|
||||
ProfileV3(
|
||||
sharedFlow = model.momentsFlow,
|
||||
profile = model.profile,
|
||||
isSelf = false,
|
||||
|
||||
Reference in New Issue
Block a user