I have a small application that intends to make an animation, consisting of a graphic with 3 elements.
The application consists of:
mainactivity that will instantiate an object of the AnimatedCanvas class and that will send a vector of 3 pointF objects.
the AnimatedCanvas class that inherits from view and manages the canvas
An abstract class called basicItems, which represents the minimum information to draw basic elements of a canvas, such as circles, lines...
I come across two situations. In the first one, if the elements that I want to draw and animate are variables of the AnimatedCanvas class, both the drawing and the animation are done as expected.
The second is when I get the coordinates sent from the mainactivity that either not all animations (or none) are performed or some element is not initialized correctly.
The idea is:
draw a sphere for each of the elements in the graph, to be represented, making an animation that makes the spheres go from alpha 0f to 1f
perform an animation on the y axis, so that these spheres move to a certain point on the axis
at the same time that the spheres are animated, draw a line that animates and moves along the y-axis, along with its sphere
Heres my code...
`
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">``your text``
<com.example.prueba_circulo_01.AnimatedCanvasView
android:id="#+id/canvas"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="13dp"
/>
</RelativeLayout>
</layout>
import android.os.Bundle
import android.os.Handler
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.example.prueba_circulo_01.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var animateC : AnimatedCanvasView
private val p1 = GraphicPoint("enero", 1f, "uno")
private val p2 = GraphicPoint("enero",2f,"dos")
private val p3 = GraphicPoint("enero", 3f, "tres")
private var listAux = listOf<GraphicPoint>(p1,p2,p3)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.apply {
canvas.addDataPoints(listAux,GraphicType.Default)
}
}
}
BasicItem class
abstract class BasicItem (var x : Float,
var y : Float,
var yFinal : Float,
var paint : Paint,
var alpha: Float
){
abstract fun updateY(value: Float)
abstract fun updateYFinal(final : Float)
abstract fun updateAlpha(value: Float)
}
//----------------------------------------------------------
class VerticalLine1 (x : Float,
y : Float,
yFinal : Float,
paint : Paint,
alpha: Float):BasicItem(x,y,yFinal,paint,alpha){
override fun updateY(value: Float) {
y = value
}
override fun updateYFinal(final: Float) {
yFinal = final
}
override fun updateAlpha(value: Float){
alpha = value
paint.alpha = (255*alpha).toInt()
}
}
//----------------------------------------------------------
class Circle1 ( x : Float,
y : Float,
yFinal : Float,
var radius : Float,
paint : Paint,
alpha : Float):BasicItem(x,y,yFinal,paint,alpha) {
override fun updateY(value: Float) {
y = value
}
override fun updateYFinal(final: Float) {
yFinal = final
}
override fun updateAlpha(value: Float){
alpha = value
paint.alpha = (255*alpha).toInt()
}
}
//----------------------------------------------------------
class StringItem(x : Float,
y : Float,
yFinal : Float,
paint : Paint,
var string: String,
alpha : Float):BasicItem(x,y,yFinal,paint,alpha) {
override fun updateY(value: Float) {
y = value
}
override fun updateYFinal(final: Float) {
yFinal = final
}
override fun updateAlpha(value: Float){
alpha = value
paint.alpha = (255*alpha).toInt()
}
}
AnimatedCanvasView class
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import kotlin.math.abs
import kotlin.math.min
class AnimatedCanvasView(context: Context, attrs: AttributeSet) : View(context, attrs), ValueAnimator.AnimatorUpdateListener {
private val data = mutableListOf<DataPoint>()
private var points = mutableListOf<PointF>()
private val conPoint1 = mutableListOf<PointF>()
private val conPoint2 = mutableListOf<PointF>()
private val graphicPoints = mutableListOf<GraphicPoint>()
private val primaryColorPaint = Paint()
private val borderPathPaint = Paint()
private val blueCircle = Paint()
private val pathPaint = Paint()
private val barPaint = Paint()
private val path = Path()
private val borderPath = Path()
private val barWidth by lazy {
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 1f, resources.displayMetrics
)
}
private val borderPathWidth by lazy {
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics
)
}
private val startX = 0f
private var delay = 500L
private val standardDuration = 3000L
private val standardDelay = 1000L
private val secondDuration = 2000L
private val animatorCircle1 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle2 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle3 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle4 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle5 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle6 = ValueAnimator.ofFloat(0f,1f)
private val animatorCircle10 = ValueAnimator.ofFloat(0f,1f)
private lateinit var circle10 : Circle
private lateinit var animatorVerticalLine1 : ValueAnimator
private lateinit var animatorVerticalLine2 : ValueAnimator
private lateinit var animatorVerticalLine3 : ValueAnimator
private var animatorVerticalLine4 : ValueAnimator? = null
private lateinit var animatorVerticalLine5 : ValueAnimator
private lateinit var animatorVerticalLine6 : ValueAnimator
private var curveTopMargin = 32
private val externalRadiusNormal = 30f
private val externalRadiusFinal = 35f
private val internalRadiusNormal = 23f
private val internalRadiusFinal = 33f
private val secondInternalRadius = 15f
private lateinit var circle1 : Circle
private lateinit var circle2 : Circle
private lateinit var circle3 : Circle
private lateinit var circle4 : Circle
private lateinit var circle5 : Circle
private lateinit var circle6 : Circle
private lateinit var internalCircle1 : Circle
private lateinit var internalCircle2 : Circle
private lateinit var internalCircle3 : Circle
private lateinit var secondInternalCircle : Circle
private lateinit var verticalLine1 : VerticalLine
private lateinit var verticalLine2 : VerticalLine
private lateinit var verticalLine3 : VerticalLine
private lateinit var verticalLine4 : VerticalLine
private lateinit var verticalLine5 : VerticalLine
private lateinit var verticalLine6 : VerticalLine
private lateinit var stringCircle1 : StringItem
private lateinit var stringCircle2 : StringItem
private lateinit var stringCircle3 : StringItem
private lateinit var bezierCurve: BezierCurve
private val paintCircleBlack = Paint()
private val paintCircleNormal = Paint()
private val paintCircleFinal = Paint()
private val paintInternal = Paint()
private val paintLine = Paint()
private val paintVerticalLineGray = Paint()
private val paintVerticalLineGreen = Paint()
private val paintSecondInternalRadius = Paint()
private val paintText = Paint()
private val paintBezier = Paint()
private var empezado = false
private val images = mutableListOf<ImageCoordinates>()
val images2 = mutableListOf<imgCoor>()
//lista de animaciones...
private var animatorList = mutableListOf<ValueAnimator>()
private lateinit var animator2 : ValueAnimator
private lateinit var animator3 : ValueAnimator
private lateinit var basicItem: BasicItem
private lateinit var basicItem2: BasicItem
init {
initPaint()
initItemsToDraw()
initAnimators()
}
override fun onAnimationUpdate(animation: ValueAnimator?) {
animation?.apply {
startDelay = standardDelay
addUpdateListener {
basicItem.updateAlpha(it.animatedValue as Float)
invalidate()
}
addListener(object:AnimatorListenerAdapter(){
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
ValueAnimator.ofFloat(basicItem.y,basicItem.yFinal).apply {
duration=secondDuration
addUpdateListener {
basicItem.updateY(it.animatedValue as Float)
invalidate()
}
start()
}
}
})
//start()
}
}
fun starAnim(){
animator2 = ValueAnimator.ofFloat(0f,1f)
animator2.duration = 1000
animator2.addUpdateListener(this)
animator2.start()
}
private fun initPaint(){
paintCircleBlack.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.BLACK
alpha = 255
}
paintCircleNormal.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.GREEN
alpha = 0
}
paintCircleFinal.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.DKGRAY
alpha = 0
}
paintInternal.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.WHITE
alpha = 0
}
paintLine.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.GREEN
strokeWidth = 3f
}
paintVerticalLineGray.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.GRAY
strokeWidth = 1f
alpha = 1
}
paintVerticalLineGreen.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.GREEN
strokeWidth = 3f
// alpha = 255
alpha = 255
}
paintSecondInternalRadius.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.DKGRAY
alpha = 0
}
paintText.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.BLACK
textSize = 50f
alpha = 0
}
paintBezier.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.GREEN
strokeWidth = 3f
}
borderPathPaint.apply {
isAntiAlias = true
strokeWidth = borderPathWidth
style = Paint.Style.STROKE
color = ContextCompat.getColor(context, R.color.blue_green_alpha_50)
}
barPaint.apply {
isAntiAlias = true
strokeWidth = barWidth
}
pathPaint.apply {
isAntiAlias = true
style = Paint.Style.FILL
color = Color.WHITE
}
blueCircle.apply {
isAntiAlias = true
color = ContextCompat.getColor(context, R.color.purple_700)
}
primaryColorPaint.apply {
isAntiAlias = true
color = ContextCompat.getColor(context, R.color.blue_green)
}
}
//----------------------------------------------------------
private fun initItemsToDraw(){
circle1=Circle(200f,600f,400f,externalRadiusNormal,paintCircleNormal,0f)
circle2=Circle(400f,600f,500f,externalRadiusNormal,paintCircleNormal,0f)
circle3=Circle(600f,600f,300f,externalRadiusFinal,paintCircleFinal,0f)
//circle4=Circle(400f,1500f,600f,externalRadiusFinal,paintCircleFinal,0f)
internalCircle1=Circle(200f,600f,400f,internalRadiusNormal,paintInternal,0f)
internalCircle2=Circle(400f,600f,500f,internalRadiusNormal,paintInternal,0f)
internalCircle3=Circle(600f,600f,300f,internalRadiusFinal,paintInternal,0f)
secondInternalCircle = Circle(600f,600f,300f,secondInternalRadius,paintSecondInternalRadius, 0f)
verticalLine1 = VerticalLine(
x = 200f,
y = 600f,
yFinal = 400f+circle1.radius,
paint = paintVerticalLineGray,
alpha = 1f
)
verticalLine2 = VerticalLine(
x = 400f,
y = 600f,
yFinal = 500f+circle2.radius,
paint = paintVerticalLineGray,
alpha = 1f
)
verticalLine3 = VerticalLine(
x = 600f,
y = 600f,
yFinal = 300f+circle3.radius,
paint = paintVerticalLineGreen,
alpha = 1f
)
stringCircle1 = StringItem(
x = 200f,
y = 600f,
yFinal = 400f+circle1.radius,
paint = paintText,
string = "hola",
alpha = 1f)
bezierCurve = BezierCurve(200f,400f,600f,paintBezier,1f,400f,500f,600f,300f)
}
private fun initAnimators(){
animatorVerticalLine1 = ValueAnimator.ofFloat(verticalLine1.y, verticalLine1.yFinal)
animatorVerticalLine2 = ValueAnimator.ofFloat(verticalLine2.y, verticalLine2.yFinal)
animatorVerticalLine3 = ValueAnimator.ofFloat(verticalLine3.y, verticalLine3.yFinal)
setAnimator(animatorCircle1, circle1)
setAnimator(animatorCircle1, internalCircle1)
setAnimator(animatorCircle2, circle2)
setAnimator(animatorCircle2, internalCircle2)
setAnimator(animatorCircle3, circle3)
setAnimator(animatorCircle3, internalCircle3)
setAnimator(animatorCircle1, secondInternalCircle)
setAnimatorVerticalLine(animatorVerticalLine1, verticalLine1)
setAnimatorVerticalLine(animatorVerticalLine2, verticalLine2)
setAnimatorVerticalLine(animatorVerticalLine3, verticalLine3)
}
private fun initPrueba(){
//basicItem = Circle(600f,1500f,1100f,40f,paintCircleNormal,1f )
basicItem = Circle(points[1].x,height.toFloat() - 16f.toDP(),1100f,40f,paintCircleNormal,1f )
basicItem2 = Circle(points[2].x,height.toFloat() - 16f.toDP(),1100f,70f,paintCircleNormal,1f )
}
private fun initAnimators2(){
try {
//if (animatorVerticalLine4==null) animatorVerticalLine4 = ValueAnimator.ofFloat(verticalLine4.y, verticalLine4.yFinal)
if (animatorVerticalLine4==null) animatorVerticalLine4 = ValueAnimator.ofFloat(1500f, 1000f)
animatorVerticalLine5 = ValueAnimator.ofFloat(verticalLine5.y, verticalLine5.yFinal)
animatorVerticalLine6 = ValueAnimator.ofFloat(verticalLine6.y, verticalLine6.yFinal)
}catch (ex:Exception){}
}
//----------------------------------------------------------
private fun setElementsToAnimators(){
try {
setAnimatorVerticalLine(animatorVerticalLine4, verticalLine4)
setAnimatorVerticalLine(animatorVerticalLine5, verticalLine5)
setAnimatorVerticalLine(animatorVerticalLine6, verticalLine6)
}catch (ex:Exception){}
}
private fun drawHorizontalLine(canvas: Canvas?, paint: Paint) {
canvas?.drawLine(startX, height.toFloat() - 20f.toDP(), width.toFloat(), height.toFloat() - 20f.toDP(), paint)
}
private fun drawBezierCurve(canvas: Canvas?) {
try {
if (points.isEmpty() && conPoint1.isEmpty() && conPoint2.isEmpty()) return
val pointsToDraw = points.filterIndexed { index, _ -> index != 0 }
path.reset()
path.moveTo(pointsToDraw.first().x, pointsToDraw.first().y)
for (i in 1 until pointsToDraw.size) {
path.cubicTo(
conPoint1[i - 1].x, conPoint1[i - 1].y, conPoint2[i - 1].x, conPoint2[i - 1].y,
pointsToDraw[i].x, pointsToDraw[i].y
)
}
borderPath.set(path)
canvas?.drawPath(path, pathPaint.apply {
})
canvas?.drawPath(borderPath, borderPathPaint)
} catch (e: Exception) {
}
}
private fun setAnimator(animatorToSet: ValueAnimator, basicItem: BasicItem){
animatorToSet.apply {
duration = standardDuration
startDelay = standardDelay
addUpdateListener {
basicItem.updateAlpha(it.animatedValue as Float)
invalidate()
}
addListener(object:AnimatorListenerAdapter(){
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
ValueAnimator.ofFloat(basicItem.y,basicItem.yFinal).apply {
duration=secondDuration
addUpdateListener {
basicItem.updateY(it.animatedValue as Float)
invalidate()
}
start()
}
}
})
start()
}
}
private fun setAnimatorVerticalLine(animatorToSet: ValueAnimator?, verticalLine: VerticalLine){
animatorToSet?.apply {
duration = secondDuration
startDelay = standardDelay + standardDuration
repeatCount=0
addUpdateListener {
verticalLine.yFinal = it.animatedValue as Float
verticalLine.paint.alpha = 255
invalidate()
}
start()
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
try {
canvas?.drawCircle(basicItem.x,basicItem.y,40f,basicItem.paint)
canvas?.drawCircle(basicItem2.x,basicItem2.y,70f,basicItem2.paint)
}catch (ex:Exception){
println(ex)}
drawFirstDraw(canvas)
}
//----------------------------------------------------------
private fun drawFirstDraw(canvas: Canvas?){
canvas?.drawCircle(circle1.x,circle1.y, circle1.radius,paintCircleNormal)
canvas?.drawCircle(circle2.x,circle2.y, circle2.radius,paintCircleNormal)
canvas?.drawCircle(circle3.x,circle3.y, circle3.radius,paintCircleNormal)
canvas?.drawCircle(internalCircle1.x,internalCircle1.y, internalCircle1.radius,paintInternal)
canvas?.drawCircle(internalCircle2.x,internalCircle2.y, internalCircle2.radius,paintInternal)
canvas?.drawCircle(internalCircle3.x,internalCircle3.y, internalCircle3.radius,paintInternal)
canvas?.drawCircle(secondInternalCircle.x,secondInternalCircle.y, secondInternalCircle.radius,paintSecondInternalRadius)
canvas?.drawLine(100f,600f,700f,600f,paintLine)
drawHorizontalLine(canvas,paintLine)
canvas?.drawLine(verticalLine1.x,verticalLine1.y,verticalLine1.x,verticalLine1.yFinal,paintVerticalLineGray)
canvas?.drawLine(verticalLine2.x,verticalLine2.y,verticalLine2.x,verticalLine2.yFinal,paintVerticalLineGray)
canvas?.drawLine(verticalLine3.x,verticalLine3.y,verticalLine3.x,verticalLine3.yFinal,paintVerticalLineGreen)
}
private fun resetDataPoints() {
this.data.clear()
points.clear()
graphicPoints.clear()
conPoint1.clear()
conPoint2.clear()
}
private fun calculatePointsForData() {
if (data.isEmpty()) return
val bottomY = height - 20f.toDP()
val xDiff =
(width.toFloat() / (data.size - 1)) //subtract -1 because we want to include position at right side
val maxData = (data.maxByOrNull { it.amount }?.amount ?: 100f) + 10
for (i in 0 until data.size) {
val y = bottomY - (data[i].amount / maxData * (bottomY - curveTopMargin))
points.add(
PointF(
when (i) {
0 -> xDiff * i + 30
data.size - 1 -> xDiff * i - 30
else ->
xDiff * i
}, y
)
)
}
}
private fun calculateConnectionPointsForBezierCurve() {
try {
val pointsToConnectPoints = points.filterIndexed { index, _ -> index != 0 }
for (i in 1 until pointsToConnectPoints.size) {
val a = pointsToConnectPoints[i - 1]
val b = pointsToConnectPoints[i]
conPoint1.add(
PointF(
a.x + (0.3f * (b.x - a.x)),
a.y + 0.3f * (b.y - a.y) / 6
)
)
conPoint2.add(
PointF(
b.x - (0.3f * (b.x - a.x)),
b.y - 0.3f * (b.y - a.y) / 6
)
)
}
} catch (e: Exception) {
}
}
private fun Float.toDP(): Float =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, resources.displayMetrics)
fun addDataPoints(data: List<GraphicPoint>, graphicType: GraphicType = GraphicType.Default) {
post {
Thread(Runnable {
resetDataPoints()
primaryColorPaint.apply {
color = ContextCompat.getColor(context, graphicType.primaryColor)
}
borderPathPaint.apply {
color = ContextCompat.getColor(context, graphicType.borderColor)
}
graphicPoints.add(GraphicPoint("", 0f, ""))
graphicPoints.addAll(data)
val max = graphicPoints.maxByOrNull { it.amount }?.amount ?: 30f
val dataPoints = graphicPoints.map { graphicPoint ->
DataPoint(if(graphicPoint.amount<=0) 0f else (graphicPoint.amount * 30f) / max)
}.toList()
graphicPoints.maxByOrNull { it.amount }
val oldPoints = points.toList()
if (oldPoints.isEmpty()) {
this.data.addAll(dataPoints)
calculatePointsForData()
calculateConnectionPointsForBezierCurve()
initPrueba()
postInvalidate()
return#Runnable
}
resetDataPoints()
this.data.addAll(dataPoints)
calculatePointsForData()
calculateConnectionPointsForBezierCurve()
val newPoints = points.toList()
val size = oldPoints.size
var maxDiffY = 0f
for (i in 0 until size) {
val abs = abs(oldPoints[i].y - newPoints[i].y)
if (abs > maxDiffY) maxDiffY = abs
}
val loopCount = maxDiffY / 16
val tempPointsForAnimation = mutableListOf<MutableList<PointF>>()
for (i in 0 until size) {
val old = oldPoints[i]
val new = newPoints[i]
val plusOrMinusAmount = abs(new.y - old.y) / maxDiffY * 16
var tempY = old.y
val tempList = mutableListOf<PointF>()
for (j in 0..loopCount.toInt()) {
if (tempY == new.y) {
tempList.add(PointF(new.x, new.y))
} else {
if (new.y > old.y) {
tempY += plusOrMinusAmount
tempY = min(tempY, new.y)
tempList.add(PointF(new.x, tempY))
} else {
tempY -= plusOrMinusAmount
tempY = kotlin.math.max(tempY, new.y)
tempList.add(PointF(new.x, tempY))
}
}
}
tempPointsForAnimation.add(tempList)
}
if (tempPointsForAnimation.isEmpty()) return#Runnable
val first = tempPointsForAnimation[0]
val length = first.size
for (i in 0 until length) {
conPoint1.clear()
conPoint2.clear()
points.clear()
points.addAll(tempPointsForAnimation.map { it[i] })
calculateConnectionPointsForBezierCurve()
postInvalidate()
Thread.sleep(16)
}
}).start()
}
}
data class ImageCoordinates(
var x: Float,
var y: Float,
var alpha:Float,
var drawable:Int)
data class imgCoor(
var x: Float,
var y:Float,
var finalY:Float
)
data class GraphicPoint(
val month: String,
val amount: Float,
val amountText: String,
)
data class DataPoint(val amount: Float)
sealed class GraphicType(val primaryColor: Int, val borderColor: Int) {
object Credit : GraphicType(R.color.trans_color_4, R.color.trans_color_4_alpha_50)
object Default : GraphicType(R.color.blue_green, R.color.blue_green_alpha_50)
}
`
was wondering if anyone knows how to produce elliptical/arched list in Compose?
Something along these lines:
Not sure If I'm overlooking an 'easy' way of doing it in Compose. Cheers!
I have an article here that shows how to do this. It is not a LazyList in that it computes all the items (but only renders the visible ones); you can use this as a starting point to build upon.
The full code is below as well:
data class CircularListConfig(
val contentHeight: Float = 0f,
val numItems: Int = 0,
val visibleItems: Int = 0,
val circularFraction: Float = 1f,
val overshootItems: Int = 0,
)
#Stable
interface CircularListState {
val verticalOffset: Float
val firstVisibleItem: Int
val lastVisibleItem: Int
suspend fun snapTo(value: Float)
suspend fun decayTo(velocity: Float, value: Float)
suspend fun stop()
fun offsetFor(index: Int): IntOffset
fun setup(config: CircularListConfig)
}
class CircularListStateImpl(
currentOffset: Float = 0f,
) : CircularListState {
private val animatable = Animatable(currentOffset)
private var itemHeight = 0f
private var config = CircularListConfig()
private var initialOffset = 0f
private val decayAnimationSpec = FloatSpringSpec(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow,
)
private val minOffset: Float
get() = -(config.numItems - 1) * itemHeight
override val verticalOffset: Float
get() = animatable.value
override val firstVisibleItem: Int
get() = ((-verticalOffset - initialOffset) / itemHeight).toInt().coerceAtLeast(0)
override val lastVisibleItem: Int
get() = (((-verticalOffset - initialOffset) / itemHeight).toInt() + config.visibleItems)
.coerceAtMost(config.numItems - 1)
override suspend fun snapTo(value: Float) {
val minOvershoot = -(config.numItems - 1 + config.overshootItems) * itemHeight
val maxOvershoot = config.overshootItems * itemHeight
animatable.snapTo(value.coerceIn(minOvershoot, maxOvershoot))
}
override suspend fun decayTo(velocity: Float, value: Float) {
val constrainedValue = value.coerceIn(minOffset, 0f).absoluteValue
val remainder = (constrainedValue / itemHeight) - (constrainedValue / itemHeight).toInt()
val extra = if (remainder <= 0.5f) 0 else 1
val target =((constrainedValue / itemHeight).toInt() + extra) * itemHeight
animatable.animateTo(
targetValue = -target,
initialVelocity = velocity,
animationSpec = decayAnimationSpec,
)
}
override suspend fun stop() {
animatable.stop()
}
override fun setup(config: CircularListConfig) {
this.config = config
itemHeight = config.contentHeight / config.visibleItems
initialOffset = (config.contentHeight - itemHeight) / 2f
}
override fun offsetFor(index: Int): IntOffset {
val maxOffset = config.contentHeight / 2f + itemHeight / 2f
val y = (verticalOffset + initialOffset + index * itemHeight)
val deltaFromCenter = (y - initialOffset)
val radius = config.contentHeight / 2f
val scaledY = deltaFromCenter.absoluteValue * (config.contentHeight / 2f / maxOffset)
val x = if (scaledY < radius) {
sqrt((radius * radius - scaledY * scaledY))
} else {
0f
}
return IntOffset(
x = (x * config.circularFraction).roundToInt(),
y = y.roundToInt()
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CircularListStateImpl
if (animatable.value != other.animatable.value) return false
if (itemHeight != other.itemHeight) return false
if (config != other.config) return false
if (initialOffset != other.initialOffset) return false
if (decayAnimationSpec != other.decayAnimationSpec) return false
return true
}
override fun hashCode(): Int {
var result = animatable.value.hashCode()
result = 31 * result + itemHeight.hashCode()
result = 31 * result + config.hashCode()
result = 31 * result + initialOffset.hashCode()
result = 31 * result + decayAnimationSpec.hashCode()
return result
}
companion object {
val Saver = Saver<CircularListStateImpl, List<Any>>(
save = { listOf(it.verticalOffset) },
restore = {
CircularListStateImpl(it[0] as Float)
}
)
}
}
#Composable
fun rememberCircularListState(): CircularListState {
val state = rememberSaveable(saver = CircularListStateImpl.Saver) {
CircularListStateImpl()
}
return state
}
#Composable
fun CircularList(
visibleItems: Int,
modifier: Modifier = Modifier,
state: CircularListState = rememberCircularListState(),
circularFraction: Float = 1f,
overshootItems: Int = 3,
content: #Composable () -> Unit,
) {
check(visibleItems > 0) { "Visible items must be positive" }
check(circularFraction > 0f) { "Circular fraction must be positive" }
Layout(
modifier = modifier.clipToBounds().drag(state),
content = content,
) { measurables, constraints ->
val itemHeight = constraints.maxHeight / visibleItems
val itemConstraints = Constraints.fixed(width = constraints.maxWidth, height = itemHeight)
val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
state.setup(
CircularListConfig(
contentHeight = constraints.maxHeight.toFloat(),
numItems = placeables.size,
visibleItems = visibleItems,
circularFraction = circularFraction,
overshootItems = overshootItems,
)
)
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
for (i in state.firstVisibleItem..state.lastVisibleItem) {
placeables[i].placeRelative(state.offsetFor(i))
}
}
}
}
private fun Modifier.drag(
state: CircularListState,
) = pointerInput(Unit) {
val decay = splineBasedDecay<Float>(this)
coroutineScope {
while (true) {
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
state.stop()
val tracker = VelocityTracker()
awaitPointerEventScope {
verticalDrag(pointerId) { change ->
val verticalDragOffset = state.verticalOffset + change.positionChange().y
launch {
state.snapTo(verticalDragOffset)
}
tracker.addPosition(change.uptimeMillis, change.position)
change.consumePositionChange()
}
}
val velocity = tracker.calculateVelocity().y
val targetValue = decay.calculateTargetValue(state.verticalOffset, velocity)
launch {
state.decayTo(velocity, targetValue)
}
}
}
}
Android graphview load widget using androidplot library in build gradle
implementation "com.androidplot:androidplot-core:1.5.7"
Dynamicaally create chart using below code getresponse based from network call.
In class file my sample code...
val plot = XYPlot(context, "")
val h = context.resources.getDimension(R.dimen.sample_widget_height).toInt()
val w = context.resources.getDimension(R.dimen.sample_widget_width).toInt()
plot.graph.setMargins(100f, 0f, 0f, 16f)
plot.graph.setPadding(0f, 0f, 0f, 0f)
plot.graph.gridInsets.left = 100f
plot.graph.gridInsets.top = 0f
plot.graph.gridInsets.right = 0f
plot.graph.gridInsets.bottom = 40f
plot.legend.textPaint.textSize = 1f
plot.linesPerRangeLabel = 8
plot.linesPerDomainLabel = 8
plot.graph.setSize(Size.FILL)
plot.measure(w, h);
plot.layout(0, 0, w, h);
plot.graph.position(0f, HorizontalPositioning.ABSOLUTE_FROM_LEFT, 0f,
VerticalPositioning.ABSOLUTE_FROM_TOP, Anchor.LEFT_TOP)
val series1Numbers = mutableListOf<Int>()
val series2Numbers = mutableListOf<Int>()
val xLabels = mutableListOf<String>()
for (i in 0 until model.Details.Items.size) {
var item = model.Details.Items[i]
series1Numbers.add(item.TotalScore)
series2Numbers.add(0)
xLabels.add(item.Date)
}
val series1 = SimpleXYSeries(series1Numbers, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY,
"Series1")
val series2 = SimpleXYSeries(series2Numbers, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY,
"Series1")
val series1Format = LineAndPointFormatter(context, R.xml.line_point_formatter_with_labels)
val series2Format = LineAndPointFormatter(context,
R.xml.line_point_formatter_with_labels_2)
plot.graph.setLineLabelEdges(XYGraphWidget.Edge.LEFT, XYGraphWidget.Edge.BOTTOM)
plot.setRangeBoundaries(-110, 110, BoundaryMode.FIXED)
plot.graph.getLineLabelStyle(XYGraphWidget.Edge.LEFT).format = object : Format() {
override fun format(obj: Any, toAppendTo:
StringBuffer, pos: FieldPosition): StringBuffer {
val i = Math.round((obj as Number).toFloat())
L.m("widget y axos label value ", i.toString())
plot.graph.setLineLabelRenderer(XYGraphWidget.Edge.LEFT, MyLineLabelRenderer())
return if (i > 50) {
return toAppendTo.append(context.getString(R.string.str_excellent))
} else if (i > 25 && i <= 50) {
return toAppendTo.append(context.getString(R.string.str_very_good))
} else if (i > 0 && i <= 25) {
return toAppendTo.append(context.getString(R.string.str_good))
} else if (i == 0) {
return toAppendTo.append(context.getString(R.string.str_neutral))
} else if (i < 0 && i >= -25) {
return toAppendTo.append(context.getString(R.string.str_not_good))
} else if (i < -25 && i >= -50) {
return toAppendTo.append(context.getString(R.string.str_be_aware))
} else if (i < -50) {
return toAppendTo.append(context.getString(R.string.str_time_out))
} else {
return toAppendTo.append(context.getString(R.string.str_neutral))
}
}
override fun parseObject(source: String, pos: ParsePosition): Any {
// unused
return ""
}
}
plot.legend.setVisible(false)
plot.getGraph().linesPerDomainLabel = 5
plot.getGraph().linesPerRangeLabel = 5
plot.graph.getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).format = object : Format() {
override fun format(obj: Any, #NonNull toAppendTo:
StringBuffer, #NonNull pos: FieldPosition): StringBuffer {
val i = Math.round((obj as Number).toFloat())
val displayFormat = SimpleDateFormat(prefs.selectedTimeFormat, Locale.US)
val originalFormat = SimpleDateFormat(Constants.DATE_yyyyMMddHHmmss, Locale.US)
var displayTime = ""
if (prefs.selectedTimeFormat.equals(Constants.TWENTYFOUR_HOUR_FORMAT))
displayTime = displayFormat.format(originalFormat.parse(xLabels.get(i)))
else {
displayTime = displayFormat.format(originalFormat.parse(xLabels.get(i)))
displayTime.replace("AM", " am")
displayTime.replace("PM", " pm")
}
plot.graph.setLineLabelRenderer(XYGraphWidget.Edge.BOTTOM, MyLineLabelRenderer())
return toAppendTo.append(displayTime)
}
override fun parseObject(source: String, #NonNull pos: ParsePosition): Any {
return ""
}
}
plot.addSeries(series1, series1Format)
plot.addSeries(series2, series2Format)
val bitmap: Bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
plot.draw(Canvas(bitmap))
remoteViews!!.setImageViewBitmap(R.id.img_graph_view, bitmap)
appWidgetManager!!.updateAppWidget(appWidgetId, remoteViews)
I am tried to reduce bottom label shown text size is not working it's overlapping one another
How to reduce bottom label text size?
setTextsize used below code
plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT).getPaint().textSize = 16f
I'm trying to make custom image view that have rounded corners and a custom transition to change a border radius smoothly.
In CircleTransition, I try to get imageCornerRadius but it's always return 0 which ruined the transaction. But in activity, when I get imageCornerRadius, it returns the value in xml file. So how i can get the imageCornerRadius to perform the transition.
This is declare of my custom view
RoundedImageView
custom attribute
<declare-styleable name="RoundedImageView">
<attr name="imageCornerRadius" format="dimension" />
</declare-styleable>
class RoundedImageView : AppCompatImageView {
constructor(context: Context) : super(context) {
Log.d("debug", "first constructor")
}
constructor(context: Context, attrSet: AttributeSet) : super(context, attrSet) {
Log.d("debug", "second constructor")
init(attrSet)
}
constructor(context: Context, attrSet: AttributeSet, defStyleAttr: Int) : super(
context,
attrSet,
defStyleAttr
) {
Log.d("debug", "third constructor")
init(attrSet)
}
private fun init(attrSet: AttributeSet){
context.theme.obtainStyledAttributes(
attrSet,
R.styleable.RoundedImageView,
0,
0
).apply {
try {
imageCornerRadius = getDimensionPixelSize(
R.styleable.RoundedImageView_imageCornerRadius,
0
).toFloat()
} finally {
recycle()
}
}
}
// Custom attr
var imageCornerRadius: Float = 0F
//Attr for drawing
private lateinit var bitmapRect: RectF
val rect = RectF(drawable.bounds)
val holePath = Path()
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
Log.d("size changed", "w = $w h = $h")
bitmapRect = RectF(0f, 0f, w.toFloat(), h.toFloat())
}
override fun onDraw(canvas: Canvas?) {
val drawableWidth = this.width
val drawableHeight = this.height
/* Clip */
holePath.apply {
reset()
addRoundRect(
0F,
0F,
rect.right + drawableWidth,
rect.bottom + drawableHeight,
imageCornerRadius,
imageCornerRadius,
Path.Direction.CW
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas?.clipPath(holePath)
} else {
#Suppress("DEPRECATION")
canvas?.clipPath(holePath, Region.Op.REPLACE)
}
// Draw image
super.onDraw(canvas)
}
}
My custom transition change Size, Coordinate, imageCornerRadius
CircleTransition.kt
class CircleTransition() : Transition() {
private val TAG = CircleTransition::class.java.simpleName
private val BOUNDS = TAG + ":viewBounds"
private val CORNER_RADIUS = TAG + ":imageCornerRadius"
private val PROPS = arrayOf(BOUNDS, CORNER_RADIUS)
init {
Log.d("debug", "Circle Transition called")
}
override fun captureStartValues(transitionValues: TransitionValues?) {
captureValues(transitionValues)
}
override fun captureEndValues(transitionValues: TransitionValues?) {
captureValues(transitionValues)
}
fun captureValues(transitionValues: TransitionValues?) {
val view = transitionValues?.view
//get View Bound
val bound = RectF()
bound.left = view?.left?.toFloat() ?: return
bound.top = view.top.toFloat()
bound.right = view.right.toFloat()
bound.bottom = view.bottom.toFloat()
transitionValues.values.put(BOUNDS, bound)
//get view Corner radius
if(view is RoundedImageView){
val cornerRadius = view.imageCornerRadius
transitionValues.values.put(CORNER_RADIUS, cornerRadius)
}
}
override fun getTransitionProperties(): Array<String> {
return PROPS
}
override fun createAnimator(
sceneRoot: ViewGroup?,
startValues: TransitionValues?,
endValues: TransitionValues?
): Animator? {
if (startValues == null || endValues == null) {
return null
}
val view = endValues.view as RoundedImageView
//startScene
val sBound = startValues.values[BOUNDS] as RectF? ?: return null
//How I get imageCornerRadius
val sCornerRadius = startValues.values[CORNER_RADIUS] as Float? ?: return null
val sWidth = sBound.right - sBound.left
val sHeight = sBound.top - sBound.bottom
//endScene
val eBound = endValues.values[BOUNDS] as RectF? ?: return null
//How I get imageCornerRadius
val eCornerRadius = endValues.values[CORNER_RADIUS] as Float? ?: return null
val eWidth = eBound.right - eBound.left
val eHeight = eBound.top - eBound.bottom
if (sBound == eBound && sCornerRadius == eCornerRadius) {
return null
}
val widthAnimator: ValueAnimator =
ValueAnimator.ofInt(sWidth.toInt(), eWidth.toInt()).apply {
addUpdateListener {
val layoutParams = view.layoutParams
layoutParams.width = it.animatedValue as Int
view.layoutParams = layoutParams
}
}
val heightAnimator: ValueAnimator =
ValueAnimator.ofInt(sHeight.toInt() * -1, eHeight.toInt() * -1).apply {
interpolator = AccelerateInterpolator()
addUpdateListener {
val layoutParams = view.layoutParams
layoutParams.height = it.animatedValue as Int
view.layoutParams = layoutParams
}
}
val cornerRadiusAnimator = ValueAnimator.ofFloat(96F, 0F).apply {
addUpdateListener {
view.imageCornerRadius = it.animatedValue as Float
}
}
// set endView have the same size, coorinate like startScene
view.x = sBound.left
view.y = sBound.top
// view.layoutParams = ViewGroup.LayoutParams(sBound.width().toInt(), sBound.height().toInt())
// move view
val startX = sBound.left
val startY = sBound.top
val moveXTo = eBound.left
val moveYTo = eBound.top
val moveXAnimator: Animator =
ObjectAnimator.ofFloat(view, "x", startX, moveXTo.toFloat())
val moveYAnimator: Animator =
ObjectAnimator.ofFloat(view, "y", startY, moveYTo.toFloat()).apply {
addUpdateListener {
view.invalidate()
}
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(
widthAnimator,
heightAnimator,
cornerRadiusAnimator,
moveXAnimator,
moveYAnimator
)
return animatorSet
}
}