Elliptical list in Compose - android

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)
}
}
}
}

Related

make an animation on canvas using specific coordinates sent from the mainactivity

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)
}
`

How to draw "line from circles" between two points in on Draw and make it zoomable and scrollable?

I have scalable image view - it can be zoomed or scrolled. In this image I able to set points with pin. These points are zoomable and scrollable too with the image. But also I need to draw several circles - the path between pin A and pin B. This path should be zoomable and scrollable too. The color of each circle can be various too. Example:
I creating pins here:
override fun onLongPress(e: MotionEvent) {
super.onLongPress(e)
pinsCoordinates.add(CoordinatesEntity(e.x, e.y))
val touchCoords = floatArrayOf(e.x, e.y)
val matrixInverse = Matrix()
mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.
val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
invertedPinsCoordinates.add(entity)
pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()
RxBus.publish(IndoorPinsCountChanged(pinsCoordinates.size))
invalidate()
}
And drawing here:
private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
val paint = Paint()
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#1417BF")
val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
val copiedBitmapPin = pinBitmap.copy(pinBitmap.config, pinBitmap.isMutable)
val canvas = Canvas(copiedBitmapPin)
val xStart = when (text.length) {
1 -> copiedBitmapPin.width / 2f - 8
2 -> copiedBitmapPin.width / 2f - 12
else -> copiedBitmapPin.width / 2f - 36
}
val yStart = when (text.length) {
1 -> copiedBitmapPin.height / 2f - 2
else -> copiedBitmapPin.height / 2f - 5
}
paint.textSize = when (text.length) {
1 -> 30f
else -> 20f
}
canvas.drawText(text, xStart, yStart, paint)
return BitmapDrawable(context.resources, copiedBitmapPin).bitmap
}
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
My whole class:
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.widget.Toast
import androidx.appcompat.widget.AppCompatImageView
import com.signalsense.signalsenseapp.R
import com.signalsense.signalsenseapp.mvp.models.entities.CoordinatesEntity
import com.signalsense.signalsenseapp.rxbus.RxBus
import com.signalsense.signalsenseapp.rxbus.events.IndoorPinsCountChanged
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
class ScaleImageView : AppCompatImageView, View.OnTouchListener {
private val dirtyRect = RectF()
private val savedContext: Context
private val maxScale = 2f
private val matrixValues = FloatArray(9)
private var lastTouchX = 0f
private var lastTouchY = 0f
private var paint = Paint()
var tag = "ScaleImageView"
// display width height.
private var mWidth = 0
private var mHeight = 0
private var mIntrinsicWidth = 0
private var mIntrinsicHeight = 0
private var mScale = 0f
private var mMinScale = 1f
private var mPrevDistance = 0f
private var isScaling = false
private var mPrevMoveX = 0
private var mPrevMoveY = 0
private var mDetector: GestureDetector? = null
private val pinsCoordinates = ArrayList<CoordinatesEntity>()
private var invertedPinsCoordinates = ArrayList<CoordinatesEntity>()
private val pinCountersMap = HashMap<String, String>()
private var pinCounter = 0
private lateinit var pinBitmap: Bitmap
constructor(context: Context, attr: AttributeSet?) : super(context, attr) {
savedContext = context
initialize()
}
constructor(context: Context) : super(context) {
savedContext = context
initialize()
}
private fun resetDirtyRect(eventX: Float, eventY: Float) {
dirtyRect.left = min(lastTouchX, eventX)
dirtyRect.right = max(lastTouchX, eventX)
dirtyRect.top = min(lastTouchY, eventY)
dirtyRect.bottom = max(lastTouchY, eventY)
}
override fun setImageBitmap(bm: Bitmap) {
super.setImageBitmap(bm)
initialize()
}
override fun setImageResource(resId: Int) {
super.setImageResource(resId)
initialize()
}
private fun initialize() {
this.scaleType = ScaleType.MATRIX
mMatrix = Matrix()
val d = drawable
paint.isAntiAlias = true
paint.color = Color.RED
paint.style = Paint.Style.STROKE
paint.strokeJoin = Paint.Join.ROUND
paint.strokeWidth = STROKE_WIDTH
if (d != null) {
mIntrinsicWidth = d.intrinsicWidth
mIntrinsicHeight = d.intrinsicHeight
setOnTouchListener(this)
}
mDetector = GestureDetector(savedContext,
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
maxZoomTo(e.x.toInt(), e.y.toInt())
cutting()
return super.onDoubleTap(e)
}
override fun onLongPress(e: MotionEvent) {
super.onLongPress(e)
pinsCoordinates.add(CoordinatesEntity(e.x, e.y))
val touchCoords = floatArrayOf(e.x, e.y)
val matrixInverse = Matrix()
mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.
val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
invertedPinsCoordinates.add(entity)
pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()
RxBus.publish(IndoorPinsCountChanged(pinsCoordinates.size))
invalidate()
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
val delta = 25
val pinIndex = pinsCoordinates.indexOfFirst {
val itXStart = it.x - delta
val itXEnd = it.x + delta
val itYStart = it.y - delta
val itYEnd = it.y + delta
val tapX = e?.x ?: 0f
val tapY = e?.y ?: 0f
(tapX in itXStart..itXEnd) && (tapY in itYStart..itYEnd)
}
if (pinIndex >= 0) Toast.makeText(context, pinIndex.toString(), Toast.LENGTH_SHORT).show()
return super.onSingleTapConfirmed(e)
}
})
pinBitmap = BitmapFactory.decodeResource(
context.resources, R.drawable.ic_pin_blue_no_middle_32
).copy(Bitmap.Config.ARGB_8888, true)
}
override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
mWidth = r - l
mHeight = b - t
mMatrix!!.reset()
val rNorm = r - l
mScale = rNorm.toFloat() / mIntrinsicWidth.toFloat()
var paddingHeight = 0
var paddingWidth = 0
// scaling vertical
if (mScale * mIntrinsicHeight > mHeight) {
mScale = mHeight.toFloat() / mIntrinsicHeight.toFloat()
mMatrix!!.postScale(mScale, mScale)
paddingWidth = (r - mWidth) / 2
paddingHeight = 0
// scaling horizontal
} else {
mMatrix!!.postScale(mScale, mScale)
paddingHeight = (b - mHeight) / 2
paddingWidth = 0
}
mMatrix!!.postTranslate(paddingWidth.toFloat(), paddingHeight.toFloat())
imageMatrix = mMatrix
mMinScale = mScale
Log.i(tag, "MinScale: $mMinScale")
zoomTo(mScale, mWidth / 2, mHeight / 2)
cutting()
return super.setFrame(l, t, r, b)
}
fun deleteLastPin() {
if (pinsCoordinates.isNotEmpty()) pinsCoordinates.removeLast()
if (invertedPinsCoordinates.isNotEmpty()) invertedPinsCoordinates.removeLast()
RxBus.publish(IndoorPinsCountChanged(pinsCoordinates.size))
pinCounter--
invalidate()
}
private fun getValue(matrix: Matrix?, whichValue: Int): Float {
matrix!!.getValues(matrixValues)
return matrixValues[whichValue]
}
private val scale: Float
get() = getValue(mMatrix, Matrix.MSCALE_X)
private val translateX: Float
get() = getValue(mMatrix, Matrix.MTRANS_X)
private val translateY: Float
get() = getValue(mMatrix, Matrix.MTRANS_Y)
private fun maxZoomTo(x: Int, y: Int) {
if (mMinScale != getCalculatedScale() && getCalculatedScale() - mMinScale > 0.1f) {
// threshold 0.1f
val scale = mMinScale / getCalculatedScale()
zoomTo(scale, x, y)
} else {
val scale = maxScale / getCalculatedScale()
zoomTo(scale, x, y)
}
}
private fun zoomTo(scale: Float, x: Int, y: Int) {
if (getCalculatedScale() * scale < mMinScale) {
return
}
if (scale >= 1 && getCalculatedScale() * scale > maxScale) {
return
}
Log.i(tag, "Scale: $scale, multiplied: ${scale * scale}")
mMatrix!!.postScale(scale, scale)
// move to center
mMatrix!!.postTranslate(-(mWidth * scale - mWidth) / 2,
-(mHeight * scale - mHeight) / 2)
// move x and y distance
mMatrix!!.postTranslate(-(x - mWidth / 2) * scale, 0f)
mMatrix!!.postTranslate(0f, -(y - mHeight / 2) * scale)
imageMatrix = mMatrix
}
private fun cutting() {
val width = (mIntrinsicWidth * getCalculatedScale()).toInt()
val height = (mIntrinsicHeight * getCalculatedScale()).toInt()
imageWidth = width
imageHeight = height
if (translateX < -(width - mWidth)) {
mMatrix!!.postTranslate(-(translateX + width - mWidth), 0f)
}
if (translateX > 0) {
mMatrix!!.postTranslate(-translateX, 0f)
}
if (translateY < -(height - mHeight)) {
mMatrix!!.postTranslate(0f, -(translateY + height - mHeight))
}
if (translateY > 0) {
mMatrix!!.postTranslate(0f, -translateY)
}
if (width < mWidth) {
mMatrix!!.postTranslate(((mWidth - width) / 2).toFloat(), 0f)
}
if (height < mHeight) {
mMatrix!!.postTranslate(0f, ((mHeight - height) / 2).toFloat())
}
imageMatrix = mMatrix
}
private fun distance(x0: Float, x1: Float, y0: Float, y1: Float): Float {
val x = x0 - x1
val y = y0 - y1
return sqrt((x * x + y * y).toDouble()).toFloat()
}
private fun dispDistance(): Float {
return sqrt((mWidth * mWidth + mHeight * mHeight).toDouble()).toFloat()
}
fun clear() {
path.reset()
invalidate()
}
fun save() {
val returnedBitmap = Bitmap.createBitmap(
width,
height,
Bitmap.Config.ARGB_8888)
val canvas = Canvas(returnedBitmap)
draw(canvas)
setImageBitmap(returnedBitmap)
}
#Suppress("DEPRECATION")
#SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (mDetector!!.onTouchEvent(event)) {
return true
}
val touchCount = event.pointerCount
when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_1_DOWN, MotionEvent.ACTION_POINTER_2_DOWN -> {
if (touchCount >= 2) {
val distance = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
mPrevDistance = distance
isScaling = true
} else {
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
}
if (touchCount >= 2 && isScaling) {
val dist = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
var scale = (dist - mPrevDistance) / dispDistance()
mPrevDistance = dist
scale += 1f
scale *= scale
zoomTo(scale, mWidth / 2, mHeight / 2)
cutting()
} else if (!isScaling) {
val distanceX = mPrevMoveX - event.x.toInt()
val distanceY = mPrevMoveY - event.y.toInt()
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
cutting()
}
}
MotionEvent.ACTION_MOVE -> if (touchCount >= 2 && isScaling) {
val dist = distance(event.getX(0), event.getX(1),
event.getY(0), event.getY(1))
var scale = (dist - mPrevDistance) / dispDistance()
mPrevDistance = dist
scale += 1f
scale *= scale
zoomTo(scale, mWidth / 2, mHeight / 2)
cutting()
} else if (!isScaling) {
val distanceX = mPrevMoveX - event.x.toInt()
val distanceY = mPrevMoveY - event.y.toInt()
mPrevMoveX = event.x.toInt()
mPrevMoveY = event.y.toInt()
mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
cutting()
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_POINTER_2_UP -> if (event.pointerCount <= 1) {
isScaling = false
}
}
return true
}
private fun getCalculatedScale() = getValue(mMatrix, Matrix.MSCALE_X)
private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
val paint = Paint()
paint.style = Paint.Style.FILL
paint.color = Color.parseColor("#1417BF")
val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
val copiedBitmapPin = pinBitmap.copy(pinBitmap.config, pinBitmap.isMutable)
val canvas = Canvas(copiedBitmapPin)
val xStart = when (text.length) {
1 -> copiedBitmapPin.width / 2f - 8
2 -> copiedBitmapPin.width / 2f - 12
else -> copiedBitmapPin.width / 2f - 36
}
val yStart = when (text.length) {
1 -> copiedBitmapPin.height / 2f - 2
else -> copiedBitmapPin.height / 2f - 5
}
paint.textSize = when (text.length) {
1 -> 30f
else -> 20f
}
canvas.drawText(text, xStart, yStart, paint)
return BitmapDrawable(context.resources, copiedBitmapPin).bitmap
}
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
return super.onTouchEvent(event)
}
companion object {
const val STROKE_WIDTH = 10f
const val HALF_STROKE_WIDTH = STROKE_WIDTH / 2
var path = Path()
var imageHeight = 0
var imageWidth = 0
private var mMatrix: Matrix? = null
}
}
How to create this path from circles? And make it scrollable and zoomable with other elements (picture and pins, that alreasy scrollable and zoomable)? Please help!
If anybody interested, I found the solution. This is the algorithm to get middle points between poina a and point b:
private fun getPointsToDraw(a: CoordinatesEntity, b: CoordinatesEntity): List<CoordinatesEntity> {
val points = ArrayList<CoordinatesEntity>()
val numberOfPoints = 5
val stepX = (b.x - a.x) / numberOfPoints
val stepY = (b.y - a.y) / numberOfPoints
points.add(a)
for (i in 0 until numberOfPoints) {
val lastPoint = points.last()
points.add(CoordinatesEntity(lastPoint.x + stepX, lastPoint.y + stepY))
}
points.add(b)
return points
}
data class:
data class CoordinatesEntity(var x: Float, var y: Float)
Of course we can get only fixed points between, cause there is indefinite count.
Here is how to write them. I using prepared bitmaps in array (here is I took 4th index from this bitmap):
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
invertedPinsCoordinates.forEach {
val marker = getPinForCoordinates(it)
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
//Starting to draw path with points
if(invertedPinsCoordinates.size > 1) {
val points = getPointsToDraw(invertedPinsCoordinates[invertedPinsCoordinates.size - 2],
invertedPinsCoordinates.last())
points.forEach {
val marker = levelsBitmapArray[4]
val matrixMarker = Matrix()
matrixMarker.setTranslate(it.x, it.y)
matrixMarker.postConcat(mMatrix)
canvas.drawBitmap(marker, matrixMarker, null)
}
}
}

Move a dot over circular arc which can move over the arc of the circle for a certain time ,can be stoped

I have to make following Ui, In which when I play this small circle will move in an arc, when I stop it will stop. I can also set the timing of rotation of the small blue circle.
Till now I have implemented the following code: this gives me an arc of the circle, but I am not able to rotate the smaller circler over the bigger one.
public class CustoCustomProgressBar : View{
private var path : Path ? =null
private var mPrimaryPaint: Paint? = null
private var mSecondaryPaint: Paint? = null
private var mBackgroundPaint: Paint? = null
private var mTextPaint: TextPaint? = null
private var mProgressDrawable : Drawable ? = null
private var mRectF: RectF? = null
private var mDrawText = false
private var mTextColor = 0
private var mSecondaryProgressColor = 0
private var mPrimaryProgressColor = 0
private var mBackgroundColor = 0
private var mStrokeWidth = 0
private var mProgress = 0
var secodaryProgress = 0
private set
private var firstArcprogress = 0
private var secondArcProgress = 0
private var thirdArcProgress = 0
private var mFristArcCapSize = 0
private var mSecondArcCapSize = 0
private var mThirdArcCapSize = 0
private var isFristCapVisible = false
private var isSecondCapVisible = false
private var isThirdCapVisible = false
private var capColor = 0
private var mPrimaryCapSize = 0
private var mSecondaryCapSize = 0
var isPrimaryCapVisible = false
var isSecondaryCapVisible = false
private var x = 0
private var y = 0
private var mWidth = 0
private var mHeight = 0
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context, attrs)
}
fun init(context: Context, attrs: AttributeSet?) {
val a: TypedArray
a = if (attrs != null) {
context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.CustomProgressBar,
0, 0
)
} else {
throw IllegalArgumentException("Must have to pass the attributes")
}
try {
mProgressDrawable = resources.getDrawable(R.drawable.test)
mDrawText = a.getBoolean(R.styleable.CustomProgressBar_showProgressText, false)
mBackgroundColor =
a.getColor(
R.styleable.CustomProgressBar_backgroundColor,
resources.getColor(R.color.white)
)
mPrimaryProgressColor =
a.getColor(
R.styleable.CustomProgressBar_progressColor,
resources.getColor(R.color.white)
)
mSecondaryProgressColor =
a.getColor(
R.styleable.CustomProgressBar_secondaryProgressColor,
resources.getColor(R.color.black)
)
capColor =
a.getColor(
R.styleable.CustomProgressBar_capColor,
resources.getColor(R.color.color_9bc6e6_mind)
)
firstArcprogress =a.getInt(R.styleable.CustomProgressBar_firstArcProgress, 0)
mProgress = a.getInt(R.styleable.CustomProgressBar_progress, 0)
secodaryProgress = a.getInt(R.styleable.CustomProgressBar_secondaryProgress, 0)
mStrokeWidth = a.getDimensionPixelSize(R.styleable.CustomProgressBar_strokeWidth, 10)
mTextColor = a.getColor(
R.styleable.CustomProgressBar_textColor,
resources.getColor(R.color.black)
)
mPrimaryCapSize = a.getInt(R.styleable.CustomProgressBar_primaryCapSize, 20)
mSecondaryCapSize = a.getInt(R.styleable.CustomProgressBar_secodaryCapSize, 20)
isPrimaryCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_primaryCapVisibility, true)
isSecondaryCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_secodaryCapVisibility, true)
isFristCapVisible = a.getBoolean(R.styleable.CustomProgressBar_firstCapVisibility, true)
isSecondCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_secodaryCapVisibility, false)
isThirdCapVisible =
a.getBoolean(R.styleable.CustomProgressBar_thirdCapVisibility, false)
} finally {
a.recycle()
}
mBackgroundPaint = Paint()
mBackgroundPaint!!.setAntiAlias(true)
mBackgroundPaint!!.setStyle(Paint.Style.STROKE)
mBackgroundPaint!!.setStrokeWidth(mStrokeWidth.toFloat())
mBackgroundPaint!!.setColor(mBackgroundColor)
mPrimaryPaint = Paint()
mPrimaryPaint!!.setAntiAlias(true)
mPrimaryPaint!!.setStyle(Paint.Style.STROKE)
mPrimaryPaint!!.setStrokeWidth(mStrokeWidth.toFloat())
mPrimaryPaint!!.setColor(capColor)
mSecondaryPaint = Paint()
mSecondaryPaint!!.setAntiAlias(true)
mSecondaryPaint!!.setStyle(Paint.Style.STROKE)
mSecondaryPaint!!.setStrokeWidth((mStrokeWidth - 2).toFloat())
mSecondaryPaint!!.setColor(mSecondaryProgressColor)
mTextPaint = TextPaint()
mTextPaint!!.color = mTextColor
mRectF = RectF()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mRectF!![paddingLeft.toFloat(), paddingTop.toFloat(), (w - paddingRight).toFloat()] =
(h - paddingBottom).toFloat()
mTextPaint!!.textSize = (w / 5).toFloat()
x = w / 2 - (mTextPaint!!.measureText("$mProgress%") / 2).toInt()
y = (h / 2 - (mTextPaint!!.descent() + mTextPaint!!.ascent()) / 2).toInt()
mWidth = w
mHeight = h
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
mPrimaryPaint?.setStyle(Paint.Style.STROKE)
mSecondaryPaint?.setStyle(Paint.Style.STROKE)
// for drawing a full progress .. The background circle
mRectF?.let {
mBackgroundPaint?.let { it1 -> canvas.drawArc(it, 270F, 180F, false, it1) } }
path = Path()
path?.arcTo(mRectF,270F,180F,true)
mRectF?.let {
mBackgroundPaint?.let { it1 ->
canvas.drawArc(it, 95F, 80F, false, it1)
}
}
mRectF?.let {
mBackgroundPaint?.let { it1 ->
canvas.drawArc(it, 180F, 80F, false, it1)
}
}
// // for drawing a secondary progress circle
// val secondarySwipeangle = secodaryProgress * 360 / 100
// mRectF?.let { mSecondaryPaint?.let { it1 ->
// canvas.drawArc(it, 270F, secondarySwipeangle.toFloat(), false,
// it1
// )
// } }
//
val firstArcProgresSwipeAngle = firstArcprogress * 180 / 100
// mRectF?.let {
// mPrimaryPaint?.let { it1 ->
// canvas.drawArc(
// it, 270F, firstArcProgresSwipeAngle.toFloat(), false,
// it1
// )
// }
// }
// // for drawing a main progress circle
// val primarySwipeangle = mProgress * 360 / 100
// mRectF?.let { mPrimaryPaint?.let { it1 ->
// canvas.drawArc(it, 270F, primarySwipeangle.toFloat(), false,
// it1
// )
// } }
// // for cap of secondary progress
// val r = (height - paddingLeft * 2) / 2 // Calculated from canvas width
// var trad = (secondarySwipeangle - 90) * (Math.PI / 180.0) // = 5.1051
// var x = (r * Math.cos(trad)).toInt()
// var y = (r * Math.sin(trad)).toInt()
// mSecondaryPaint?.setStyle(Paint.Style.FILL)
// if (isSecondaryCapVisible) mSecondaryPaint?.let {
// canvas.drawCircle(
// (x + mWidth / 2).toFloat(),
// (y + mHeight / 2).toFloat(),
// mSecondaryCapSize.toFloat(),
// it
// )
// }
val r = (height - paddingLeft * 2) / 2
// for cap of primary progress
var trad = (firstArcProgresSwipeAngle - 90) * (Math.PI / 180.0) // = 5.1051
x = (r * Math.cos(trad)).toInt()
y = (r * Math.sin(trad)).toInt()
mPrimaryPaint?.setStyle(Paint.Style.FILL)
if (isPrimaryCapVisible) mPrimaryPaint?.let {
canvas.drawCircle(
(x + mWidth / 2).toFloat(),
(y + mHeight / 2).toFloat(),
mPrimaryCapSize.toFloat(),
it
)
}
if (mDrawText) mTextPaint?.let {
canvas.drawText(
"$mProgress%", x.toFloat(), y.toFloat(),
it
)
}
}
fun setDrawText(mDrawText: Boolean) {
this.mDrawText = mDrawText
invalidate()
}
override fun setBackgroundColor(mBackgroundColor: Int) {
this.mBackgroundColor = mBackgroundColor
mBackgroundPaint?.setColor(mBackgroundColor)
invalidate()
}
fun setStrokeWidth(mStrokeWidth: Int) {
this.mStrokeWidth = mStrokeWidth
invalidate()
}
fun setSecondaryProgress(mSecondaryProgress: Int) {
secodaryProgress = mSecondaryProgress
invalidate()
}
fun setTextColor(mTextColor: Int) {
this.mTextColor = mTextColor
mTextPaint!!.color = mTextColor
invalidate()
}
var secondaryProgressColor: Int
get() = mSecondaryProgressColor
set(mSecondaryProgressColor) {
this.mSecondaryProgressColor = mSecondaryProgressColor
mSecondaryPaint?.setColor(mSecondaryProgressColor)
invalidate()
}
var primaryProgressColor: Int
get() = mPrimaryProgressColor
set(mPrimaryProgressColor) {
this.mPrimaryProgressColor = mPrimaryProgressColor
mPrimaryPaint?.setColor(mPrimaryProgressColor)
invalidate()
}
var progress: Int
get() = mProgress
set(mProgress) {
while (this.mProgress <= mProgress) {
postInvalidateDelayed(150)
this.mProgress++
}
}
fun getBackgroundColor(): Int {
return mBackgroundColor
}
var primaryCapSize: Int
get() = mPrimaryCapSize
set(mPrimaryCapSize) {
this.mPrimaryCapSize = mPrimaryCapSize
invalidate()
}
var secondaryCapSize: Int
get() = mSecondaryCapSize
set(mSecondaryCapSize) {
this.mSecondaryCapSize = mSecondaryCapSize
invalidate()
}
var arcprogress: Int
get() = firstArcprogress
set(firstArcprogress) {
while (this.firstArcprogress <= firstArcprogress) {
postInvalidateDelayed(150)
this.firstArcprogress = firstArcprogress
}
}
fun getPath(): Path? {
return path
}
fun getXCOORDINTE(): Float{
return x.toFloat()
}
fun getYCoordinate(): Float{
return y.toFloat()
}
}

Cannot not get the right value of custom attribute in custom transition

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
}
}

Right way for use kotlin coroutine

I am using kotlin coroutinescope for loading thumnails from video uri and add imageview with that bitmaps in linear layout.
Currently after all thumnail loaded, I am adding into linearlayout. Can anyone suggest me for getting one by one bitmao and adding into linearlayout?
private fun loadThumbnails(uri: Uri) {
val metaDataSource = MediaMetadataRetriever()
metaDataSource.setDataSource(context, uri)
val videoLength = (metaDataSource.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION).toInt() * 1000).toLong()
val thumbnailCount = 8
val interval = videoLength / thumbnailCount
var listOfImage: ArrayList<Bitmap?> = ArrayList()
for (i in 0 until thumbnailCount - 1) {
try {
var bitmap: Bitmap? = null
val job = CoroutineScope(Dispatchers.IO).launch {
val frameTime = i * interval
bitmap = metaDataSource.getFrameAtTime(frameTime, MediaMetadataRetriever.OPTION_CLOSEST)
bitmap?.let {
val targetWidth: Int
val targetHeight: Int
if (it.height > it.width) {
targetHeight = frameDimension
val percentage = frameDimension.toFloat() / it.height
targetWidth = (it.width * percentage).toInt()
} else {
targetWidth = frameDimension
val percentage = frameDimension.toFloat() / it.width
targetHeight = (it.height * percentage).toInt()
}
bitmap = Bitmap.createScaledBitmap(it, targetWidth, targetHeight, false)
}
listOfImage.add(bitmap)
metaDataSource.release()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
listOfImage.forEach {
container_thumbnails.addView(ThumbnailView(context).apply { setImageBitmap(it) })
}
}
Please try next approach:
val job = CoroutineScope(Dispatchers.Main).launch {
val frameTime = i * interval
val bitmap = loadBitmap(frameTime) // loads bitmap asynchronously using withContext(Dispatchers.IO)
// ... use bitmap to set into a view
}
suspend fun loadBitmap(frameTime: Int): Bitmap? = withContext(Dispatchers.IO) {
bitmap = metaDataSource.getFrameAtTime(frameTime, MediaMetadataRetriever.OPTION_CLOSEST)
bitmap?.let {
val targetWidth: Int
val targetHeight: Int
if (it.height > it.width) {
targetHeight = frameDimension
val percentage = frameDimension.toFloat() / it.height
targetWidth = (it.width * percentage).toInt()
} else {
targetWidth = frameDimension
val percentage = frameDimension.toFloat() / it.width
targetHeight = (it.height * percentage).toInt()
}
Bitmap.createScaledBitmap(it, targetWidth, targetHeight, false)
}
}

Categories

Resources