Jetpack compose - Wrong AnnotatedString for Arabic (RTL) punctuation - android-jetpack-compose

I'm looking for Arabic notation support in jetpack compose. I want to change the color of some Arabic punctuations and customize their look. Currently, I'm using this function to achieve this:
private fun colorPonctuation(input: String): AnnotatedString {
return buildAnnotatedString {
input.forEach {
if (it == '\u064B' || it == '\u064D'
|| it == '\u064D' || it == '\u064E' || it == '\u064F' ||
it == '\u0650' || it == '\u0651' || it == '\u0652' || it == '\u0653' ||
it == '\u0654' || it == '\u0655' || it == '\u0656' || it == '\u0657' ||
it == '\u0658' || it == '\u0659' || it == '\u065A' || it == '\u065B' ||
it == '\u065C' || it == '\u065D' || it == '\u065E' || it == '\u065F' || it == '\u0670'
) {
withStyle(style = SpanStyle(color = Color.Red)) {
append(it)
}
} else {
withStyle(style = SpanStyle(color = Color.Blue)) {
append(it)
}
}
}
}
}
Using this, the Arabic notations and punctuation change their color, but also change their position in the final string which is not what I want.
Here is the full code I'm using:
class Arabic : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
PreviewArabicText()
}
}
}
#Composable
fun ShowArabicText() {
val str by remember { mutableStateOf(AnnotatedString("بِسْمِ اللهِ الرّحْمنِ الرَّحِیم")) }
val text by remember { mutableStateOf("بِسْمِ اللهِ الرّحْمنِ الرَّحِیم") }
var isSelected by remember { mutableStateOf(false) }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = if (isSelected) colorPonctuation(text) else str,
color = Color.Blue,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Default,
fontStyle = FontStyle.Normal,
fontSize = 30.sp
)
Spacer(modifier = Modifier.width(10.dp))
Button(onClick = {
isSelected = !isSelected
}) {
Text(text = "change color")
}
}
}
Here you can see unwanted position changes for Arabic notations:

Related

How can I show a composable on top of the visible Keyboard?

Since we have differen screen sizes and resolutions, I would like to place a Composable on top of unfolded keybard:
The keyboard (see above picture) is visible and I want to show nother composable (red square) like a message for a few seconds..
Using Jetpack Compose what would be a easy way to position that red composable?
This question can be solved using WindowInsets.isImeVisible however for it to return correct values you should set
WindowCompat.setDecorFitsSystemWindows(window, false)
i set this in Activity before setContent{}
Using WindowInsets.isImeVisible to check whether keyboard is open or not.
And we need to show message while keyboard is opening up so
val offsetY = WindowInsets.ime.getBottom(density)
var previousOffset by remember { mutableStateOf(0) }
val isKeyboardGoingDown by remember(offsetY) {
derivedStateOf {
val isGoingDown = previousOffset - offsetY > 0
previousOffset = offsetY
isGoingDown
}
}
is used for tracking whether keyboard is going up or down with LaunchedEffect
LaunchedEffect(key1 = isImeVisible, key2 = isKeyboardGoingDown) {
if (isImeVisible && !isKeyboardGoingDown) {
showMessage = true
delay(1000)
showMessage = false
} else {
showMessage = false
}
}
Full implementation
#OptIn(ExperimentalLayoutApi::class)
#Composable
private fun TimedMessageLayout() {
val isImeVisible = WindowInsets.isImeVisible
var showMessage by remember { mutableStateOf(false) }
val density = LocalDensity.current
val offsetY = WindowInsets.ime.getBottom(density)
var previousOffset by remember { mutableStateOf(0) }
val isKeyboardGoingDown by remember(offsetY) {
derivedStateOf {
val isGoingDown = previousOffset - offsetY > 0
previousOffset = offsetY
isGoingDown
}
}
LaunchedEffect(key1 = isImeVisible, key2 = isKeyboardGoingDown) {
if (isImeVisible && !isKeyboardGoingDown) {
showMessage = true
delay(1000)
showMessage = false
} else {
showMessage = false
}
}
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomStart) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(WindowInsets.systemBars.asPaddingValues())
.border(2.dp, Color.Green)
) {
Image(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(4 / 3f),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = null
)
var text by remember { mutableStateOf("") }
Text(
"Ime visible: ${WindowInsets.isImeVisible}, isKeyboardGoingDown: $isKeyboardGoingDown\n" +
"ime bottom: ${WindowInsets.ime.getBottom(density)}\n"
)
Spacer(modifier = Modifier.weight(1f))
TextField(
value = text,
onValueChange = { text = it }
)
}
if (showMessage && !isKeyboardGoingDown && offsetY != 0) {
Box(modifier = Modifier
.offset {
IntOffset(0, -offsetY)
}
.fillMaxWidth()
.height(200.dp)
.border(3.dp, Color.Red))
}
}
}
Result
Try this solution from here, I slightly modified
#Composable
fun keyboardHeightAsState(): State<Int> {
val keyboardHeight = remember { mutableStateOf(0) }
val view = LocalView.current
DisposableEffect(view) {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
keyboardHeight.value = if (keypadHeight > screenHeight * 0.15) {
keypadHeight
} else {
0
}
}
}
return keyboardHeight
}
But I still have a small difference. I guess its the top bar, which I have to subtract

Observing position of item in Lazy Column in Jetpack Compose

Is there a way where I can observer the Cyan color field position progress in percentage/fraction, to get 100% when it is fully visible and 0% when it is closed(list elements are scrolled), the idea is that I want to remove "First Last" text on left with animation.
Here is the code:
val scaffoldState = rememberScaffoldState()
val lazyListState = rememberLazyListState()
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = {
Text(fontSize = 30.sp, text = "Account", color = Color.White)
},
backgroundColor = Color.Black
)
},
content = {
LazyColumn(state = lazyListState) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.background(color = Color.Cyan)
) {
Text(
text = "First Last",
fontSize = 20.sp,
color = Color.White,
modifier = Modifier
.padding(20.dp)
)
}
}
items(list) {
Row(
Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 20.dp, top = 10.dp, bottom = 10.dp)
) {
Text(it)
}
}
}
}
)
You can achieve this by getting height of your RowItem and using a derivedStateOf that calculates using this height or getting it using lazyListState.layoutInfo.visibleItemsInfo
val percent = remember {
derivedStateOf {
val isFirstItemVisible = lazyListState.firstVisibleItemIndex == 0
// If your first item is not visible then our progress is 0%
if (isFirstItemVisible) {
// ALTERNATIVE 1
// // This is for not dividing when it's 0 initially
// if (rowHeight == 0f) {
// 100f
// } else {
// (100f * (1 - lazyListState.firstVisibleItemScrollOffset / rowHeight)).roundToInt()
// }
// ALTERNATIVE 2
val items = lazyListState.layoutInfo.visibleItemsInfo
if (items.isNotEmpty()) {
val firstItem = lazyListState.layoutInfo.visibleItemsInfo.first()
(100f * (1f + firstItem.offset / firstItem.size.toFloat())).roundToInt()
} else {
100f
}
} else {
0f
}
}
}
Full example
You don't have to set Modifier.onSizeChanged{} with alternative 2
#Composable
private fun MyList() {
val lazyListState = rememberLazyListState()
var rowHeight by remember { mutableStateOf(0f) }
val list = mutableListOf<String>()
val percent = remember {
derivedStateOf {
val isFirstItemVisible = lazyListState.firstVisibleItemIndex == 0
// If your first item is not visible then our progress is 0%
if (isFirstItemVisible) {
// ALTERNATIVE 1
// // This is for not dividing when it's 0 initially
// if (rowHeight == 0f) {
// 100f
// } else {
// (100f * (1 - lazyListState.firstVisibleItemScrollOffset / rowHeight)).roundToInt()
// }
// ALTERNATIVE 2
val items = lazyListState.layoutInfo.visibleItemsInfo
if (items.isNotEmpty()) {
val firstItem = lazyListState.layoutInfo.visibleItemsInfo.first()
(100f * (1f + firstItem.offset / firstItem.size.toFloat())).roundToInt()
} else {
100f
}
} else {
0f
}
}
}
repeat(20) {
list.add("Row $it")
}
Box(contentAlignment = Alignment.TopEnd) {
LazyColumn(state = lazyListState) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.onSizeChanged {
if (rowHeight == 0f) {
rowHeight = it.height.toFloat()
}
}
.background(color = Color.Cyan)
) {
Text(
text = "First Last",
fontSize = 20.sp,
color = Color.White,
modifier = Modifier
.padding(20.dp)
)
}
}
items(items = list) {
Row(
Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(start = 20.dp, top = 10.dp, bottom = 10.dp)
) {
Text(it)
}
}
}
Text(
"Percent: ${percent.value}"
)
}
}
Result

Navigating non-visible FocusRequesters in LazyColumn

I have a scrollable container (LazyColumn) and I want to navigate to the next FocusRequester in the list. But if the FocusRequester exists outside the rendered elements the FocusRequester can not be reached by FocusManger.
Code to reproduce problem:
#ExperimentalComposeUiApi
#Composable
fun exampleDesktop() { // Also works on phone but you need an external keyboard
val focusManager = LocalFocusManager.current
fun handleKeyPreview(evt: KeyEvent) : Boolean {
return if (evt.type == KeyEventType.KeyDown && evt.key == Key.Tab) {
if (evt.isShiftPressed) {
focusManager.moveFocus(FocusDirection.Up)
} else {
focusManager.moveFocus(FocusDirection.Down)
}
true
} else false
}
LazyColumn {
repeat(100) {
item {
FocusableItem(it, ::handleKeyPreview)
}
}
}
}
#ExperimentalComposeUiApi
#Composable
fun examplePhone() {
val focusManager = LocalFocusManager.current
Column {
Row {
Button(onClick = { focusManager.moveFocus(FocusDirection.Up)}) { Text("Up")}
Button(onClick = { focusManager.moveFocus(FocusDirection.Down)}) { Text("Down")}
}
LazyColumn {
repeat(100) {
item {
FocusableItem(it, handler = {false})
}
}
}
}
}
#Composable
fun FocusableItem(i: Int, handler: (KeyEvent) -> Boolean) {
val requester = remember { FocusRequester() }
TextField(
modifier = Modifier
.focusRequester(requester)
.onPreviewKeyEvent(onPreviewKeyEvent = handler),
value = "$i", onValueChange = {})
}
Is there a way to know of the existence of other focus requesters and put them into the view?

Cannot Type into OutlinedTextField when dropdownmenu opens below it

I am trying to make autocomplete feature by using OutlinedTextField, DropdownMenu.
When I programmatically open the DropdownMenu, I can then no longer type into the OutlinedTextField.
var expanded by remember { mutableStateOf(false) }
val suggestions = remember { mutableStateListOf ("karan", "karanx", "karany") }
var selectedText by remember { mutableStateOf("") }
var dropDownWidth by remember { mutableStateOf(0) }
val focusManager = LocalFocusManager.current
Box() {
OutlinedTextField(
value = selectedText,
onValueChange = {
selectedText = it
expanded = true
},
modifier = Modifier.fillMaxWidth()
.onSizeChanged {
dropDownWidth = it.width
} .onFocusChanged {
expanded = it.isFocused
},
//why cant i do expanded = true on cliackable modifier of outlinetextfield
label = { Text("Label") }
)
DropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
focusManager.clearFocus()
},
modifier = Modifier
.width(with(LocalDensity.current) { dropDownWidth.toDp() }).border(width = 2.dp,color = Color.Blue)
) {
suggestions.forEach { label ->
DropdownMenuItem(onClick = {
selectedText = label
expanded = false
focusManager.clearFocus()
}) {
Text(text = label)
}
}
}
}
This got solved by adding focusable = false to PopupProperties for the DropdownMenu like shown below:
DropdownMenu(
expanded = suggestions.isNotEmpty(),
onDismissRequest = { suggestions.removeAll{true} },
properties = PopupProperties(focusable = false),
modifier = Modifier.fillMaxWidth()
)
Full working code for autocomplete with Jetpack Compose Below :
var expanded by remember { mutableStateOf(false) }
var masterSuggestions = listOf ("karan", "priya", "vihaan")
var suggestions = remember { mutableStateListOf <String>() }
var selectedText by remember { mutableStateOf("") }
var dropDownWidth by remember { mutableStateOf(0) }
Column() {
OutlinedTextField(
value = selectedText,
onValueChange = {
println ("inside value change")
suggestions.removeAll{true}
selectedText = it
for (name in masterSuggestions)
{
if (name.startsWith(selectedText)) {
suggestions.add(name)
}
}
},
modifier = Modifier.fillMaxWidth(),
label = {Text ("label")}
)
DropdownMenu(
expanded = suggestions.isNotEmpty(),
onDismissRequest = { suggestions.removeAll{true} },
properties = PopupProperties(focusable = false),
modifier = Modifier.fillMaxWidth()
) {
suggestions.forEach { label ->
DropdownMenuItem(onClick = {
selectedText = label
}) {
Text(text = label)
}
}
}
}

Nested scrolling Column and LazyList

I need the whole column as one scroll component. I can't use item directly since I need to use aclassen/ComposeReorderable library for drag and drop scrolling. Any ideas?
val scrollState = rememberScrollState()
Column(
modifier = Modifier.verticalScroll(scrollState)
) {
LazyColumn(){
repeat(20) {
item {
Text(text = "Item: " + it)
}
}
}
Text(text = "Header")
LazyColumn(){
repeat(20) {
item {
Text(text = "Item: " + it)
}
}
}
}
I tried this but the scrolling feels unnatural and I don't know how to add the nestedScroll on non list Composables like the header
val offset = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
offset.value = (offset.value + available.y).coerceIn(-200f, 0f)
return Offset.Zero
}
}
}
Column(
modifier = Modifier.offset(y = offset.value.dp)
) {
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollConnection)
){
repeat(20) {
item { Text(text = "Item: $it") }
}
}
Text(text = "Header")
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollConnection)
){
repeat(20) {
item { Text(text = "Item: $it") }
}
}
}

Resources