Jetpack Compose Canvas drawPath doesn't work - kotlin

I have this composable that is supposed to allow drawing with the pointer to a canvas but when I move the pointer nothing happens
#ExperimentalComposeUiApi
#Composable
fun CanvasDraw() {
val path = remember { Path() }
Canvas(modifier = Modifier
.fillMaxSize()
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
path.moveTo(it.x, it.y)
}
MotionEvent.ACTION_MOVE -> {
path.lineTo(it.x, it.y)
}
else -> false
}
true
}) {
drawPath(
path = path,
color = Color.Blue,
style = Stroke(10f)
)
}
}

remember { Path() } is caching lambda content value for next recompositions, but it cannot trigger recomposition when content on this object is changed. To make some state, which will trigger recomposition on changes in Compose, you need to use a mutable state of some kind - it's a new thing made for Compose.
You can find more info about state in Compose in documentation, including this youtube video which explains the basic principles.
Storing points inside Path will not be clean, as it's not a basic type. Instead I'm using a mutable state list of points, like this:
val points = remember { mutableStateListOf<Offset>() }
Canvas(modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures { change, _ ->
points.add(change.position)
}
}
) {
drawPath(
path = Path().apply {
points.forEachIndexed { i, point ->
if (i == 0) {
moveTo(point.x, point.y)
} else {
lineTo(point.x, point.y)
}
}
},
color = Color.Blue,
style = Stroke(10f)
)
}

Related

how create custom shape in android jetpack compose?

How can I make a shape like this?
I see there two ways how I would've done this:
You can create #Composable function with Canvases (official guideline, article on Medium), if you need to use tab of this folder-like shape
// Usage:
#Composable
fun Somewhere() {
FolderLikeCard(
topTailContent = {
// for example, your tab will just be, without content, only for shape
Box(modifier = Modifier.size(64.dp, 12.dp))
},
mainContent = {
// main content
}
)
}
// Implementation:
#Composable
fun FolderLikeCard(
topTailContent: #Composable () -> Unit,
mainContent: #Composable () -> Unit
) {
val cornerSize = 4.dp // for example
Column {
Row {
Spacer(modifier = Modifier.weight(1f))
Box {
Canvas {
TODO("draw with help of links above tab's outline (using drawArc maybe)")
}
Box(
modifier = Modifier.padding(left = cornerSize, top = cornerSize, right = cornerSize),
content = topTailContent
)
}
}
Box {
Canvas {
TODO("draw main part outline")
}
Box(
modifier = Modifier.padding(left = cornerSize, bottom = cornerSize, right = cornerSize),
content = mainContent
)
}
}
}
+ I feel it can be refactored with help of Modifier.drawBehind method
Create an actual Shape for using it in Modifier.background(color = yourColor, shape = FolderLikeShape(tabHeight, tabWidth, ...)) method, link to the article that I read some time ago, + linked question helps, but I don’t see how you can then put the content there so far, if you need it, then I hope the following commentators will help with this.

Can I change the value of a component from a separate button in Compose Multiplatform?

I am trying to make a desktop application that allows you to search through a number of predefined locations stored in Kotlin classes in a separate directory. To accomplish this, I've used the reflections and compose-jb libraries.
The problem I've run into is that I can't figure out how to update a Column of Boxes (located in another Box component) to change when I press the search button after entering tags that I want to search by.
My code is below (for the Main.kt file) that describes the entire desktop application.
val reflections = Reflections("io.github.mobomega.project.attractions")
var display = mutableSetOf<Attraction>()
fun main() = application {
val stateVertical = rememberScrollState(0)
val stateHorizontal = rememberScrollState(0)
var state = Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(stateVertical)
.padding(end = 12.dp, bottom = 12.dp)
.horizontalScroll(stateHorizontal)
)
Window(
onCloseRequest = ::exitApplication,
title = "Search",
state = rememberWindowState(width = 2256.dp, height = 1504.dp)
) {
val count = remember { mutableStateOf(1) }
MaterialTheme {
Column {
val text = remember { mutableStateOf("") }
OutlinedTextField(
value = text.value,
singleLine = true,
onValueChange = { text.value = it },
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Row (modifier = Modifier.size(2256.dp, 50.dp), horizontalArrangement = Arrangement.Center) {
Button(modifier = Modifier.align(Alignment.Top),
onClick = {
val tags = text.value.split(", ", ",")
for (tag in tags) {
search(tag.lowercase())
println("$display have tag $tag")
}
// Setting the new value of the Box
state = create(stateVertical, stateHorizontal)
// Creates error:
// "#Composable invocations can only happen from the context of a #Composable function"
}) {
Text("Search")
}
Button (modifier = Modifier.align(Alignment.Top),
onClick = {
display.clear()
}) {
Text("Reset")
}
}
Row (horizontalArrangement = Arrangement.Center) {
Box(
modifier = Modifier.fillMaxSize()
.background(color = Color(red = 0xFF, green = 0xFF, blue = 0xFF))
.padding(10.dp)
) {
state // Creating the state Box component in the Row
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight(),
adapter = rememberScrollbarAdapter(stateVertical)
)
HorizontalScrollbar(
modifier = Modifier.align(Alignment.BottomStart)
.fillMaxWidth()
.padding(end = 12.dp),
adapter = rememberScrollbarAdapter(stateHorizontal)
)
}
}
}
}
}
}
#Composable
fun textBox(text: String = "Item") {
Box(
modifier = Modifier.height(32.dp)
.width(400.dp)
.background(color = Color(200, 0, 0, 20))
.padding(start = 10.dp),
contentAlignment = Alignment.CenterStart
) {
Text(text = text)
}
}
#Composable
fun create(stateVertical: ScrollState, stateHorizontal: ScrollState) = Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(stateVertical)
.padding(end = 12.dp, bottom = 12.dp)
.horizontalScroll(stateHorizontal)
) {
Column {
var x = 0
for (attr in display) {
x++
textBox(attr.name)
if (x < display.size) {
Spacer(modifier = Modifier.height(5.dp).align(Alignment.CenterHorizontally))
}
}
}
}
fun search(text: String) {
for (attr in reflections.getSubTypesOf(Attraction::class.java)) {
val temp = attr.getConstructor().newInstance()
println("${temp.name} has tags ${temp.tags}")
if (temp.matches(text) && (temp !in display)) {
display += temp
}
}
}
I have tried to update the value of the Box that contains all of the items that match any of the search criteria, but I have run into a number of issues, such as the "onClick" function in which I set the new value of the "state" variable (storing all of the matching items) not being a Composable function, and therefore I can't change the value.
How would I accomplish changing the value of a Component such as a Box from another Component, such as a Button?
In Compose you can't create a view like you're doing with state variable. Result of your call is just Unit, and when you later call it you should see a warning "The expression is unused". The view is added at the tree hierarchy at the moment your variable is created.
To solve your problem you need to declare display as a mutable state - it's a new thing made especially for Compose, which allows triggering recomposition when this state changes:
val display by mutableStateOf<Attraction>(setOf())
And then update like this in your search:
val mutableDisplay = mutableSetOf<Attraction>()
// for
// ...
mutableDisplay += temp
// ...
display = mutableDisplay
Note that you can't use mutable set inside your mutable state, as mutable state won't be able to track changes of this set.
To learn more about state in Compose I suggest you checking this youtube video which explains the basic principles, and Compose mental model for better understanding of how to work with it.

Should I use remember with animateDpAsState?

The Code A is from the official sample code here.
I know that in order to preserve state across recompositions, remember the mutable state using remember.
I think that the code val extraPadding by animateDpAsState(...) should be val extraPadding by remember { animateDpAsState(...) }, is it right?
BTW, val extraPadding by remember { animateDpAsState(...) } will cause the error
Composable calls are not allowed inside the calculation parameter of inline fun remember(calculation: () -> TypeVariable(T)): TypeVariable(T)
Code A
#Composable
private fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState( //Should I add remember
if (expanded) 48.dp else 0.dp
)
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
No, you shouldn't. This function is marked with #Composable so it should be used directly in the view builder.
animateDpAsState will calculate its value depending on targetValue on each recomposition.
If you check it source code you'll see, that it uses remember inside, that's why it's marked with #Composable, and that's why you shouldn't bother about remembering some values manually.
For anyone that comes across this in the future, instead of attempting to remember the animateDpAsState it would be better to remember the expansion state. Because in this case we're trying to remember the state in an item of a list we use rememberSaveable:
var expanded by rememberSaveable { mutableStateOf(false) }
A very similar case of storing indexes across list items is shown in the official documentation here

How do you fling a ScrollState in Jetpack Compose?

For reasons that have to do with Jetpack Compose input modifiers consuming all MotionEvents, I find myself writing my own scroll routine for a Composable, of which I have access to the ScrollState.
I have figured out everything I need except flinging. I can't see how to apply performFling(initialVelocity) on a ScrollState. All I can find in the docs are ScrollState.scrollTo and ScrollState.scrollBy, which aren't so useful with flings, since the scroll destination or size is unknown.
I also can't find a ScrollState listener, similar to onScrollStateChanged(state: Int) in the old Android world, that fires when scrolling state changes.
Here is what I have in case somebody can point me in the right direction:
var lastY: Float? = null
var velocityTracker: VelocityTracker? = null
fun scroll(event: MotionEvent) {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
velocityTracker = VelocityTracker.obtain()
lastY = event.y
}
MotionEvent.ACTION_UP -> {
lastY = null
velocityTracker?.let {
it.computeCurrentVelocity(1000)
val initialVelocity = it.yVelocity
velocityTracker?.recycle()
coroutineScope.launch {
???? scrollState.PERFORMFLING?(initialVelocity) ????
AND THEN WHEN THE FLING IS FINISHED viewModel.scrollOffset = scrollState.value
}
}
}
else -> {
velocityTracker?.addMovement(event)
lastY?.let {
val scrollAmount = it - event.y
lastY = event.y
coroutineScope.launch {
scrollState.scrollBy(scrollAmount)
viewModel.scrollOffset = scrollState.value
}
}
}
}
}
You could try using a nestedScroll:
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return super.onPostFling(consumed, available)
}
override suspend fun onPreFling(available: Velocity): Velocity {
return super.onPreFling(available)
}
}
}
Column(modifier = Modifier
.verticalScroll(rememberScrollState())
.nestedScroll(nestedScrollConnection)) {
}
If this doesn't work, just search
cs.android.com
for theNestedScrollConnection and it should give you a hint on how to handle flinging in Compose. Maybe NestedScrollConnection is all you need since it provides support for scrolling as well. You probably can ditch your code and just use NestedScrollConnection. To see NestedScrollConnection in action, check out the demo:
https://github.com/JohannBlake/Jetmagic

Update State outside the composable function. (Jetpack compose)

I am trying to implement redux with Jetpack compose. The scenario looks like this:
I have a list view where I need to show data, in composable function.
#Composable
fun CreateListView(text: String) {
val listdata = state { store.state }
LazyColumn {
//some listview code here
}
}
above, I want to use the data that I got from the redux store. but the store. The subscription method is standalone, and outside the composable. where, though I am able to update the state through new data, but the changes are not reflecting back to composable listview:
// activity page outside composable
private fun storeSubscription(){
viewModel.storeSubscription = store.subscribe {
when (store.state) {
store.state = // list data from some source
}
}
}
Is it possible to update the composable, like above, from outside the function, and without sending any parameter? Since the redux store is a global one, so it should work I think.
You can use MutableLiveData outside of composable function. Use observeAsState() in composable to recompose when data changes.
private val myLive = MutableLiveData<String>()
fun nonComposableScope(){
myLive.postValue("hello")
}
#Composable
fun MyScreen(textLive:LiveData<String>){
val text: String? by textLive.observeAsState()
// use text here
}
Try something like,
#Composable
fun <T> Store<T>.asState(): State<T> {
val result = remember { mutableStateOf(store.state) }
DisposableEffect {
val unsubscribe = store.subscribe {
result.value = store.state
}
onDispose { unsubscribe() }
}
return result
}
#Composable
fun CreateListView(text: String) {
val listdata by store.asState()
LazyColumn {
//some listview code here
}
}
The exact code might differ as I don't know what redux implementation you are using.
This creates an observable state object that will be updated whenever the lambda passed to subscribe is called. Also, it will automatically unsubscribe when CreateListView is no longer part of the composition.
You have to follow the state hosting pattern
From Android Domcumentaiton
Key Term: State hoisting is a pattern of moving state up the tree to
make a component stateless.
When applied to composables, this often means introducing two
parameters to the composable:
value: T: the current value to display. onValueChange: (T) -> Unit: an
event that requests the value to change where T is the proposed new
value.
So in your case you will save the state in the upper Composable that needs to access it, and pass the value of the state and a lambda function to change it to the other Composable, you can learn more from the Official Documentation.
You could simply use a lambda like so:
(An example from an app I am working on.)
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun RumbleSearchResult(rumbleSearchResult: RumbleSearchResult, onClick: () -> Unit) {
ListItem(
headlineText = {
rumbleSearchResult.title?.let { title ->
Text(title)
}
},
supportingText = {
rumbleSearchResult.channel.let { creator ->
val text = when {
rumbleSearchResult.views > 0 -> {
"${creator.name}, ${rumbleSearchResult.views} views"
}
else -> {
creator.name ?: ""
}
}
Row {
Text(text)
if (creator.isVerified) {
Icon(
painter = painterResource(R.drawable.ic_baseline_verified_24),
tint = Color.Cyan,
contentDescription = stringResource(id = R.string.mainActivity_verified_content_description)
)
}
}
}
},
leadingContent = {
AsyncImage(
rumbleSearchResult.thumbnailSrc,
contentDescription = null,
modifier = Modifier.size(100.dp, 100.dp)
)
},
modifier = Modifier.clickable {
onClick.invoke()
}
)
Divider()
}
Main composable:
LazyColumn {
items(viewModel.searchResults) {
RumbleSearchResult(rumbleSearchResult = it) {
openDialog = true
}
}
}