[Compound] Bloom (#1253)
* Add `bloom` and `avatarBloom` modifiers. * Add `ConnectivityIndicatorContainer` to control the padding needed at the top. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
committed by
GitHub
parent
ddb8f66c32
commit
94dc6f41e2
1
changelog.d/1217.feature
Normal file
1
changelog.d/1217.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implement Bloom effect modifier.
|
||||
@@ -18,6 +18,9 @@ package io.element.android.features.networkmonitor.api.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
@@ -27,34 +30,44 @@ 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.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.WifiOff
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
* A view that displays a connectivity indicator when the device is offline, adding a default
|
||||
* padding to make sure the status bar is not overlapped.
|
||||
*/
|
||||
@Composable
|
||||
fun ConnectivityIndicatorView(
|
||||
isOnline: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline }
|
||||
val isStatusBarPaddingVisible = remember { MutableTransitionState(isOnline) }.apply { targetState = isOnline }
|
||||
@@ -78,6 +91,46 @@ fun ConnectivityIndicatorView(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A view that displays a connectivity indicator when the device is offline, passing the padding
|
||||
* needed to make sure the status bar is not overlapped to its content views.
|
||||
*/
|
||||
@Composable
|
||||
fun ConnectivityIndicatorContainer(
|
||||
isOnline: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable (topPadding: Dp) -> Unit,
|
||||
) {
|
||||
val isIndicatorVisible = remember { MutableTransitionState(!isOnline) }.apply { targetState = !isOnline }
|
||||
|
||||
val statusBarTopPadding = if (LocalInspectionMode.current) {
|
||||
// Needed to get valid UI previews
|
||||
24.dp
|
||||
} else {
|
||||
WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + 6.dp
|
||||
}
|
||||
val target = remember(isOnline) { if (isOnline) 0.dp else statusBarTopPadding }
|
||||
val animationStateOffset by animateDpAsState(
|
||||
targetValue = target,
|
||||
animationSpec = spring(
|
||||
stiffness = Spring.StiffnessMediumLow,
|
||||
visibilityThreshold = 1.dp,
|
||||
),
|
||||
label = "insets-animation",
|
||||
)
|
||||
|
||||
content(animationStateOffset)
|
||||
|
||||
// Display the network indicator with an animation
|
||||
AnimatedVisibility(
|
||||
visibleState = isIndicatorVisible,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
Indicator(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Indicator(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
|
||||
@@ -18,13 +18,13 @@ package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
@@ -45,7 +45,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorContainer
|
||||
import io.element.android.features.roomlist.impl.components.RequestVerificationHeader
|
||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||
import io.element.android.features.roomlist.impl.components.RoomListTopBar
|
||||
@@ -76,8 +76,10 @@ fun RoomListView(
|
||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
ConnectivityIndicatorContainer(
|
||||
modifier = modifier,
|
||||
isOnline = state.hasNetworkConnection,
|
||||
) { topPadding ->
|
||||
Box {
|
||||
fun onRoomLongClicked(
|
||||
roomListRoomSummary: RoomListRoomSummary
|
||||
@@ -96,6 +98,7 @@ fun RoomListView(
|
||||
LeaveRoomView(state = state.leaveRoomState)
|
||||
|
||||
RoomListContent(
|
||||
modifier = Modifier.padding(top = topPadding),
|
||||
state = state,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onRoomClicked = onRoomClicked,
|
||||
@@ -111,6 +114,8 @@ fun RoomListView(
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.padding(top = topPadding)
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
)
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
package io.element.android.features.roomlist.impl.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.outlined.BugReport
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -30,24 +33,37 @@ import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatarBloom
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.text.applyScaleDown
|
||||
import io.element.android.libraries.designsystem.text.roundToPx
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.text.toSp
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
|
||||
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
|
||||
@@ -60,6 +76,9 @@ import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.designsystem.R as CommonR
|
||||
|
||||
private val avatarBloomSize = 430.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -89,6 +108,7 @@ fun RoomListTopBar(
|
||||
|
||||
DefaultRoomListTopBar(
|
||||
matrixUser = matrixUser,
|
||||
areSearchResultsDisplayed = areSearchResultsDisplayed,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onSearchClicked = onToggleSearch,
|
||||
onMenuActionClicked = onMenuActionClicked,
|
||||
@@ -101,6 +121,7 @@ fun RoomListTopBar(
|
||||
@Composable
|
||||
private fun DefaultRoomListTopBar(
|
||||
matrixUser: MatrixUser?,
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
onOpenSettings: () -> Unit,
|
||||
onSearchClicked: () -> Unit,
|
||||
@@ -108,94 +129,139 @@ private fun DefaultRoomListTopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
MediumTopAppBar(
|
||||
modifier = modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
title = {
|
||||
val fontStyle = if (scrollBehavior.state.collapsedFraction > 0.5)
|
||||
ElementTheme.typography.aliasScreenTitle
|
||||
else
|
||||
ElementTheme.typography.fontHeadingLgBold.copy(
|
||||
// Due to a limitation of MediumTopAppBar, and to avoid the text to be truncated,
|
||||
// ensure that the font size will never be bigger than 28.dp.
|
||||
fontSize = 28.dp.applyScaleDown().toSp()
|
||||
)
|
||||
Text(
|
||||
style = fontStyle,
|
||||
text = stringResource(id = R.string.screen_roomlist_main_space_title)
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
if (matrixUser != null) {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag(TestTags.homeScreenSettings),
|
||||
onClick = onOpenSettings
|
||||
) {
|
||||
val avatarData by remember {
|
||||
derivedStateOf {
|
||||
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
}
|
||||
}
|
||||
Avatar(avatarData, contentDescription = stringResource(CommonStrings.common_settings))
|
||||
|
||||
// We need this to manually clip the top app bar in preview mode
|
||||
val previewAppBarHeight = if (LocalInspectionMode.current) {
|
||||
112.dp.roundToPx()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val collapsedFraction = scrollBehavior.state.collapsedFraction
|
||||
var appBarHeight by remember {
|
||||
mutableIntStateOf(previewAppBarHeight ?: 0)
|
||||
}
|
||||
|
||||
val avatarData by remember(matrixUser) {
|
||||
derivedStateOf {
|
||||
matrixUser?.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
}
|
||||
}
|
||||
|
||||
val statusBarPadding = with (LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
MediumTopAppBar(
|
||||
modifier = Modifier
|
||||
.onSizeChanged {
|
||||
appBarHeight = it.height
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = onSearchClicked,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = stringResource(CommonStrings.action_search),
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.avatarBloom(
|
||||
avatarData = avatarData,
|
||||
background = ElementTheme.materialColors.background,
|
||||
blurSize = DpSize(avatarBloomSize, avatarBloomSize),
|
||||
offset = DpOffset(24.dp, 24.dp + statusBarPadding),
|
||||
clipToSize = if (appBarHeight > 0) DpSize(
|
||||
avatarBloomSize,
|
||||
appBarHeight.toDp()
|
||||
) else DpSize.Unspecified,
|
||||
bottomSoftEdgeAlpha = 1f - collapsedFraction,
|
||||
alpha = if (areSearchResultsDisplayed) 0f else 1f,
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { showMenu = !showMenu }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
.statusBarsPadding(),
|
||||
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent,
|
||||
),
|
||||
title = {
|
||||
val fontStyle = if (scrollBehavior.state.collapsedFraction > 0.5)
|
||||
ElementTheme.typography.aliasScreenTitle
|
||||
else
|
||||
ElementTheme.typography.fontHeadingLgBold.copy(
|
||||
// Due to a limitation of MediumTopAppBar, and to avoid the text to be truncated,
|
||||
// ensure that the font size will never be bigger than 28.dp.
|
||||
fontSize = 28.dp.applyScaleDown().toSp()
|
||||
)
|
||||
Text(
|
||||
style = fontStyle,
|
||||
text = stringResource(id = R.string.screen_roomlist_main_space_title)
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.InviteFriends)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.action_invite)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.Share,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
},
|
||||
navigationIcon = {
|
||||
avatarData?.let {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag(TestTags.homeScreenSettings),
|
||||
onClick = onOpenSettings
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = it,
|
||||
contentDescription = stringResource(CommonStrings.common_settings),
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.ReportBug)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.common_report_a_bug)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.BugReport,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = onSearchClicked,
|
||||
) {
|
||||
Icon(
|
||||
resourceId = CommonR.drawable.ic_search,
|
||||
contentDescription = stringResource(CommonStrings.action_search),
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { showMenu = !showMenu }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.InviteFriends)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.action_invite)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.Share,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.ReportBug)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.common_report_a_bug)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Outlined.BugReport,
|
||||
tint = ElementTheme.materialColors.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.alpha(collapsedFraction)
|
||||
.align(Alignment.BottomCenter),
|
||||
color = ElementTheme.materialColors.outlineVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -211,6 +277,7 @@ internal fun DefaultRoomListTopBarDarkPreview() = ElementPreviewDark { DefaultRo
|
||||
private fun DefaultRoomListTopBarPreview() {
|
||||
DefaultRoomListTopBar(
|
||||
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
|
||||
areSearchResultsDisplayed = false,
|
||||
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
|
||||
onOpenSettings = {},
|
||||
onSearchClicked = {},
|
||||
|
||||
@@ -0,0 +1,534 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Build
|
||||
import android.text.TextPaint
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.center
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.ClipOp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.LinearGradientShader
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.RadialGradientShader
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.isSpecified
|
||||
import androidx.compose.ui.unit.toOffset
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.ImageRequest
|
||||
import com.airbnb.android.showkase.annotation.ShowkaseComposable
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
import io.element.android.libraries.designsystem.R
|
||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.MediumTopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Default bloom configuration values.
|
||||
*/
|
||||
object BloomDefaults {
|
||||
/**
|
||||
* Number of components to use with BlurHash to generate the blur effect.
|
||||
* Larger values mean more detailed blurs.
|
||||
*/
|
||||
const val HASH_COMPONENTS = 8
|
||||
|
||||
/** Default bloom layers. */
|
||||
@Composable
|
||||
fun defaultLayers() = persistentListOf(
|
||||
// Bottom layer
|
||||
if (isSystemInDarkTheme()) {
|
||||
BloomLayer(0.5f, BlendMode.Exclusion)
|
||||
} else {
|
||||
BloomLayer(0.2f, BlendMode.Hardlight)
|
||||
},
|
||||
// Top layer
|
||||
BloomLayer(if (isSystemInDarkTheme()) 0.2f else 0.8f, BlendMode.Color),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bloom layer configuration.
|
||||
* @param alpha The alpha value to apply to the layer.
|
||||
* @param blendMode The blend mode to apply to the layer.
|
||||
*/
|
||||
data class BloomLayer(
|
||||
val alpha: Float,
|
||||
val blendMode: BlendMode,
|
||||
)
|
||||
|
||||
/**
|
||||
* Bloom effect modifier. Applies a bloom effect to the component.
|
||||
* @param hash The BlurHash to use as the bloom source.
|
||||
* @param background The background color to use for the bloom effect. Since we use blend modes it must be non-transparent.
|
||||
* @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component.
|
||||
* @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
|
||||
* @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
|
||||
* @param layerConfiguration The configuration for the bloom layers. If not specified the default layers configuration will be used.
|
||||
* @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
|
||||
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
|
||||
* @param alpha The alpha value to apply to the bloom effect.
|
||||
*/
|
||||
fun Modifier.bloom(
|
||||
hash: String?,
|
||||
background: Color,
|
||||
blurSize: DpSize = DpSize.Unspecified,
|
||||
offset: DpOffset = DpOffset.Unspecified,
|
||||
clipToSize: DpSize = DpSize.Unspecified,
|
||||
layerConfiguration: ImmutableList<BloomLayer>? = null,
|
||||
bottomSoftEdgeHeight: Dp = 40.dp,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
bottomSoftEdgeAlpha: Float = 1.0f,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
alpha: Float = 1f,
|
||||
) = composed {
|
||||
val defaultLayers = BloomDefaults.defaultLayers()
|
||||
val layers = layerConfiguration ?: defaultLayers
|
||||
// Bloom only works on API 29+
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this
|
||||
if (hash == null) return@composed this
|
||||
|
||||
val hashedBitmap = remember(hash) {
|
||||
BlurHash.decode(hash, BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)?.asImageBitmap()
|
||||
} ?: return@composed this
|
||||
val density = LocalDensity.current
|
||||
val pixelSize = remember(blurSize, density) { blurSize.toIntSize(density) }
|
||||
val clipToPixelSize = remember(clipToSize, density) { clipToSize.toIntSize(density) }
|
||||
val bottomSoftEdgeHeightPixels = remember(bottomSoftEdgeHeight, density) { with(density) { bottomSoftEdgeHeight.roundToPx() } }
|
||||
val isRTL = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
drawWithCache {
|
||||
val dstSize = if (pixelSize != IntSize.Zero) {
|
||||
pixelSize
|
||||
} else {
|
||||
IntSize(size.width.toInt(), size.height.toInt())
|
||||
}
|
||||
// Calculate where to place the center of the bloom effect
|
||||
val centerOffset = if (offset.isSpecified) {
|
||||
if (isRTL) {
|
||||
IntOffset(
|
||||
size.width.roundToInt() - offset.x.roundToPx(),
|
||||
size.height.roundToInt() - offset.y.roundToPx(),
|
||||
)
|
||||
} else {
|
||||
IntOffset(
|
||||
offset.x.roundToPx(),
|
||||
offset.y.roundToPx(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IntOffset(
|
||||
size.center.x.toInt(),
|
||||
size.center.y.toInt(),
|
||||
)
|
||||
}
|
||||
// Calculate the offset to draw the different layers and apply clipping
|
||||
// This offset is applied to place the top left corner of the bloom effect
|
||||
val layersOffset = if (offset.isSpecified) {
|
||||
// Offsets the layers so the center of the bloom effect is at the provided offset value
|
||||
IntOffset(
|
||||
centerOffset.x - dstSize.width / 2,
|
||||
centerOffset.y - dstSize.height / 2,
|
||||
)
|
||||
} else {
|
||||
// Places the layers at the center of the component
|
||||
IntOffset.Zero
|
||||
}
|
||||
val radius = max(dstSize.width, dstSize.height).toFloat() / 2
|
||||
val circularGradientShader = RadialGradientShader(
|
||||
centerOffset.toOffset(),
|
||||
radius,
|
||||
listOf(Color.Red, Color.Transparent),
|
||||
listOf(0f, 1f)
|
||||
)
|
||||
val circularGradientBrush = ShaderBrush(circularGradientShader)
|
||||
val bottomEdgeGradient = LinearGradientShader(
|
||||
from = IntOffset(0, clipToPixelSize.height - bottomSoftEdgeHeightPixels).toOffset(),
|
||||
to = IntOffset(0, clipToPixelSize.height).toOffset(),
|
||||
listOf(Color.Transparent, background),
|
||||
listOf(0f, 1f)
|
||||
)
|
||||
val bottomEdgeGradientBrush = ShaderBrush(bottomEdgeGradient)
|
||||
onDrawBehind {
|
||||
if (dstSize != IntSize.Zero) {
|
||||
val circleClipPath = Path().apply {
|
||||
addOval(Rect(centerOffset.toOffset(), radius - 1))
|
||||
}
|
||||
// Clip the external radius of bloom gradient too, otherwise we have a 1px border
|
||||
clipPath(circleClipPath, clipOp = ClipOp.Intersect) {
|
||||
// Draw the bloom layers
|
||||
drawWithLayer {
|
||||
// Clip rect to the provided size if needed
|
||||
if (clipToPixelSize != IntSize.Zero) {
|
||||
drawContext.canvas.clipRect(Rect(Offset.Zero, clipToPixelSize.toSize()), ClipOp.Intersect)
|
||||
}
|
||||
// Draw background color for blending
|
||||
drawRect(background, size = pixelSize.toSize())
|
||||
// Draw layers
|
||||
for (layer in layers) {
|
||||
drawImage(
|
||||
hashedBitmap,
|
||||
srcSize = IntSize(BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS),
|
||||
dstSize = dstSize,
|
||||
dstOffset = layersOffset,
|
||||
alpha = layer.alpha * alpha,
|
||||
blendMode = layer.blendMode,
|
||||
)
|
||||
}
|
||||
// Mask the layers erasing the outer radius using the gradient brush
|
||||
drawCircle(
|
||||
circularGradientBrush,
|
||||
radius,
|
||||
centerOffset.toOffset(),
|
||||
blendMode = BlendMode.DstIn
|
||||
)
|
||||
}
|
||||
}
|
||||
// Draw the bottom soft edge
|
||||
drawRect(
|
||||
bottomEdgeGradientBrush,
|
||||
topLeft = IntOffset(0, clipToPixelSize.height - bottomSoftEdgeHeight.roundToPx()).toOffset(),
|
||||
size = IntSize(pixelSize.width, bottomSoftEdgeHeight.roundToPx()).toSize(),
|
||||
alpha = bottomSoftEdgeAlpha
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bloom effect modifier for avatars. Applies a bloom effect to the component.
|
||||
* @param avatarData The avatar data to use as the bloom source.
|
||||
* If the avatar data has a URL it will be used as the bloom source, otherwise the initials will be used. If `null` is passed, no bloom effect will be applied.
|
||||
* @param background The background color to use for the bloom effect. Since we use blend modes it must be non-transparent.
|
||||
* @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component.
|
||||
* @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
|
||||
* @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
|
||||
* @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
|
||||
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
|
||||
* @param alpha The alpha value to apply to the bloom effect.
|
||||
*/
|
||||
fun Modifier.avatarBloom(
|
||||
avatarData: AvatarData?,
|
||||
background: Color,
|
||||
blurSize: DpSize = DpSize.Unspecified,
|
||||
offset: DpOffset = DpOffset.Unspecified,
|
||||
clipToSize: DpSize = DpSize.Unspecified,
|
||||
bottomSoftEdgeHeight: Dp = 40.dp,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
bottomSoftEdgeAlpha: Float = 1.0f,
|
||||
@FloatRange(from = 0.0, to = 1.0)
|
||||
alpha: Float = 1f,
|
||||
) = composed {
|
||||
// Bloom only works on API 29+
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this
|
||||
avatarData ?: return@composed this
|
||||
|
||||
if (avatarData.url != null) {
|
||||
// Request the avatar contents to use as the bloom source
|
||||
val painter = rememberAsyncImagePainter(
|
||||
ImageRequest.Builder(LocalContext.current)
|
||||
.data(avatarData)
|
||||
// Needed to be able to read pixels from the Bitmap for the hash
|
||||
.allowHardware(false)
|
||||
.build()
|
||||
)
|
||||
var blurHash by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(avatarData) {
|
||||
val drawable = painter.imageLoader.execute(painter.request).drawable ?: return@LaunchedEffect
|
||||
val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@LaunchedEffect
|
||||
blurHash = BlurHash.encode(bitmap, BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)
|
||||
}
|
||||
|
||||
bloom(
|
||||
hash = blurHash,
|
||||
background = background,
|
||||
blurSize = blurSize,
|
||||
offset = offset,
|
||||
clipToSize = clipToSize,
|
||||
bottomSoftEdgeHeight = bottomSoftEdgeHeight,
|
||||
bottomSoftEdgeAlpha = bottomSoftEdgeAlpha,
|
||||
alpha = alpha,
|
||||
)
|
||||
} else {
|
||||
// There is no URL so we'll generate an avatar with the initials and use that as the bloom source
|
||||
val avatarColors = AvatarColorsProvider.provide(avatarData.id, ElementTheme.isLightTheme)
|
||||
val initialsBitmap = initialsBitmap(
|
||||
width = avatarData.size.dp,
|
||||
height = avatarData.size.dp,
|
||||
text = avatarData.initial,
|
||||
textColor = avatarColors.foreground,
|
||||
backgroundColor = avatarColors.background,
|
||||
)
|
||||
val hash = remember(avatarData, avatarColors) {
|
||||
BlurHash.encode(initialsBitmap.asAndroidBitmap(), BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)
|
||||
}
|
||||
bloom(
|
||||
hash = hash,
|
||||
background = background,
|
||||
blurSize = blurSize,
|
||||
offset = offset,
|
||||
clipToSize = clipToSize,
|
||||
bottomSoftEdgeHeight = bottomSoftEdgeHeight,
|
||||
bottomSoftEdgeAlpha = bottomSoftEdgeAlpha,
|
||||
alpha = alpha,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Used to create a Bitmap version of the initials avatar
|
||||
@Composable
|
||||
private fun initialsBitmap(
|
||||
text: String,
|
||||
backgroundColor: Color,
|
||||
textColor: Color,
|
||||
width: Dp = 32.dp,
|
||||
height: Dp = 32.dp,
|
||||
): ImageBitmap = with(LocalDensity.current) {
|
||||
val backgroundPaint = remember(backgroundColor) {
|
||||
Paint().also { it.color = backgroundColor }
|
||||
}
|
||||
val resolver: FontFamily.Resolver = LocalFontFamilyResolver.current
|
||||
val fontSize = remember { height.toSp() / 2 }
|
||||
val typeface: Typeface = remember(resolver) {
|
||||
resolver.resolve(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontStyle = FontStyle.Normal,
|
||||
)
|
||||
}.value as Typeface
|
||||
val textPaint = remember(textColor, typeface) {
|
||||
TextPaint().apply {
|
||||
color = textColor.toArgb()
|
||||
textSize = fontSize.toPx()
|
||||
this.typeface = typeface
|
||||
}
|
||||
}
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
val result = remember(text) { textMeasurer.measure(text, TextStyle.Default.copy(fontSize = fontSize)) }
|
||||
val centerPx = remember(width, height) { IntOffset(width.roundToPx() / 2, height.roundToPx() / 2) }
|
||||
remember(text, width, height, backgroundColor, textColor) {
|
||||
val bitmap = Bitmap.createBitmap(width.roundToPx(), height.roundToPx(), Bitmap.Config.ARGB_8888).asImageBitmap()
|
||||
androidx.compose.ui.graphics.Canvas(bitmap).also { canvas ->
|
||||
canvas.drawCircle(centerPx.toOffset(), width.toPx() / 2, backgroundPaint)
|
||||
canvas.nativeCanvas.drawText(text, centerPx.x.toFloat() - result.size.width/2, centerPx.y * 2f - result.size.height/2 - 4, textPaint)
|
||||
}
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
|
||||
// Translates DP sizes into pixel sizes, taking into account unspecified values
|
||||
private fun DpSize.toIntSize(density: Density) = with(density) {
|
||||
if (isSpecified) {
|
||||
IntSize(width.roundToPx(), height.roundToPx())
|
||||
} else {
|
||||
IntSize.Zero
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to draw to a canvas using layers. This allows us to apply clipping to those layers only.
|
||||
*/
|
||||
fun DrawScope.drawWithLayer(block: DrawScope.() -> Unit) {
|
||||
with(drawContext.canvas.nativeCanvas) {
|
||||
val checkPoint = saveLayer(null, null)
|
||||
block()
|
||||
restoreToCount(checkPoint)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@DayNightPreviews
|
||||
@ShowkaseComposable(group = PreviewGroup.Bloom)
|
||||
@Composable
|
||||
internal fun BloomPreview() {
|
||||
val blurhash = "eePn{tI?xExEja}ooKWWodjtNJoKR,j@a|sBWpS3WDbGazoKWWWWj@"
|
||||
var topAppBarHeight by remember { mutableIntStateOf(-1) }
|
||||
val topAppBarState = rememberTopAppBarState()
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState)
|
||||
ElementPreview {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Box {
|
||||
MediumTopAppBar(
|
||||
modifier = Modifier
|
||||
.onSizeChanged { size ->
|
||||
topAppBarHeight = size.height
|
||||
}
|
||||
.bloom(
|
||||
hash = blurhash,
|
||||
background = ElementTheme.materialColors.background,
|
||||
blurSize = DpSize(430.dp, 430.dp),
|
||||
offset = DpOffset(24.dp, 24.dp),
|
||||
clipToSize = if (topAppBarHeight > 0) DpSize(430.dp, topAppBarHeight.toDp()) else DpSize.Zero,
|
||||
),
|
||||
colors = TopAppBarDefaults.largeTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Black.copy(alpha = 0.05f),
|
||||
),
|
||||
navigationIcon = {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp)
|
||||
.size(32.dp)
|
||||
.clip(CircleShape),
|
||||
painter = painterResource(id = R.drawable.sample_avatar),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {}) {
|
||||
Icon(imageVector = Icons.Default.Share, contentDescription = null)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text("Title")
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
}
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
repeat(20) {
|
||||
Text("Content", modifier = Modifier.padding(vertical = 20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InitialsColorStateProvider : PreviewParameterProvider<Int> {
|
||||
override val values: Sequence<Int>
|
||||
get() = sequenceOf(0, 1, 2, 3, 4, 5, 6, 7)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
@ShowkaseComposable(group = PreviewGroup.Bloom)
|
||||
internal fun BloomInitialsPreview(@PreviewParameter(InitialsColorStateProvider::class) color: Int) {
|
||||
ElementPreview {
|
||||
val avatarColors = AvatarColorsProvider.provide("$color", ElementTheme.isLightTheme)
|
||||
val bitmap = initialsBitmap(text = "F", backgroundColor = avatarColors.background, textColor = avatarColors.foreground)
|
||||
val hash = BlurHash.encode(bitmap.asAndroidBitmap(), BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)
|
||||
Box(
|
||||
modifier = Modifier.size(256.dp)
|
||||
.bloom(
|
||||
hash = hash,
|
||||
background = ElementTheme.materialColors.background,
|
||||
blurSize = DpSize(256.dp, 256.dp),
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(CircleShape),
|
||||
painter = BitmapPainter(bitmap),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
enum class AvatarSize(val dp: Dp) {
|
||||
CurrentUserTopBar(28.dp),
|
||||
CurrentUserTopBar(32.dp),
|
||||
|
||||
RoomHeader(96.dp),
|
||||
RoomListItem(52.dp),
|
||||
|
||||
@@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.preview
|
||||
object PreviewGroup {
|
||||
const val AppBars = "App Bars"
|
||||
const val Avatars = "Avatars"
|
||||
const val Bloom = "Bloom"
|
||||
const val BottomSheets = "Bottom Sheets"
|
||||
const val Buttons = "Buttons"
|
||||
const val DateTimePickers = "DateTime pickers"
|
||||
|
||||
25
libraries/designsystem/src/main/res/drawable/ic_search.xml
Normal file
25
libraries/designsystem/src/main/res/drawable/ic_search.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.05 16.46C13.79 17.43 12.2 18 10.5 18 6.36 18 3 14.64 3 10.5 3 6.36 6.36 3 10.5 3c4.14 0 7.5 3.36 7.5 7.5 0 1.71-0.57 3.29-1.54 4.55l3.25 3.24c0.39 0.4 0.39 1.03 0 1.42-0.4 0.39-1.03 0.39-1.42 0l-3.24-3.25ZM16 10.5C16 7.46 13.54 5 10.5 5S5 7.46 5 10.5 7.46 16 10.5 16s5.5-2.46 5.5-5.5Z"/>
|
||||
</vector>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user