更新登录UI

This commit is contained in:
2024-10-02 20:55:24 +08:00
parent 45a698003f
commit 66aaaa37d7
20 changed files with 2898 additions and 574 deletions

View File

@@ -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

View File

@@ -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)
)
}
}
}
}

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -0,0 +1,6 @@
package com.aiosman.riderpro.ui.composables.toolbar
enum class FabPosition {
Center,
End
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -6,8 +6,6 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -20,7 +18,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -41,11 +38,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -59,64 +54,41 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.R import com.aiosman.riderpro.R
import com.aiosman.riderpro.entity.AccountProfileEntity import com.aiosman.riderpro.entity.AccountProfileEntity
import com.aiosman.riderpro.entity.MomentEntity import com.aiosman.riderpro.entity.MomentEntity
import com.aiosman.riderpro.exp.formatPostTime2
import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.CustomAsyncImage import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.DropdownMenu import com.aiosman.riderpro.ui.composables.DropdownMenu
import com.aiosman.riderpro.ui.composables.MenuItem import com.aiosman.riderpro.ui.composables.MenuItem
import com.aiosman.riderpro.ui.composables.StatusBarSpacer import com.aiosman.riderpro.ui.composables.StatusBarSpacer
import com.aiosman.riderpro.ui.index.tabs.profile.composable.EmptyMomentPostUnit
import com.aiosman.riderpro.ui.index.tabs.profile.composable.GalleryItem
import com.aiosman.riderpro.ui.index.tabs.profile.composable.MomentPostUnit
import com.aiosman.riderpro.ui.index.tabs.profile.composable.OtherProfileAction
import com.aiosman.riderpro.ui.index.tabs.profile.composable.SelfProfileAction
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserContentPageIndicator
import com.aiosman.riderpro.ui.index.tabs.profile.composable.UserItem
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.navigateToPost
import com.aiosman.riderpro.ui.post.NewPostViewModel import com.aiosman.riderpro.ui.post.NewPostViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable
fun ProfileWrap(
) {
LaunchedEffect(Unit) {
MyProfileViewModel.loadProfile()
}
Profile(
onUpdateBanner = { uri, context ->
MyProfileViewModel.updateUserProfileBanner(uri, context)
},
onLogout = {
MyProfileViewModel.viewModelScope.launch {
MyProfileViewModel.logout()
}
},
profile = MyProfileViewModel.profile,
sharedFlow = MyProfileViewModel.sharedFlow
)
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun Profile( fun Profile(
@@ -188,14 +160,6 @@ fun Profile(
return Offset(x = 0f, y = consumedHeader / speedFactor) return Offset(x = 0f, y = consumedHeader / speedFactor)
} }
// override fun onPostScroll(
// consumed: Offset,
// available: Offset,
// source: NestedScrollSource
// ): Offset {
// return Offset.Zero
// }
} }
} }
@@ -393,8 +357,6 @@ fun Profile(
.alpha(1 - alpha) .alpha(1 - alpha)
.background(Color(0xfff8f8f8)) .background(Color(0xfff8f8f8))
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
StatusBarSpacer() StatusBarSpacer()
@@ -425,57 +387,7 @@ fun Profile(
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Row( UserContentPageIndicator(pagerState)
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.clip(RoundedCornerShape(32.dp))
.background(if (pagerState.currentPage == 0) Color(0xFFFFFFFF) else Color.Transparent)
.padding(horizontal = 16.dp, vertical = 4.dp)
.noRippleClickable {
// switch to gallery
scope.launch {
pagerState.scrollToPage(0)
}
}
) {
Text(
text = stringResource(R.string.gallery),
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = Color.Black,
modifier = Modifier.padding(8.dp)
)
}
Spacer(modifier = Modifier.width(4.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.clip(RoundedCornerShape(32.dp))
.background(if (pagerState.currentPage == 1) Color(0xFFFFFFFF) else Color.Transparent)
.padding(horizontal = 16.dp, vertical = 8.dp)
.noRippleClickable {
// switch to moments
scope.launch {
pagerState.scrollToPage(1)
}
}
) {
Text(
text = stringResource(R.string.moment),
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = Color.Black,
modifier = Modifier.padding(8.dp)
)
}
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
@@ -530,30 +442,7 @@ fun Profile(
} }
items(moments.itemCount) { idx -> items(moments.itemCount) { idx ->
val moment = moments[idx] ?: return@items val moment = moments[idx] ?: return@items
Box( GalleryItem(moment)
modifier = Modifier
.fillMaxWidth()
.aspectRatio(
if (idx % 3 == 0) 1.5f else 1f
)
.clip(RoundedCornerShape(8.dp))
.noRippleClickable {
navController.navigateToPost(
moment.id
)
}
) {
CustomAsyncImage(
LocalContext.current,
moment.images[0].thumbnail,
modifier = Modifier
.fillMaxSize(),
contentDescription = "",
contentScale = ContentScale.Crop,
)
}
} }
items(2) { items(2) {
Spacer(modifier = Modifier.height(120.dp)) Spacer(modifier = Modifier.height(120.dp))
@@ -563,14 +452,15 @@ fun Profile(
1 -> { 1 -> {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxHeight(), modifier = Modifier.fillMaxSize(),
state = scrollState state = scrollState
) { ) {
item { if (moments.itemCount == 0) {
if (moments.itemCount == 0) { item {
EmptyMomentPostUnit() EmptyMomentPostUnit()
} }
} }
item { item {
for (idx in 0 until moments.itemCount) { for (idx in 0 until moments.itemCount) {
val moment = moments[idx] val moment = moments[idx]
@@ -590,449 +480,4 @@ fun Profile(
} }
} }
@Composable
fun UserItem(accountProfileEntity: AccountProfileEntity) {
val navController = LocalNavController.current
Column(
modifier = Modifier
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
// 头像
CustomAsyncImage(
LocalContext.current,
accountProfileEntity.avatar,
modifier = Modifier
.clip(CircleShape)
.size(48.dp),
contentDescription = "",
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(32.dp))
//个人统计
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.noRippleClickable {
navController.navigate(
NavigationRoute.FollowerList.route.replace(
"{id}",
accountProfileEntity.id.toString()
)
)
}
) {
Text(
text = accountProfileEntity.followerCount.toString(),
fontWeight = FontWeight.W600,
fontSize = 16.sp
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.followers_upper),
)
}
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.weight(1f)
.noRippleClickable {
navController.navigate(
NavigationRoute.FollowingList.route.replace(
"{id}",
accountProfileEntity.id.toString()
)
)
},
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = accountProfileEntity.followingCount.toString(),
fontWeight = FontWeight.W600,
fontSize = 16.sp
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.following_upper),
)
}
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
}
}
}
Spacer(modifier = Modifier.height(12.dp))
// 昵称
Text(
text = accountProfileEntity.nickName,
fontWeight = FontWeight.W600,
fontSize = 16.sp,
)
Spacer(modifier = Modifier.height(4.dp))
// 个人简介
Text(
text = accountProfileEntity.bio,
fontSize = 14.sp,
color = Color.Gray
)
}
}
@Composable
fun SelfProfileAction(
onEditProfile: () -> Unit
) {
// 按钮
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.clip(RoundedCornerShape(32.dp))
.background(Color(0xffebebeb))
.padding(horizontal = 16.dp, vertical = 4.dp)
.noRippleClickable {
onEditProfile()
}
) {
Icon(
Icons.Default.Edit,
contentDescription = "",
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = stringResource(R.string.edit_profile),
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = Color.Black,
modifier = Modifier.padding(8.dp)
)
}
}
@Composable
fun OtherProfileAction(
profile: AccountProfileEntity,
onFollow: (() -> Unit)? = null,
onChat: (() -> Unit)? = null
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.clip(RoundedCornerShape(32.dp))
.background(if (profile.isFollowing) Color(0xffebebeb) else Color(0xffda3833))
.padding(horizontal = 16.dp, vertical = 4.dp)
.noRippleClickable {
onFollow?.invoke()
}
) {
Icon(
Icons.Default.Add,
contentDescription = "",
modifier = Modifier.size(24.dp),
tint = if (profile.isFollowing) Color.Black else Color.White
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = stringResource(if (profile.isFollowing) R.string.following_upper else R.string.follow_upper),
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = if (profile.isFollowing) Color.Black else Color.White,
modifier = Modifier.padding(8.dp),
)
}
Spacer(modifier = Modifier.width(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.clip(RoundedCornerShape(32.dp))
.background(Color(0xffebebeb))
.padding(horizontal = 16.dp, vertical = 4.dp)
.noRippleClickable {
onChat?.invoke()
}
) {
Image(
painter = painterResource(id = R.drawable.rider_pro_comment),
contentDescription = "",
modifier = Modifier.size(24.dp),
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = stringResource(R.string.chat_upper),
fontSize = 14.sp,
fontWeight = FontWeight.W600,
color = Color.Black,
modifier = Modifier.padding(8.dp),
)
}
}
// 按钮
}
@Composable
fun EmptyMomentPostUnit() {
TimeGroup(stringResource(R.string.empty_my_post_title))
ProfileEmptyMomentCard()
}
@Composable
fun ProfileEmptyMomentCard(
) {
var columnHeight by remember { mutableStateOf(0) }
val navController = LocalNavController.current
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, top = 18.dp, end = 24.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
) {
Canvas(
modifier = Modifier
.height(with(LocalDensity.current) { columnHeight.toDp() })
.width(14.dp)
) {
drawLine(
color = Color(0xff899DA9),
start = Offset(0f, 0f),
end = Offset(0f, size.height),
strokeWidth = 4f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
)
}
Spacer(modifier = Modifier.width(10.dp))
Column(
modifier = Modifier
.weight(1f)
.onGloballyPositioned { coordinates ->
columnHeight = coordinates.size.height
}
) {
Text(stringResource(R.string.empty_my_post_content), fontSize = 16.sp)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(3f / 2f)
.background(Color.White)
.padding(16.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF5F5F5))
.noRippleClickable {
NewPostViewModel.asNewPost()
navController.navigate(NavigationRoute.NewPost.route)
}
) {
Icon(
Icons.Default.Add,
tint = Color(0xFFD8D8D8),
contentDescription = "New post",
modifier = Modifier
.size(32.dp)
.align(Alignment.Center)
)
}
}
}
}
}
}
@Composable
fun MomentPostUnit(momentEntity: MomentEntity) {
TimeGroup(momentEntity.time.formatPostTime2())
ProfileMomentCard(
momentEntity.momentTextContent,
momentEntity.images[0].thumbnail,
momentEntity.likeCount.toString(),
momentEntity.commentCount.toString(),
momentEntity = momentEntity
)
}
@Composable
fun TimeGroup(time: String = "2024.06.08 12:23") {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, top = 40.dp, end = 24.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier
.height(16.dp)
.width(14.dp),
painter = painterResource(id = R.drawable.rider_pro_moment_time_flag),
contentDescription = ""
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = time,
fontSize = 16.sp,
color = Color.Black,
style = TextStyle(fontWeight = FontWeight.W600)
)
}
}
@Composable
fun ProfileMomentCard(
content: String,
imageUrl: String,
like: String,
comment: String,
momentEntity: MomentEntity
) {
var columnHeight by remember { mutableStateOf(0) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, top = 18.dp, end = 24.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
) {
Canvas(
modifier = Modifier
.height(with(LocalDensity.current) { columnHeight.toDp() })
.width(14.dp)
) {
drawLine(
color = Color(0xff899DA9),
start = Offset(0f, 0f),
end = Offset(0f, size.height),
strokeWidth = 4f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 20f), 0f)
)
}
Spacer(modifier = Modifier.width(10.dp))
Column(
modifier = Modifier
.background(Color.White)
.weight(1f)
.onGloballyPositioned { coordinates ->
columnHeight = coordinates.size.height
}
) {
if (content.isNotEmpty()) {
MomentCardTopContent(content)
}
MomentCardPicture(imageUrl, momentEntity = momentEntity)
MomentCardOperation(like, comment)
}
}
}
}
@Composable
fun MomentCardTopContent(content: String) {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(top = 16.dp, bottom = 0.dp, start = 16.dp, end = 16.dp),
text = content, fontSize = 16.sp, color = Color.Black
)
}
}
@Composable
fun MomentCardPicture(imageUrl: String, momentEntity: MomentEntity) {
val navController = LocalNavController.current
val context = LocalContext.current
CustomAsyncImage(
context,
imageUrl,
modifier = Modifier
.fillMaxSize()
.aspectRatio(3f / 2f)
.padding(top = 16.dp)
.noRippleClickable {
navController.navigateToPost(
id = momentEntity.id,
highlightCommentId = 0,
initImagePagerIndex = 0
)
},
contentDescription = "",
contentScale = ContentScale.Crop
)
}
@Composable
fun MomentCardOperation(like: String, comment: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
// Spacer(modifier = Modifier.weight(1f))
MomentCardOperationItem(
drawable = R.drawable.rider_pro_like,
number = like,
modifier = Modifier.padding(end = 32.dp)
)
MomentCardOperationItem(
drawable = R.drawable.rider_pro_moment_comment,
number = comment,
modifier = Modifier.padding(end = 32.dp)
)
}
}
@Composable
fun MomentCardOperationItem(@DrawableRes drawable: Int, number: String, modifier: Modifier) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier.padding(start = 16.dp, end = 8.dp),
painter = painterResource(id = drawable), contentDescription = ""
)
Text(text = number)
}
}

View File

@@ -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))
}
}
}
}
}
}
}
}

View File

@@ -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
)
}

View File

@@ -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))
}
}
}
}
}
}
}
}

View File

@@ -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,
)
}
}

View File

@@ -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)
}
}

View File

@@ -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),
)
}
}
// 按钮
}

View File

@@ -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)
)
}
}

View File

@@ -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)
)
}
}
}

View File

@@ -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
)
}
}

View File

@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -17,8 +16,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -28,7 +25,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -44,7 +40,6 @@ import com.aiosman.riderpro.ui.NavigationRoute
import com.aiosman.riderpro.ui.composables.ActionButton import com.aiosman.riderpro.ui.composables.ActionButton
import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.utils.GoogleLogin import com.aiosman.riderpro.utils.GoogleLogin
import com.google.accompanist.systemuicontroller.SystemUiController
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@@ -6,6 +6,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.exp.viewModelFactory import com.aiosman.riderpro.exp.viewModelFactory
import com.aiosman.riderpro.ui.index.tabs.profile.Profile import com.aiosman.riderpro.ui.index.tabs.profile.Profile
import com.aiosman.riderpro.ui.index.tabs.profile.ProfileV2
import com.aiosman.riderpro.ui.index.tabs.profile.ProfileV3
import com.aiosman.riderpro.ui.navigateToChat import com.aiosman.riderpro.ui.navigateToChat
@Composable @Composable
@@ -17,7 +19,7 @@ fun AccountProfileV2(id: String){
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
model.loadProfile(id) model.loadProfile(id)
} }
Profile( ProfileV3(
sharedFlow = model.momentsFlow, sharedFlow = model.momentsFlow,
profile = model.profile, profile = model.profile,
isSelf = false, isSelf = false,