Compose zIndex in isometric grid - android-jetpack-compose

I have an isometric grid with images and need to make an overlay for each of them. Tried to use Modifier.zIndex recommended in this answer but didn't succeed. So, I decided to pass images second time after my first layer is completed, adding an extra loop. Can it be simplified with zIndex or other methods?
Box {
repeat(2) {
for (y in 0 until gridHeight) {
for (x in 0 until gridWidth) {
Box(
modifier = Modifier.padding(start = start.dp, top = top.dp)
) {
if (it == 0)
itemContent(index, data[index]) //The basic image
else
Box(
modifier = Modifier.size(Dimens.imageWidth.dp),
contentAlignment = BiasAlignment(0f, 0.5f)
)
{
Icon( //The overlay
imageVector = Icons.Filled.Image,
contentDescription = null,
modifier = Modifier.size(32.dp)
)
}
}
}
}
}
}

Related

Multitouch with gestures in Jetpack Compose

In this gesture multi-point control demo, it should be zooming and rotating with two fingers, but I found that one finger can be dragged in the test.
I want to drag with two fingers, how can I modify it?
#Composable
fun TransformableSample() {
// set up all transformation states
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
scale *= zoomChange
rotation += rotationChange
offset += offsetChange
}
Box(
Modifier
// apply other transformations like rotation and zoom
// on the pizza slice emoji
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
translationX = offset.x,
translationY = offset.y
)
// add transformable to listen to multitouch transformation events
// after offset
.transformable(state = state)
.background(Color.Blue)
.fillMaxSize()
)
}
Modifier.transformable doesn't have much customization options, you can copy source code file to your project and replace
val panChange = event.calculatePan()
With
val panChange = if (event.changes.count() > 1) event.calculatePan() else Offset.Zero
You can create a Modifier as
Modifier
.pointerInput(Unit) {
awaitEachGesture {
// Wait for at least one pointer to press down
awaitFirstDown()
do {
val event = awaitPointerEvent()
// You can set this as required
if(event.changes.size==2){
offset = event.calculatePan()
// zoom
event.calculateZoom()
// rotation
event.calculateRotation()
}
// This is for preventing other gestures consuming events
// prevent scrolling or other continuous gestures
event.changes.forEach { pointerInputChange: PointerInputChange ->
pointerInputChange.consume()
}
} while (event.changes.any { it.pressed })
}
}
I also wrote a library that returns transform values with start, move, and end callbacks and number of pointer down that you can also use
https://github.com/SmartToolFactory/Compose-Extended-Gestures
Modifier.pointerInput(Unit) {
detectTransformGestures(
onGestureStart = {
transformDetailText = "GESTURE START"
},
onGesture = { gestureCentroid: Offset,
gesturePan: Offset,
gestureZoom: Float,
gestureRotate: Float,
mainPointerInputChange: PointerInputChange,
pointerList: List<PointerInputChange> ->
},
onGestureEnd = {
borderColor = Color.LightGray
transformDetailText = "GESTURE END"
}
)
}
Or this one that checks number of pointers and prequsite condition before returning zoom, rotate, drag and so on
Modifier
.pointerInput(Unit) {
detectPointerTransformGestures(
numberOfPointers = 1,
requisite = PointerRequisite.GreaterThan,
onGestureStart = {
transformDetailText = "GESTURE START"
},
onGesture = { gestureCentroid: Offset,
gesturePan: Offset,
gestureZoom: Float,
gestureRotate: Float,
numberOfPointers: Int ->
},
onGestureEnd = {
transformDetailText = "GESTURE END"
}
)
}

Why isn't conversion from dp to px precise?

I'm trying to draw a black rectangle that covers the yellow Box composable, After converting the size to pixels. The Box is still slightly visible underneath. Is there a way around this?
val size = 50.dp
Box(
modifier = Modifier
.size(size)
.background(color = Color.Yellow)
.drawWithContent {
val sizePx = size.toPx()
drawRect(
color = Color.Black,
size = Size(width = sizePx, height = sizePx)
)
}
)
Dp to px conversion is dp.value * density and it's precise. However Modifier.layout or Layout returns dimensions in Int while conversion dpSize.toPx() and you can use dpSize.roundToPx() for Int conversion.
On my device
var text by remember { mutableStateOf("") }
val dpSize = 50.dp
Box(
modifier = Modifier
.size(dpSize)
.background(color = Color.Yellow)
.drawWithContent {
val sizePx = dpSize.toPx()
text = "drawWithContent: density: $density, dpSize in px: $sizePx, drawScope size: ${size.width}\n"
drawRect(
color = Color.Black,
size = Size(width = sizePx, height = sizePx),
)
}
)
Text(text = text)
When you draw anything inside DrawScope if you are going to draw anything that covers your Composable you don't need to pass a size.
You can use DrawScope.size. Same goes for pointerEvents either. inside pointer scope and draw scope you can get size without any extra work.
The issue you faced might be a preview bug. Even if it wasn't precise size you get from conversion is bigger than Composable size

Detecting counter clockwise rotary input, Wear OS Jetpack Compose

I'm trying to implement rotary input scrolling (currently with Galaxy Watch 4, so the bezel controls rotary input) on a Horizontal Pager. I can get it to move forward, but not backwards, no matter what direction I move the bezel in. How do I detect counter clockwise rotary to make the pager go back instead of forward?
Note:
pagerState.scrollBy(it.horizontalScrollPixels) does work forwards and backwards but doesn't snap to the next page, only slowly scrolls partially. This can be solved this way too (barring janky animation, animateToPage flows better, but presents the same lack of backwards scroll issue). I will accept an answer that can get a value to snap to the next page for all different screen sizes centered using scrollBy. I'm thinking it's it.horizontalScrollPixels times something ("it" is a RotaryScrollEvent object)
This code moves the pager forward
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
HorizontalPager(
count = 4,
state = pagerState,
// Add 32.dp horizontal padding to 'center' the pages
modifier = Modifier.fillMaxSize().onRotaryScrollEvent {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.targetPage, 1f)
}
true
}
.focusRequester(focusRequester)
.focusable()
You should check how the direction and size of the scroll event.
Also scroll by changing the target page, not the offset within that page. You code happens to work because scrolling to 1.0 in the page moves to the next page.
/**
* ScrollableState integration for Horizontal Pager.
*/
public class PagerScrollHandler(
private val pagerState: PagerState,
private val coroutineScope: CoroutineScope
) : ScrollableState {
override val isScrollInProgress: Boolean
get() = totalDelta != 0f
override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta)
private var totalDelta = 0f
private val scrollableState = ScrollableState { delta ->
totalDelta += delta
val offset = when {
// tune to match device
totalDelta > 40f -> {
1
}
totalDelta < -40f -> {
-1
}
else -> null
}
if (offset != null) {
totalDelta = 0f
val newTargetPage = pagerState.targetPage + offset
if (newTargetPage in (0 until pagerState.pageCount)) {
coroutineScope.launch {
pagerState.animateScrollToPage(newTargetPage, 0f)
}
}
}
delta
}
override suspend fun scroll(
scrollPriority: MutatePriority,
block: suspend ScrollScope.() -> Unit
) {
scrollableState.scroll(block = block)
}
}
val state = rememberPagerState()
val pagerScrollHandler = remember { PagerScrollHandler(state, coroutineScope) }
modifier = Modifier
.fillMaxSize()
.onRotaryScrollEvent {
coroutineScope.launch {
pagerScrollHandler.scrollBy(it.verticalScrollPixels)
}
true
}
.focusRequester(viewModel.focusRequester)
.focusable()
Also you should check that targetPage + offset is a valid page.
I tested this on a Galaxy Watch 4. Using this guide in the official documentation (https://developer.android.com/training/wearables/user-input/rotary-input#kotlin) I printed the delta values when I scrolled using bezel of the watch. For each scroll that I made i clockwise direction I got a delta of 128 and -128 for each counterclockwise scroll.
Using simple if else blocks I was able to distinguish when I scrolled above and below.
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_SCROLL && event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)) {
runBlocking {
val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) *
ViewConfigurationCompat.getScaledVerticalScrollFactor(
ViewConfiguration.get(baseContext), baseContext
)
if (delta < 127) {
scalingLazyListState.scrollBy(-100f)
} else {
scalingLazyListState.scrollBy(100f)
}
}
}
return super.onGenericMotionEvent(event)
}

AnimatedVisibility doesn't work in Canvas

When I use AnimatedVisibility around Canvas, it doesn't work.
AnimatedVisibility(
visible = firstShowVisible,
modifier = Modifier.align(Alignment.Center),
enter = fadeIn(0f, tween(300, 3100, LinearEasing))
) {
Canvas(modifier = Modifier) {
drawCircleBackground(color, radius, strokeWidth)
drawCircleProgress(color, progress, radius, strokeWidth)
}
}
The Canvas item would show immediately, rather than fade in slowly.
And firstShowVisible is changed by
var firstShowVisible by remember { mutableStateOf(false) }
LaunchedEffect(true) {
firstShowVisible = true
}
It works for other items, cannot work for Canvas only to me
It's solved.
It's because the arc I draw is not in the area of the outer Box.
When I make the outer Box fillMaxSize, it works.

Move a UIImage inside of CGRect using animateWithDuration

So I'm trying to figure out how to move UIImages that are drawn inside a CGRect up a cell on the screen using animateWithDuration, but I'm having trouble visualizing where to write the code as well as how to write it for a UIImage. I have an array of CGRects with content the UIImage drawn inside of it, and I want to move all of the images at the same time. Once they get to the top most cell, I want them to then appear in the bottom cell and start again. Here is a picture to get a better idea of what I'm talking about:
DragMeToHell Screenshot
And here's my drawRect code for the UIView and UIImages:
override func drawRect(rect: CGRect) {
print( "drawRect:" )
let context = UIGraphicsGetCurrentContext()! // obtain graphics context
// CGContextScaleCTM( context, 0.5, 0.5 ) // shrink into upper left quadrant
let bounds = self.bounds // get view's location and size
let w = CGRectGetWidth( bounds ) // w = width of view (in points)
let h = CGRectGetHeight( bounds ) // h = height of view (in points)
self.dw = w/10.0 // dw = width of cell (in points)
self.dh = h/10.0 // dh = height of cell (in points)
print( "view (width,height) = (\(w),\(h))" )
print( "cell (width,height) = (\(self.dw),\(self.dh))" )
// draw lines to form a 10x10 cell grid
CGContextBeginPath( context ) // begin collecting drawing operations
for i in 1..<10 {
// draw horizontal grid line
let iF = CGFloat(i)
CGContextMoveToPoint( context, 0, iF*(self.dh) )
CGContextAddLineToPoint( context, w, iF*self.dh )
}
for i in 1..<10 {
// draw vertical grid line
let iFlt = CGFloat(i)
CGContextMoveToPoint( context, iFlt*self.dw, 0 )
CGContextAddLineToPoint( context, iFlt*self.dw, h )
}
UIColor.grayColor().setStroke() // use gray as stroke color
CGContextDrawPath( context, CGPathDrawingMode.Stroke ) // execute collected drawing ops
// establish bounding box for image
let tl = self.inMotion ? CGPointMake( self.x, self.y )
: CGPointMake( CGFloat(row)*self.dw, CGFloat(col)*self.dh )
let imageRect = CGRectMake(tl.x, tl.y, self.dw, self.dh)
// place images in random cells
//cellCoordinates = self.generateCoordinates()
for xy in cellCoordinates {
let randomImageRect = CGRectMake(xy.x, xy.y, self.dw, self.dh)
let lavaImage : UIImage? = UIImage(named: "lava.png")
lavaImage!.drawInRect(randomImageRect)
imageCells.append(randomImageRect)
}
// place appropriate image where dragging stopped [EDITED]
var img : UIImage?
if ( self.col == 9 ) {
img = UIImage(named:"otto.png")
} else {
img = UIImage(named:"angel.png")
}
img!.drawInRect(imageRect)
// check for image intersection
for tempImageRect in imageCells {
if (CGRectIntersectsRect(imageRect, tempImageRect)) {
img = UIImage(named:"devil.png")
img!.drawInRect(imageRect)
self.backgroundColor = UIColor.redColor()
}
}
}

Resources