[Room list] Search & menu improvements (#356)

* Remove settings menu item, start splitting search UI. Also, add `applyIf` and `circularReveal` modifiers.

* Split UI & logic for room list search

* Suppress `composed` warning, improve its debuggability

* Add content description to the user's avatar, fix window insets. Also, remove unused `SearchRoomListTopBar`.
This commit is contained in:
Jorge Martin Espinosa 2023-04-25 13:35:36 +02:00 committed by GitHub
parent b59ea181eb
commit 0234553bca
31 changed files with 534 additions and 199 deletions

View file

@ -41,7 +41,11 @@ import io.element.android.libraries.designsystem.theme.components.Text
import timber.log.Timber
@Composable
fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
fun Avatar(
avatarData: AvatarData,
modifier: Modifier = Modifier,
contentDescription: String? = null,
) {
val commonModifier = modifier
.size(avatarData.size.dp)
.clip(CircleShape)
@ -54,6 +58,7 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
ImageAvatar(
avatarData = avatarData,
modifier = commonModifier,
contentDescription = contentDescription,
)
}
}
@ -62,13 +67,14 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
private fun ImageAvatar(
avatarData: AvatarData,
modifier: Modifier = Modifier,
contentDescription: String? = null,
) {
AsyncImage(
model = avatarData,
onError = {
Timber.e("TAG", "Error $it\n${it.result}", it.result.throwable)
},
contentDescription = null,
contentDescription = contentDescription,
contentScale = ContentScale.Crop,
placeholder = debugPlaceholderAvatar(),
modifier = modifier
@ -89,7 +95,7 @@ private fun InitialsAvatar(
end = Offset(100f, 0f)
)
Box(
modifier.background(brush = initialsGradient)
modifier.background(brush = initialsGradient),
) {
Text(
modifier = Modifier.align(Alignment.Center),

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.modifiers
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.platform.debugInspectorInfo
/**
* Applies the [ifTrue] modifier when the [condition] is true, [ifFalse] otherwise.
*/
@SuppressLint("UnnecessaryComposedModifier") // It's actually necessary due to the `@Composable` lambdas
fun Modifier.applyIf(
condition: Boolean,
ifTrue: @Composable Modifier.() -> Modifier,
ifFalse: @Composable (Modifier.() -> Modifier)? = null
): Modifier =
composed(
inspectorInfo = debugInspectorInfo {
name = "applyIf"
value = condition
}
) {
when {
condition -> then(ifTrue(Modifier))
ifFalse != null -> then(ifFalse(Modifier))
else -> this
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.modifiers
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.platform.debugInspectorInfo
import kotlin.math.sqrt
// Note: these modifiers come from https://gist.github.com/darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8
/**
* A modifier that clips the composable content using an animated circle. The circle will
* expand/shrink with an animation whenever [visible] changes.
*
* For more fine-grained control over the transition, see this method's overload, which allows passing
* a [State] object to control the progress of the reveal animation.
*
* By default, the circle is centered in the content, but custom positions may be specified using
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).*/
fun Modifier.circularReveal(
visible: Boolean,
showScrim: Boolean = false,
revealFrom: Offset = Offset(0.5f, 0.5f),
): Modifier = composed(
factory = {
val factor = updateTransition(visible, label = "Visibility")
.animateFloat(label = "revealFactor") { if (it) 1f else 0f }
circularReveal(factor, showScrim, revealFrom)
},
inspectorInfo = debugInspectorInfo {
name = "circularReveal"
properties["visible"] = visible
properties["revealFrom"] = revealFrom
}
)
/**
* A modifier that clips the composable content using a circular shape. The radius of the circle
* will be determined by the [transitionProgress].
*
* The values of the progress should be between 0 and 1.
*
* By default, the circle is centered in the content, but custom positions may be specified using
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).
* */
fun Modifier.circularReveal(
transitionProgress: State<Float>,
showScrim: Boolean = false,
revealFrom: Offset = Offset(0.5f, 0.5f)
): Modifier {
return drawWithCache {
val path = Path()
val center = revealFrom.mapTo(size)
val radius = calculateRadius(revealFrom, size)
val scrimColor = if (showScrim)
Color.Gray
else
Color.Transparent
path.addOval(Rect(center, radius * transitionProgress.value))
onDrawWithContent {
if (showScrim) {
drawRect(scrimColor, alpha = transitionProgress.value * 0.75f)
}
clipPath(path) { this@onDrawWithContent.drawContent() }
}
}
}
private fun Offset.mapTo(size: Size): Offset {
return Offset(x * size.width, y * size.height)
}
private fun calculateRadius(normalizedOrigin: Offset, size: Size) = with(normalizedOrigin) {
val x = (if (x > 0.5f) x else 1 - x) * size.width
val y = (if (y > 0.5f) y else 1 - y) * size.height
sqrt(x * x + y * y)
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.utils
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
@Composable
fun WindowInsets.copy(
top: Int? = null,
right: Int? = null,
bottom: Int? = null,
left: Int? = null
): WindowInsets {
val density = LocalDensity.current
val direction = LocalLayoutDirection.current
return WindowInsets(
top = top ?: this.getTop(density),
right = right ?: this.getRight(density, direction),
bottom = bottom ?: this.getBottom(density),
left = left ?: this.getLeft(density, direction)
)
}