diff --git a/app/src/main/java/com/iqball/app/component/Actions.kt b/app/src/main/java/com/iqball/app/component/Actions.kt new file mode 100644 index 0000000..6ae6025 --- /dev/null +++ b/app/src/main/java/com/iqball/app/component/Actions.kt @@ -0,0 +1,330 @@ +package com.iqball.app.component + +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.rotate +import arrow.core.Either +import com.iqball.app.geo.Vector +import com.iqball.app.model.tactic.Action +import com.iqball.app.model.tactic.ActionType +import com.iqball.app.model.tactic.ComponentId +import com.iqball.app.model.tactic.Segment +import com.iqball.app.model.tactic.StepContent +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin + +private const val ArrowWidthPx = 5F +private const val ArrowHeadHeightPx = 35F + +fun drawActions( + drawer: ContentDrawScope, + content: StepContent, + offsets: Map, + area: Rect, + playersPixelsRadius: Float +) { + for (component in content.components) { + val originPos = offsets[component.id]!! + for (action in component.actions) { + + val type = action.type + + val forceStraight = type == ActionType.Shoot + + val strokeStyle = when (type) { + ActionType.Screen, ActionType.Move, ActionType.Dribble -> Stroke(width = ArrowWidthPx) + ActionType.Shoot -> Stroke( + width = ArrowWidthPx, + pathEffect = PathEffect.dashPathEffect(floatArrayOf(15F, 15F)) + ) + } + + // draw the arrow body + if (forceStraight) { + val targetOffset = when (val value = action.target) { + is Either.Left -> offsets[value.value]!! + is Either.Right -> value.value.posWithinArea(area) + } + val path = Path() + val pathStart = constraintInCircle( + originPos, + getOrientationPointFromSegmentBounds( + action.segments, + originPos, + offsets, + area, + false + ), + playersPixelsRadius + ) + val pathEnd = constraintInCircle( + targetOffset, + getOrientationPointFromSegmentBounds( + action.segments, + originPos, + offsets, + area, + true + ), + playersPixelsRadius + ArrowHeadHeightPx + ) + path.moveTo(pathStart.x, pathStart.y) + path.lineTo(pathEnd.x, pathEnd.y) + drawer.drawPath( + path = path, + color = Color.Black, + style = strokeStyle + ) + } else { + drawer.drawPath( + path = computeSegmentsToPath( + originPos, + action.segments, + offsets, + area, + type == ActionType.Dribble, + playersPixelsRadius + ), + color = Color.Black, + style = strokeStyle + ) + } + + drawArrowHead(drawer, originPos, action, offsets, area, playersPixelsRadius) + } + } +} + +private fun drawArrowHead( + drawer: DrawScope, + originPos: Vector, + action: Action, + offsets: Map, + area: Rect, + playersPixelsRadius: Float +) { + val segments = action.segments + val lastSegment = segments.last() + val target = lastSegment.next + var targetPos = extractPos(target, offsets, area) + val segmentOrientationPoint = + getOrientationPointFromSegmentBounds(segments, originPos, offsets, area, true) + + targetPos = constraintInCircle(targetPos, segmentOrientationPoint, playersPixelsRadius) + + val pathAngleToTarget = -targetPos.angleDegWith(segmentOrientationPoint) + + drawer.rotate(pathAngleToTarget, pivot = targetPos.toOffset()) { + + val path = + if (action.type == ActionType.Screen) getRectangleHeadPath(targetPos) else getTriangleHeadPath( + targetPos + ) + drawPath(path = path, color = Color.Black, style = Fill) + } +} + +private fun getTriangleHeadPath(start: Vector): Path { + val path = Path() + path.moveTo(start.x, start.y) + path.relativeLineTo(-ArrowHeadHeightPx, -ArrowHeadHeightPx) + path.relativeLineTo(ArrowHeadHeightPx * 2, 0F) + return path +} + +private fun getRectangleHeadPath(start: Vector): Path { + val path = Path() + path.moveTo(start.x, start.y - ArrowHeadHeightPx / 2F) + path.relativeLineTo(-ArrowHeadHeightPx, 0F) + path.relativeLineTo(0F, -ArrowHeadHeightPx / 2F) + path.relativeLineTo(ArrowHeadHeightPx * 2, 0F) + path.relativeLineTo(0F, ArrowHeadHeightPx / 2F) + return path +} + +private fun getOrientationPointFromSegmentBounds( + segments: List, + originPos: Vector, + offsets: Map, + area: Rect, + head: Boolean +): Vector { + val boundSegment = if (head) segments.last() else segments.first() + return boundSegment + .controlPoint?.posWithinArea(area) + ?: if (segments.size == 1) + if (head) originPos else extractPos(segments.last().next, offsets, area) + else run { + val referenceSegment = if (head) segments[segments.size - 2] else segments[1] + extractPos(referenceSegment.next, offsets, area) + } +} + +private fun computeSegmentsToPath( + originPos: Vector, + segments: List, + offsets: Map, + area: Rect, + wavy: Boolean, + playersPixelsRadius: Float +): Path { + val path = Path() + + + val firstSegment = segments.first() + + var segmentStart = constraintInCircle( + originPos, + getOrientationPointFromSegmentBounds(segments, originPos, offsets, area, false), + playersPixelsRadius + ) + + var lastSegmentCp = + firstSegment.controlPoint?.posWithinArea(area) ?: (segmentStart + extractPos( + firstSegment.next, offsets, area + ) / 2F) + + path.moveTo(segmentStart.x, segmentStart.y) + + + for (i in segments.indices) { + val segment = segments[i] + var nextPos = extractPos(segment.next, offsets, area) + + if (i == segments.size - 1) { + // if it is the last segment, the next position must be constrained to the player's radius + nextPos = constraintInCircle( + nextPos, + getOrientationPointFromSegmentBounds( + segments, + originPos, + offsets, + area, + true + ), + playersPixelsRadius + ArrowHeadHeightPx + ) + } + + val segmentCp = segment.controlPoint?.posWithinArea(area) ?: ((segmentStart + nextPos) / 2F) + val castedCp = + if (i == 0) segmentCp else segmentStart + (segmentStart - lastSegmentCp) + + if (wavy) { + wavyBezier(segmentStart, castedCp, segmentCp, nextPos, 7, 15, path) + } else { + path.cubicTo(castedCp.x, castedCp.y, segmentCp.x, segmentCp.y, nextPos.x, nextPos.y) + } + + lastSegmentCp = segmentCp + segmentStart = nextPos + } + + return path +} + +private fun cubicBeziersDerivative( + start: Vector, + cp1: Vector, + cp2: Vector, + end: Vector, + t: Float +): Vector = ((cp1 - start) * 3F * (1 - t).pow(2)) + + ((cp2 - cp1) * 6F * (1 - t) * t) + + ((end - cp2) * 3F * t.pow(2)) + + +private fun cubicBeziers( + start: Vector, + cp1: Vector, + cp2: Vector, + end: Vector, + t: Float +): Vector = (start * (1 - t).pow(3)) + + (cp1 * 3F * t * (1 - t).pow(2)) + + (cp2 * 3F * t.pow(2) * (1 - t)) + + (end * t.pow(3)) + +private fun wavyBezier( + start: Vector, + cp1: Vector, + cp2: Vector, + end: Vector, + wavesPer100Px: Int, + amplitude: Int, + path: Path +) { + + fun getVerticalDerivativeProjectionAmplification(t: Float): Vector { + val velocity = cubicBeziersDerivative(start, cp1, cp2, end, t) + val velocityLength = velocity.norm() + val projection = Vector(velocity.y, -velocity.x) + + return (projection / velocityLength) * amplitude + } + + + val dist = start.distanceFrom(cp1) + cp1.distanceFrom(cp2) + cp2.distanceFrom(end) + + val waveLength = (dist / 100) * wavesPer100Px * 2 + val step = 1F / waveLength + + // 0 : middle to up + // 1 : up to middle + // 2 : middle to down + // 3 : down to middle + var phase = 0 + + var t = step + while (t <= 1) { + val pos = cubicBeziers(start, cp1, cp2, end, t) + val amplification = getVerticalDerivativeProjectionAmplification(t) + + val nextPos = when (phase) { + 1, 3 -> pos + 0 -> pos + amplification + else -> pos - amplification + } + + val controlPointBase = cubicBeziers(start, cp1, cp2, end, t - step / 2) + val controlPoint = + if (phase == 0 || phase == 1) controlPointBase + amplification else controlPointBase - amplification + path.quadraticBezierTo(controlPoint.x, controlPoint.y, nextPos.x, nextPos.y) + phase = (phase + 1) % 4 + t += step + if (t < 1 && t > 1 - step) t = 1F + } +} + +/** + * Given a circle shaped by a central position, and a radius, return + * a position that is constrained on its perimeter, pointing to the direction + * between the circle's center and the reference position. + * @param center circle's center. + * @param reference a reference point used to create the angle where the returned position + * will point to on the circle's perimeter + * @param radius circle's radius. + */ +private fun constraintInCircle(center: Vector, reference: Vector, radius: Float): Vector { + val theta = center.angleRadWith(reference) + return Vector( + x = center.x - sin(theta) * radius, + y = center.y - cos(theta) * radius + ) +} + +private fun extractPos( + next: Either, + offsets: Map, + area: Rect +) = when (next) { + is Either.Left -> offsets[next.value]!! + is Either.Right -> next.value.posWithinArea(area) +} \ No newline at end of file diff --git a/app/src/main/java/com/iqball/app/component/BasketCourt.kt b/app/src/main/java/com/iqball/app/component/BasketCourt.kt index 7c7ffce..cb17aaf 100644 --- a/app/src/main/java/com/iqball/app/component/BasketCourt.kt +++ b/app/src/main/java/com/iqball/app/component/BasketCourt.kt @@ -2,19 +2,32 @@ package com.iqball.app.component import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp import com.iqball.app.R import com.iqball.app.domains.getPlayerInfo +import com.iqball.app.geo.toVector import com.iqball.app.model.tactic.BallComponent +import com.iqball.app.model.tactic.ComponentId import com.iqball.app.model.tactic.CourtType import com.iqball.app.model.tactic.PlayerLike import com.iqball.app.model.tactic.StepContent @@ -31,40 +44,73 @@ fun BasketCourt(content: StepContent, type: CourtType) { val zoomState = rememberZoomState() val components = content.components - Row( + val componentsOffset = remember { mutableStateMapOf() } + var courtArea by remember { mutableStateOf(Rect.Zero) } + + val playersPixelsRadius = LocalDensity.current.run { PlayerPieceDiameterDp.dp.toPx() / 2 } + + Box( modifier = Modifier .background(Color.LightGray) - .fillMaxSize() - .zoomable(zoomState), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + .fillMaxSize(), + contentAlignment = Alignment.Center ) { - Box { + Box( + modifier = Modifier + .width(IntrinsicSize.Min) + .zoomable(zoomState), + contentAlignment = Alignment.Center, + ) { Image( painter = painterResource(id = courtImg), contentDescription = "court", modifier = Modifier .background(Color.White) - .align(Alignment.Center) - .fillMaxHeight() + .fillMaxSize() ) - for (component in components) { - when (component) { - is PlayerLike -> { - val info = getPlayerInfo(component, content) - PlayerPiece( - player = info, - modifier = Modifier.align(info.pos.toBiasAlignment()) - ) + Box( + modifier = Modifier + .fillMaxSize() + .onGloballyPositioned { + if (courtArea == Rect.Zero) + courtArea = it.boundsInRoot() + } + .drawWithContent { + val relativeOffsets = + componentsOffset.mapValues { (it.value - courtArea.topLeft).toVector() } + drawActions(this, content, relativeOffsets, courtArea, playersPixelsRadius) + drawContent() } + ) { + for (component in components) { + val modifier = Modifier + .onGloballyPositioned { + if (!componentsOffset.containsKey(component.id)) + componentsOffset[component.id] = it.boundsInRoot().center + } + when (component) { + is PlayerLike -> { + val info = getPlayerInfo(component, content) + PlayerPiece( + player = info, + modifier = modifier + .align(info.pos.toBiasAlignment()) - is BallComponent -> BallPiece( - modifier = Modifier.align(component.pos.toPos().toBiasAlignment()) - ) + ) + } + + is BallComponent -> BallPiece( + modifier = modifier + .align( + component.pos + .toPos() + .toBiasAlignment() + ) + ) + } } } } } - } \ No newline at end of file diff --git a/app/src/main/java/com/iqball/app/component/StepTree.kt b/app/src/main/java/com/iqball/app/component/StepTree.kt index 869a353..21a55af 100644 --- a/app/src/main/java/com/iqball/app/component/StepTree.kt +++ b/app/src/main/java/com/iqball/app/component/StepTree.kt @@ -38,14 +38,17 @@ fun StepsTree(root: StepNodeInfo, selectedNodeId: Int, onNodeSelected: (StepNode val scrollState = rememberFreeScrollState() val nodesOffsets = remember { mutableStateMapOf() } - var globalOffset by remember { mutableStateOf(Offset(0F, 0F)) } + var globalOffset by remember { mutableStateOf(Offset.Zero) } Box( modifier = Modifier .fillMaxSize() .background(Color.LightGray) .freeScroll(scrollState) - .onGloballyPositioned { globalOffset = it.boundsInRoot().topLeft } + .onGloballyPositioned { + if (globalOffset == Offset.Zero) + globalOffset = it.boundsInRoot().topLeft + } .drawWithContent { if (nodesOffsets.isEmpty()) { @@ -56,10 +59,15 @@ fun StepsTree(root: StepNodeInfo, selectedNodeId: Int, onNodeSelected: (StepNode val toDraw = mutableListOf(root) while (toDraw.isNotEmpty()) { val parent = toDraw.removeLast() - val parentCenter = nodesOffsets[parent]!!.center.minus(globalOffset) + val parentCenter = nodesOffsets[parent]!!.center - globalOffset for (children in parent.children) { - val childrenCenter = nodesOffsets[children]!!.center.minus(globalOffset) - drawLine(Color.Black, start = parentCenter, end = childrenCenter, strokeWidth = 5F) + val childrenCenter = nodesOffsets[children]!!.center - globalOffset + drawLine( + Color.Black, + start = parentCenter, + end = childrenCenter, + strokeWidth = 5F + ) toDraw += children } } @@ -92,7 +100,8 @@ private fun StepsTreeContent( modifier = Modifier .padding(10.dp) .onGloballyPositioned { - nodesOffsets[node] = it.boundsInRoot() + if (!nodesOffsets.containsKey(node)) + nodesOffsets[node] = it.boundsInRoot() } ) diff --git a/app/src/main/java/com/iqball/app/domains/PlayerDomains.kt b/app/src/main/java/com/iqball/app/domains/PlayerDomains.kt index deed797..c084757 100644 --- a/app/src/main/java/com/iqball/app/domains/PlayerDomains.kt +++ b/app/src/main/java/com/iqball/app/domains/PlayerDomains.kt @@ -46,11 +46,11 @@ fun computePhantomPosition(phantom: PhantomComponent, content: StepContent): Vec ?.merge() ?: computeComponentPosition(phantomBefore, content) - val axisSegment = (referentPos - directionalPos) + val axisSegment = (directionalPos - referentPos) val segmentLength = axisSegment.norm() val projectedVector = Vector( - x = (axisSegment.x / segmentLength) * 0.05, - y = (axisSegment.y / segmentLength) * 0.05, + x = (axisSegment.x / segmentLength) * 0.05F, + y = (axisSegment.y / segmentLength) * 0.05F, ) referentPos + projectedVector diff --git a/app/src/main/java/com/iqball/app/geo/Area.kt b/app/src/main/java/com/iqball/app/geo/Area.kt deleted file mode 100644 index af0e53c..0000000 --- a/app/src/main/java/com/iqball/app/geo/Area.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.iqball.app.geo - -data class Area(val pos: Vector, val width: Float, val height: Float) diff --git a/app/src/main/java/com/iqball/app/geo/Vector.kt b/app/src/main/java/com/iqball/app/geo/Vector.kt index a768a68..9140f54 100644 --- a/app/src/main/java/com/iqball/app/geo/Vector.kt +++ b/app/src/main/java/com/iqball/app/geo/Vector.kt @@ -1,25 +1,45 @@ package com.iqball.app.geo import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import kotlin.math.atan2 import kotlin.math.sqrt typealias Pos = Vector -data class Vector(val x: Double, val y: Double) { +data class Vector(val x: Float, val y: Float) { fun toBiasAlignment(): BiasAlignment = - BiasAlignment((x * 2 - 1).toFloat(), (y * 2 - 1).toFloat()) + BiasAlignment((x * 2 - 1), (y * 2 - 1)) infix operator fun minus(other: Vector) = Vector(x - other.x, y - other.y) infix operator fun plus(other: Vector) = Vector(x + other.x, y + other.y) + infix operator fun div(other: Vector) = Vector(x / other.x, y / other.y) + infix operator fun div(n: Float) = Vector(x / n, y / n) - fun distanceWith(other: Vector) = + infix operator fun times(other: Vector) = Vector(x * other.x, y * other.y) + infix operator fun times(n: Float) = Vector(x * n, y * n) + infix operator fun times(n: Int) = Vector(x * n, y * n) + + + fun angleRadWith(other: Vector): Float { + val (x, y) = this - other + return atan2(x, y) + } + + fun angleDegWith(other: Vector): Float = (angleRadWith(other) * (180 / Math.PI)).toFloat() + + + fun distanceFrom(other: Vector) = sqrt(((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y))) - fun norm() = NullVector.distanceWith(this) + fun norm() = NullVector.distanceFrom(this) + + fun posWithinArea(area: Rect) = Vector(x * area.width, y * area.height) - fun posWithinArea(area: Area) = Vector(x * area.width, y * area.height) - fun ratioWithinArea(area: Area) = - Vector((x - area.pos.x) * area.width, (y - area.pos.y) * area.height) + fun toOffset() = Offset(x, y) } -val NullVector = Vector(.0, .0) \ No newline at end of file +val NullVector = Vector(0F, 0F) + +fun Offset.toVector() = Vector(x, y) \ No newline at end of file diff --git a/app/src/main/java/com/iqball/app/model/tactic/Action.kt b/app/src/main/java/com/iqball/app/model/tactic/Action.kt index bfa35c5..9a68d9e 100644 --- a/app/src/main/java/com/iqball/app/model/tactic/Action.kt +++ b/app/src/main/java/com/iqball/app/model/tactic/Action.kt @@ -1,6 +1,7 @@ package com.iqball.app.model.tactic import arrow.core.Either +import arrow.core.NonEmptyList import com.iqball.app.geo.Vector diff --git a/app/src/main/java/com/iqball/app/model/tactic/Positioning.kt b/app/src/main/java/com/iqball/app/model/tactic/Positioning.kt index e9be5bf..ed02c62 100644 --- a/app/src/main/java/com/iqball/app/model/tactic/Positioning.kt +++ b/app/src/main/java/com/iqball/app/model/tactic/Positioning.kt @@ -4,6 +4,6 @@ import com.iqball.app.geo.Pos sealed interface Positioning data class RelativePositioning(val attach: ComponentId) : Positioning -data class FixedPosition(val x: Double, val y: Double) : Positioning { +data class FixedPosition(val x: Float, val y: Float) : Positioning { fun toPos() = Pos(x, y) } diff --git a/app/src/main/java/com/iqball/app/serialization/EitherTypeAdapter.kt b/app/src/main/java/com/iqball/app/serialization/EitherTypeAdapter.kt index 0ad4d36..6854a60 100644 --- a/app/src/main/java/com/iqball/app/serialization/EitherTypeAdapter.kt +++ b/app/src/main/java/com/iqball/app/serialization/EitherTypeAdapter.kt @@ -2,27 +2,46 @@ package com.iqball.app.serialization import arrow.core.Either import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.rawType -import java.lang.ClassCastException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type -private class EitherTypeAdapter(val leftType: JsonPrimitiveType, val rightType: JsonPrimitiveType) : - JsonAdapter>() { +private class EitherTypeAdapter( + val leftType: Class<*>, + val rightType: Class<*>, + private val moshi: Moshi +) : JsonAdapter>() { + + private val leftJsonType = leftType.getJsonPrimitive() + private val rightJsonType = rightType.getJsonPrimitive() + override fun fromJson(reader: JsonReader): Either<*, *>? { - val value = reader.readJsonValue() ?: return null - val valueJsonType = value.javaClass.getJsonPrimitive() - if (valueJsonType == leftType) + val valueJsonType = when (val token = reader.peek()) { + JsonReader.Token.BEGIN_ARRAY -> JsonPrimitiveType.Array + JsonReader.Token.BEGIN_OBJECT -> JsonPrimitiveType.Object + JsonReader.Token.STRING -> JsonPrimitiveType.String + JsonReader.Token.NUMBER -> JsonPrimitiveType.Number + JsonReader.Token.BOOLEAN -> JsonPrimitiveType.Boolean + JsonReader.Token.NULL -> return null + else -> throw JsonDataException("unexpected token : $token") + } + + if (valueJsonType == leftJsonType) { + val value = moshi.adapter(leftType).fromJson(reader) return Either.Left(value) + } - if (valueJsonType == rightType) + if (valueJsonType == rightJsonType) { + val value = moshi.adapter(rightType).fromJson(reader) return Either.Right(value) - throw ClassCastException("Cannot cast a value of type " + value.javaClass + " as either " + leftType.name.lowercase() + " or " + rightType.name.lowercase()) + } + throw ClassCastException("Cannot cast a json value of type " + valueJsonType + " as either " + leftType.name.lowercase() + " or " + rightType.name.lowercase()) } @@ -37,7 +56,7 @@ private class EitherTypeAdapter(val leftType: JsonPrimitiveType, val rightType: } -object EitherTypeAdapterFactory: JsonAdapter.Factory { +object EitherTypeAdapterFactory : JsonAdapter.Factory { override fun create( type: Type, annotations: MutableSet, @@ -47,12 +66,12 @@ object EitherTypeAdapterFactory: JsonAdapter.Factory { return null if (type.rawType != Either::class.java) return null - val leftType = type.actualTypeArguments[0].rawType.getJsonPrimitive() - val rightType = type.actualTypeArguments[1].rawType.getJsonPrimitive() - if (leftType == rightType) { + val leftType = type.actualTypeArguments[0].rawType + val rightType = type.actualTypeArguments[1].rawType + if (leftType.getJsonPrimitive() == rightType.getJsonPrimitive()) { throw UnsupportedOperationException("Cannot handle Either types with both sides being object, array, string or number. Provided type is : $type") } - return EitherTypeAdapter(leftType, rightType) + return EitherTypeAdapter(leftType, rightType, moshi) } } @@ -60,12 +79,13 @@ private enum class JsonPrimitiveType { Array, Object, String, - Number; + Number, + Boolean } private fun Class<*>.getJsonPrimitive(): JsonPrimitiveType { if (isPrimitive) - return JsonPrimitiveType.Number + return if (java.lang.Boolean.TYPE == this) JsonPrimitiveType.Boolean else JsonPrimitiveType.Number if (isArray) return JsonPrimitiveType.Array if (this == String::class.java) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3a9e53e..ca4c795 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,7 @@ composeFreeScroll = "0.2.2" converterGson = "2.9.0" converterMoshi = "2.5.0" converterMoshiVersion = "2.5.0" +graphicsShapes = "1.0.0-alpha05" kotlin = "1.9.0" coreKtx = "1.12.0" junit = "4.13.2" @@ -26,6 +27,7 @@ zoomable = "1.6.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-graphics-shapes = { module = "androidx.graphics:graphics-shapes", version.ref = "graphicsShapes" } arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowCore" } compose-free-scroll = { module = "com.github.chihsuanwu:compose-free-scroll", version.ref = "composeFreeScroll" } converter-moshi-v250 = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshiVersion" }