jetpack compose declare variable in compose fun body - android-jetpack-compose

#Composable
private fun PostListPopularSection(
posts: List<Post>,
navigateToArticle: (String) -> Unit
) {
var postName = post.first.name
var postNumberOfChars = post.first.name.length / 2
if (postNumberOfChars >=30)
Text(postNumberOfChars.toString())
Column {
Text(
modifier = Modifier.padding(16.dp),
text = stringResource(id = R.string.home_popular_section_title),
style = MaterialTheme.typography.subtitle1
)
LazyRow(modifier = Modifier.padding(end = 16.dp)) {
items(posts) { post ->
PostCardPopular(
post,
navigateToArticle,
Modifier.padding(start = 16.dp, bottom = 16.dp)
)
}
}
PostListDivider()
}
}
my question is declares variables like these, and make some calculations,
is it make bad for performance, does it worth moving these lines to the view?
var postName = post.first.name
var postNumberOfChars = post.first.name.length /2
if (postNumberOfChars >=30)
Text(postNumberOfChars.toString())

Related

Text hyperlink hashtags(#) and mentions(#) in Jetpack Compose?

Text hyperlink hashtags(#) and mentions(#) in Jetpack Compose?
#Composable
fun HashtagsAndMentions() {
val colorScheme = MaterialTheme.colorScheme
val primaryStyle = SpanStyle(color = colorScheme.primary)
val textStyle = SpanStyle(color = colorScheme.onBackground)
val annotatedString = buildAnnotatedString {
withStyle(style = textStyle) {
append("I am ")
}
pushStringAnnotation(tag = "hashtags", annotation = "hashtags")
withStyle(style = primaryStyle) {
append(text = "#hashtags")
}
pop()
withStyle(style = textStyle) {
append(" and ")
}
pushStringAnnotation(tag = "mentions", annotation = "mentions")
withStyle(style = primaryStyle) {
append(text = "#mentions")
}
pop()
withStyle(style = textStyle) {
append(" in Jetpack Compose.")
}
}
ClickableText(
onClick = {
annotatedString.getStringAnnotations("hashtags", it, it).firstOrNull()?.let {
}
annotatedString.getStringAnnotations("mentions", it, it).firstOrNull()?.let {
}
},
text = annotatedString,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(16.dp))
}
#Preview
#Composable
fun PreviewTest() {
HashtagsAndMentions()
}
The above are fixed tags, how to dynamically identify and link?
ideas String to array
val string = "I am #hashtags and #mentions in Jetpack Compose."
val array = arrayOf("I am ", "#hashtags", " and ", "#mentions", "in Jetpack Compose.")
Here is an available hashtag and mention link.
Many thanks to #cyberpunk_unicorn for the idea, unfortunately his answer is not available, but I marked it anyway.
I refactored his code to ensure that it is runnable, concise enough, and very helpful to those who need it later.
This is a very flexible link scheme, you can add support for mobile phone numbers, emails, links, etc., as long as your regular expressions are correct, these links can be correctly identified.
#Composable
fun HashtagsMentionsTextView(text: String, modifier: Modifier = Modifier, onClick: (String) -> Unit) {
val colorScheme = MaterialTheme.colorScheme
val textStyle = SpanStyle(color = colorScheme.onBackground)
val primaryStyle = SpanStyle(color = colorScheme.blue)
val hashtags = Regex("((?=[^\\w!])[##][\\u4e00-\\u9fa5\\w]+)")
val annotatedStringList = remember {
var lastIndex = 0
val annotatedStringList = mutableStateListOf<AnnotatedString.Range<String>>()
// Add a text range for hashtags
for (match in hashtags.findAll(text)) {
val start = match.range.first
val end = match.range.last + 1
val string = text.substring(start, end)
if (start > lastIndex) {
annotatedStringList.add(
AnnotatedString.Range(
text.substring(lastIndex, start),
lastIndex,
start,
"text"
)
)
}
annotatedStringList.add(
AnnotatedString.Range(string, start, end, "link")
)
lastIndex = end
}
// Add remaining text
if (lastIndex < text.length) {
annotatedStringList.add(
AnnotatedString.Range(
text.substring(lastIndex, text.length),
lastIndex,
text.length,
"text"
)
)
}
annotatedStringList
}
// Build an annotated string
val annotatedString = buildAnnotatedString {
annotatedStringList.forEach {
if (it.tag == "link") {
pushStringAnnotation(tag = it.tag, annotation = it.item)
withStyle(style = primaryStyle) { append(it.item) }
pop()
} else {
withStyle(style = textStyle) { append(it.item) }
}
}
}
ClickableText(
text = annotatedString,
style = MaterialTheme.typography.bodyLarge,
modifier = modifier,
onClick = { position ->
val annotatedStringRange =
annotatedStringList.first { it.start < position && position < it.end }
if (annotatedStringRange.tag == "link") onClick(annotatedStringRange.item)
}
)
}
#Preview
#Composable
fun PreviewTest() {
val string = "I am #hashtags or #hashtags# and #mentions in Jetpack Compose. I am #hashtags or #hashtags# and #mentions in Jetpack Compose. 这是在 Jetpack Compose 中的一个 #标签 和 #提及 的超链接。 这是在 Jetpack Compose 中的一个 #标签 和 #提及 的超链接。"
HashtagsMentionsTextView(string, Modifier.padding(16.dp)) {
println(it)
}
}
preview effect

Why is MediumTopAppBar (and Large) showing two TextField in compose?

I am trying to make the title of a screen editable.
MediumTopAppBar(
title = {
val name: String? = "Some Title"
var input by remember { mutableStateOf(name ?: "") }
when (state.isEditingTitle) {
true ->
TextField(
value = input,
onValueChange = { input = it },
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = {
callbacks.onEditTitleChange(editTitle = false, updatedTitle = input)
})
)
false -> {
Text(
modifier = Modifier.clickable { callbacks.onEditTitleChange(true, null) },
text = name ?: "(No Title)"
)
}
}
},
... more app bar parameters
}
When I click on the title Text(...) and the view gets recomposed the AppBar shows two TextFields
How do I ignore the top one and only show the one in the bottom, like the Text() is only shown in the bottom?
(Fyi: the two TextInputs have their own remembered state and calls the callback with their own respective value)
Bonus question: How do I handle the remembered state "input" so that it resets every time the onDone keyboard action is triggered? Instead of val name: String? = "Some Title" it would of course be something in the line of val name: String? = state.stateModel.title
I found out why it does this, but I have no idea how to solve it (except for just making my own views and placing it close by)
It's easy to see when looking at the function for the MediumTopBar
// androidx.compose.material3.AppBar.kt
#ExperimentalMaterial3Api
#Composable
fun MediumTopAppBar(
title: #Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: #Composable () -> Unit = {},
actions: #Composable RowScope.() -> Unit = {},
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
colors: TopAppBarColors = TopAppBarDefaults.mediumTopAppBarColors(),
scrollBehavior: TopAppBarScrollBehavior? = null
) {
TwoRowsTopAppBar(
modifier = modifier,
title = title,
titleTextStyle = MaterialTheme.typography.fromToken(TopAppBarMediumTokens.HeadlineFont),
smallTitleTextStyle = MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont),
titleBottomPadding = MediumTitleBottomPadding,
smallTitle = title, // <- this thing, right here
navigationIcon = navigationIcon,
actions = actions,
colors = colors,
windowInsets = windowInsets,
maxHeight = TopAppBarMediumTokens.ContainerHeight,
pinnedHeight = TopAppBarSmallTokens.ContainerHeight,
scrollBehavior = scrollBehavior
)
}
There's some internal state shenanigans going on, probably checking for a Text being shown in the 2nd TopAppBarLayout (more digging required to find that), but not for any other view.
TwoRowsTopAppBar and TopAppBarLayout are not public, and can't be used directly.
This is explains why, but it would be interesting to see how to solve it (still using Medium or Large -TopAppBar)
it is stupid thing devs overlooked and should be warned against, at least. The answer is do not give default colors to your Typography TextStyles.
private val BodySmall = TextStyle(
fontSize = 10.sp,
lineHeight = 12.sp,
fontWeight = FontWeight.SemiBold,
fontFamily = Manrope,
color = Color.Black // REMOVE THIS
)
val OurTypography = Typography(
...
bodySmall = BodySmall
)

BottomNavigationItems padding

Is there any way to remove this padding from the BottomNavigationItem?
Image
If I have very large text, I have to use ResponsiveText to manage this, but that's not what I intended. What I need is that it doesn't have that side padding/margin, both on the left and on the right, in order to occupy as much space as possible.
My code:
#Composable
fun BottomNavBar(
backStackEntryState: State<NavBackStackEntry?>,
navController: NavController,
bottomNavItems: List<NavigationItem>
) {
BottomNavigation(
backgroundColor = DarkGray.copy(alpha = 0.6f),
elevation = Dimen0,
modifier = Modifier
.padding(Dimen10, Dimen20, Dimen10, Dimen20)
.clip(RoundedCornerShape(Dimen13, Dimen13, Dimen13, Dimen13))
) {
bottomNavItems.forEach { item ->
val isSelected = item.route == backStackEntryState.value?.destination?.route
BottomNavigationItem(
icon = {
Icon(
painter = painterResource(id = item.icon.orZero()),
contentDescription = stringResource(id = item.title)
)
},
label = {
ResponsiveText(
text = stringResource(id = item.title),
textStyle = TextStyle14,
maxLines = 1
)
},
selectedContentColor = Color.White,
unselectedContentColor = Color.White,
alwaysShowLabel = true,
selected = isSelected,
onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route = route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
},
modifier = if (isSelected) {
Modifier
.clip(RoundedCornerShape(Dimen13, Dimen13, Dimen13, Dimen13))
.background(color = DarkGray)
} else {
Modifier.background(color = Color.Unspecified)
}
)
}
}
}
Apparently this is currently (I am using compose version '1.2.0-rc03') not possible when using BottomNavigation, as there is padding set for each element in these lines:
.padding(horizontal = BottomNavigationItemHorizontalPadding)
Here is what is said about this value:
/**
* Padding at the start and end of a [BottomNavigationItem]
*/
private val BottomNavigationItemHorizontalPadding = 12.dp
[Solution]
Just copy BottomNavigation from androidx and remov this line:
.padding(horizontal = BottomNavigationItemHorizontalPadding)
However, it is necessary that the first and last elements still have padding, so add the innerHorizontalPaddings parameter to the your CustomBottomNavigation constructor
There are a few more changes, you can see the full code of my CustomBottomNavigation here
Example of usage:
CustomBottomNavigation(
...,
innerHorizontalPaddings = 12.dp
) {
items.forEach { item ->
BottomNavigationItem(
icon = {
Icon(...)
},
label = {
Text(
...
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 2.dp)
)
},
...
)
}
}
Another solution is to wrap the label in a BoxWithConstraints and draw outside of it:
BottomNavigationItem(
label = {
/**
* Because of [BottomNavigationItemHorizontalPadding] (12.dp), we need to
* think (and draw) outside the box.
*/
BoxWithConstraints {
Text(
modifier = Modifier
.wrapContentWidth(unbounded = true)
.requiredWidth(maxWidth + 24.dp), // 24.dp = the padding * 2
text = "Centered text and clipped at the end if too long",
softWrap = false,
textAlign = TextAlign.Center
)
}
},
...
)
To get a little bit of padding, you can set requiredWidth(maxWidth + 18.dp).
With this solution, you don't need to copy the enire BottomNavigation :)

goneMargin in ConstraintLayout with Compose

I'm trying to use ConstraintLayout in Compose. I want the second Composable to be constrained at the end of the first Composable. The first composable can exist or not. In each case, I would like to have different margins. Thus, I use goneMargin but this seems to not be respected. Do I need to do something else?
ConstraintLayout {
val (firstItemRef, secondItemRef) = createRefs()
if (isFirstItemVisible) {
Text(
text = "First",
modifier = Modifier.constrainAs(firstItemRef) {
top.linkTo(anchor = parent.top)
start.linkTo(anchor = parent.start)
}
)
}
Text(
text = "Second",
modifier = Modifier.constrainAs(secondItemRef) {
top.linkTo(anchor = parent.top)
start.linkTo(anchor = firstItemRef.end, margin = 8.dp, goneMargin = 16.dp)
}
)
}
As a workaround, we could do something like that, but this seems to counterfeit the purpose of goneMargin
Text(
text = "Second",
modifier = Modifier.constrainAs(secondItemRef) {
val margin = if (isFirstItemVisible) 8.dp else 16.dp
val end = if (isFirstItemVisible) firstItemRef.end else parent.end
top.linkTo(anchor = parent.top)
start.linkTo(anchor = end, margin = margin)
}
)
You have to use the visibility property in ConstrainScope like this:
ConstraintLayout(Modifier.fillMaxSize()) {
val (firstItemRef, secondItemRef) = createRefs()
Text(
text = "First",
modifier = Modifier.constrainAs(firstItemRef) {
top.linkTo(anchor = parent.top)
start.linkTo(anchor = parent.start)
// >> This is what you want <<<
visibility = if (isFirstItemVisible) Visibility.Visible else Visibility.Gone
}
)
Text(
text = "Second",
modifier = Modifier.constrainAs(secondItemRef) {
top.linkTo(anchor = parent.top)
start.linkTo(anchor = firstItemRef.end, margin = 8.dp, goneMargin = 16.dp)
}
)
}

How to make a lazycolumn scroll to the end when using bottomsheetscaffold?

I am going to design a layout with a bottomsheetscaffold with sheetPeekHeight to be 100 dp in order to show the sheet content. I also need to put a lazyColumn for the main content of the bottomsheetscaffold. But when the lazy column scrolls to the end, the final item will be behind the bottom sheet. How can I make the final item of the column be above the sheet?
Here is the code for the bottom sheet:
#ExperimentalMaterialApi
#Composable
fun HomeScreen() {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Text("this is test", modifier = Modifier.fillMaxWidth().height(60.dp))
},
sheetPeekHeight = 100.dp,
sheetShape = RoundedCornerShape(topEnd = 52.dp, topStart = 52.dp),
backgroundColor = Color.White
) {
MainContent()
}
}
#Composable
fun MainContent() {
LazyColumn {
items(count = 5) { itemIndex ->
when (itemIndex) {
0 -> {
Image(modifier = Modifier
.fillMaxWidth()
.height(100.dp), contentDescription = "test",
painter = painterResource(id = R.drawable.image))
}
}
}
}
}
Spacer(modifier=Modifier.height(100.dp)) I think fits better than a box here.
In your case it is easier to use fixed height, but if your content is dynamic you can also calculate bottomSheet height based on screenheight - bottomSheetOffset
fun YourComposable{
...
val bottomSheetHeight =
configuration.screenHeightDp.dp - bottomSheetScaffoldState.bottomSheetState.offset.value.pxToDp
...
}
private val Float.pxToDp: Dp
get() = (this / Resources.getSystem().displayMetrics.density).dp
As a workaround, I added an empty box with the same height as peekheight to the end of the lazycloumn. But I'm still interested in a better solution.

Resources