How to render(or convert) a composable to image in compose-desktop - android-jetpack-compose

As the title says, hot to render a composable to image.
I want to render a composable to image on my server and send it to client
I have try for these code, bat it does not work.
SwingUtilities.invokeLater {
val composePanel = ComposePanel()
composePanel.setSize(2000,2000)
composePanel.setContent {
MaterialTheme {
Surface(
modifier = Modifier.size(300.dp)
) {
Text("asdasdsad")
}
}
}
val bounds=Rectangle()
composePanel.getBounds(bounds)
val img=BufferedImage(
(bounds.getX() + bounds.getWidth()).toInt(),
(bounds.getY() + bounds.getHeight()).toInt(),
BufferedImage.TYPE_INT_ARGB
)
composePanel.print(img.graphics)
val out=File("test.png")
out.delete()
ImageIO.write(img, "png", out)
}

Now I found a solution.The code below works fine.
But I didn't find a better way to direct convert Image to BufferedImage.
val scene=ImageComposeScene(2000,2000, density = Density(3f), coroutineContext = Dispatchers.Unconfined){
MaterialTheme {
Surface(
modifier = Modifier.size(300.dp)
) {
Text("asdasdsad")
}
}
}
val img=scene.render()
val bitmap=Bitmap.makeFromImage(img)
val bufferedImage=bitmap.toBufferedImage()
val out=File("test.png")
out.delete()
ImageIO.write(bufferedImage, "png", out)

Related

Manually move accessibility focus in Jetpack Compose

With Android View, I'm able to move the focus to a View like so:
fun View.requestAccessibilityFocus() {
requestFocus()
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}
How do I achieve this in Jetpack Compose?
I tried using FocusRequester but it doesn't seem to do anything:
val lifecycleOwner = LocalLifecycleOwner.current
val requester = FocusRequester()
Box {
...
Image(
...
contentDescription = "My heading",
modifier = Modifier
...
.focusRequester(requester)
)
}
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
requester.requestFocus()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
I had to add .focusable() and must do so after .focusRequester(focusRequester).
Box {
...
Image(
...
contentDescription = "My heading",
modifier = Modifier
...
.focusRequester(requester)
.focusable()
)
}
This happens because DiposableEffect might be calling during recomposition. You should not request focus during composition. The solution I have found is to request a focus right after the composition. Like this
LaunchedEffect(Unit) {
this.coroutineContext.job.invokeOnCompletion {
focusRequester.requestFocus()
}
}
This makes sure your code will run when the LauchedEffect leaves the composition because it(the coroutine) either got canceled or completed.

I have a composable not setting button text as expected; wondering why. Have a reproducible example

this started as a new compose project
with the following code the intent is to change the text to the picked time. The code is commented where the behavior occurs
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTestTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
TimeCardButton(id = 1, symbol ="In", enabled=true,modifier = Modifier) { entry ->
Log.d("click", "$entry result")
}
}
}
}
}
}
data class TimeCardEntry(val id: Int = -1, var entry: String = "")
#Composable
fun TimeCardButton(
id: Int,
symbol: String,
enabled: Boolean = false,
modifier: Modifier,
onValueChange: (TimeCardEntry) -> Unit = {},
) {
// Value for storing time as a string
val timeState = remember {
mutableStateOf(TimeCardEntry(id, symbol))
}
val validState = remember {
timeState.value.entry.trim().isNotEmpty()
}
val mTime = remember { mutableStateOf(symbol) }
if (enabled) {
// Fetching local context
val mContext = LocalContext.current
// Declaring and initializing a calendar
val mCalendar = Calendar.getInstance()
val mHour = mCalendar[Calendar.HOUR_OF_DAY]
val mMinute = mCalendar[Calendar.MINUTE]
// Creating a TimePicker dialog
val mTimePickerDialog = TimePickerDialog(
mContext,
{ _, mHour: Int, mMinute: Int ->
timeState.value.entry = "$mHour:$mMinute"
mTime.value = "$mHour:$mMinute"
onValueChange(timeState.value)
}, mHour, mMinute, false
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clip(CircleShape)
.then(modifier)
) {
TextButton(onClick = { mTimePickerDialog.show() }.also {
Log.d("click", "id $id clicked!") }) {
Column() {
// if I use just this it works [in changes to the time picked]
//Text(text = mTime.value)
// if i use both of these BOTH are set when the date picker is invoked
// if I just use the second one alone, the text never changes
Text(text = timeState.value.entry)
}
}
}
} else {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clip(CircleShape)
.then(modifier)
) {
Text(text = symbol, color =
MaterialTheme.colors.onBackground)
}
}
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MyApplicationTestTheme {
}
}
First of all how to fix it:
Your problem basically is this. The easiest way to fix it would be to reassign the whole value of TimeState, not just entry by calling
timeState.value = timeState.value.copy(entry = "$mHour:$mMinute")
The reason it doesn't work with only the second one is that the change of a property doesn't trigger recomposition, even if the variable containing it is a mutableState. To fix (as outlined in the answers to the question linked above) this you either have to reassign the whole variable or make the parameter you want to observe observable (for example changing the String to State<String>)
PS: if you use by with mutableStateOf (i.e. val timeState = remember { mutableStateOf(TimeCardEntry(id, symbol)) }) you don't have to use .value every time. I find that a lot cleaner and more readable

compose can not test Dialog

I have dialog in compose:
#Composable
fun testgtt() {
val saveDialogState = remember { mutableStateOf(false) }
Button(onClick = { saveDialogState.value = true }, modifier = Modifier.testTag(PLACE_TAG)) {
Text(text = "helllow")
}
Dialog(onDismissRequest = { saveDialogState.value = false }) {
Text(text = "helllow",modifier = Modifier.testTag(BUTTON_TAG))
}
}
and want to test it:
#Test
fun das(){
composeTestRule.setContent {
TerTheme {
testgtt()
}
}
composeTestRule.onRoot(useUnmergedTree = true).printToLog("currentLabelExists")
composeTestRule.onNodeWithTag(PLACE_TAG).performClick()
composeTestRule.onNodeWithTag(BUTTON_TAG).assertIsDisplayed()
}
but I get this error:
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but found '2' nodes that satisfy: (isRoot)
Nodes found:
1) Node #1 at (l=0.0, t=0.0, r=206.0, b=126.0)px
Has 1 child
2) Node #78 at (l=0.0, t=0.0, r=116.0, b=49.0)px
Has 1 child
Inspite of the fact that I see the Dialog itself.
The reason for this error is the line: composeTestRule.onRoot(useUnmergedTree = true).printToLog("currentLabelExists")
onRoot expects a single node, but i suspect both the containing view and the dialog each return their own root (Speculation)
A possible workaround is to instead print both root trees using something like
composeTestRule.onAllNodes(isRoot()).printToLog("currentLabelExists")
use navigation component:
#Composable
fun de(){
val navController = rememberNavController()
Scaffold { innerPadding ->
NavHost(navController, "home", Modifier.padding(innerPadding)) {
composable("home") {
// This content fills the area provided to the NavHost
val saveDialogState = remember { mutableStateOf(true) }
Button(onClick = {
navController.navigate("detail_dialog")
}, modifier = Modifier.testTag(PLACE_TAG)) {
Text(text = "helllow")
}
}
dialog("detail_dialog") {
// This content will be automatically added to a Dialog() composable
// and appear above the HomeScreen or other composable destinations
Dialog(onDismissRequest = {navController.navigate("home")}) {
Card(
shape = RoundedCornerShape(10.dp),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
// .padding(horizontal = 16.dp)
.padding(vertical = 8.dp),
elevation = 8.dp
){
Text(text = "helllow", modifier = Modifier.testTag(BUTTON_TAG))
}
}
}
}
}
}
As #DanielO said you can use the isRoot() selector, see below. That however prints out the same message as before.
A possible workaround is to instead print both root trees using something like
composeTestRule.onAllNodes(isRoot()).printToLog("currentLabelExists")
You have to distinctivly select which root you are looking for. By using the selectors:
.get( index )
.onFirst()
.onLast()
When added it should look like this:
composeTestRule.onAllNodes(isRoot()).get(1).printToLog("T:")
composeTestRule.onAllNodes(isRoot()).onFirst().printToLog("T:")
composeTestRule.onAllNodes(isRoot()).onLast().printToLog("T:")

LazyColumn list not showing in jetpack compose

I m new To Jetpack Compose , i'm trying to create a Proxy list app , used lazy column to list proxies but its now showing .. app runs successfully but result not getting , Give Me a Solution . Thx
Lib : https://github.com/gumify/hiper
#Preview
#Composable
fun App(){
val hiper = Hiper.getInstance().async() // for asynchronous requests
val Proxies = remember {mutableStateListOf<String>() }
Surface(modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()) {
Column(modifier = Modifier
.background(Color.Blue)
.height(20.dp),
horizontalAlignment = Alignment.CenterHorizontally) {
Button (
modifier = Modifier.padding(all = Dp(10F)),
onClick = {
hiper.get("http://spys.me/proxy.txt") { response ->
if (response.isSuccessful) {
val lines = response.text.toString().split("\n")
for (line in lines) {
Proxies.add(line)
}
}
}
}) { Text(text = "Get Proxies") }
LazyColumn( modifier = Modifier.background( color = Color.Gray)){
items(Proxies){item ->
Text(text = item)
}
}
}
}
}
}

Jetpack Compose - Scroll to focused composable in Column

I have UI like this:
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize(1F)
.padding(horizontal = 16.dp)
.verticalScroll(scrollState)
) {
TextField(...)
// multiple textfields
TextField(
//...
modifier = Modifier.focusOrder(countryFocus).onFocusChanged {
if(it == FocusState.Active) {
// scroll to this textfield
}
},
)
}
I have multiple TextFields in this column and when one of them is focused I want to scroll Column to it. There is a method in scrollState scrollState.smoothScrollTo(0f) but I have no idea how to get a focused TextField position.
Update:
It seems that I've found a working solution. I've used onGloballyPositioned and it works. But I'm not sure if it the best way of solving this.
var scrollToPosition = 0.0F
TextField(
modifier = Modifier
.focusOrder(countryFocus)
.onGloballyPositioned { coordinates ->
scrollToPosition = scrollState.value + coordinates.positionInRoot().y
}
.onFocusChanged {
if (it == FocusState.Active) {
scope.launch {
scrollState.smoothScrollTo(scrollToPosition)
}
}
}
)
There is a new thing in compose called RelocationRequester. That solved the problem for me. I have something like this inside of my custom TextField.
val focused = source.collectIsFocusedAsState()
val relocationRequester = remember { RelocationRequester() }
val ime = LocalWindowInsets.current.ime
if (ime.isVisible && focused.value) {
relocationRequester.bringIntoView()
}
Also you can use BringIntoViewRequester
//
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val coroutineScope = rememberCoroutineScope()
//--------
TextField( ..., modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester)
.onFocusEvent {
if (it.isFocused) {
coroutineScope.launch {
bringIntoViewRequester.bringIntoView()
}
}
}
It seems that using LazyColumn and LazyListState.animateScrollToItem() instead of Column could be a good option for your case.
Reference: https://developer.android.com/jetpack/compose/lists#control-scroll-position
By the way, thank you for the information about onGloballyPositioned() modifier. I was finding a solution for normal Column case. It saved me a lot of time!
Here's some code I used to make sure that the fields in my form were not cut off by the keyboard:
From: stack overflow - detect when keyboard is open
enum class Keyboard {
Opened, Closed
}
#Composable
fun keyboardAsState(): State<Keyboard> {
val keyboardState = remember { mutableStateOf(Keyboard.Closed) }
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
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
Keyboard.Opened
} else {
Keyboard.Closed
}
}
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
}
}
return keyboardState
}
and then in my composable:
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
val isKeyboardOpen by keyboardAsState()
if (isKeyboardOpen == Keyboard.Opened) {
val view = LocalView.current
val screenHeight = view.rootView.height
scope.launch { scrollState.scrollTo((screenHeight * 2)) }
}
Surface(modifier = Modifier
.fillMaxHeight()
.verticalScroll(scrollState),
) {
//Rest of your Composables, Columns, Rows, TextFields, Buttons
//add this so the screen can scroll up and keyboard cannot cover the form fields - Important!
/*************************************************/
if (isKeyboardOpen == Keyboard.Opened) {
Spacer(modifier = Modifier.height(140.dp))
}
}
Hope it helps someone. I was using:
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val scope = rememberCoroutineScope()
val view = LocalView.current
DisposableEffect(view) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
scope.launch { bringIntoViewRequester.bringIntoView() }
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
Surface(modifier.bringIntoViewRequester(bringIntoViewRequester)) {
///////////rest of my composables
}
But this did not work.

Resources