Android Native - Animate Alternating Yin Yang Symbol - Part 3

dimitrilc 1 Tallied Votes 78 Views Share

Animation Strategy

Welcome to part three of the tutorial; part two can be found here.

For this animation, I have chosen to redraw the YinYang Drawable at each frame. This means recreating the YinYang object with different constructor parameters on every frame (the current YinYang implementation has a zero-arg constructor).

Animating The First Yin Arc

Do you recall that our Yin teardrop contains three arcs in total? The first step to animate our Yin Yang is to isolate the parts that need to be animated. You can go ahead and comment out the code that draws the second and third Yin arcs, as well as the code that draws the Dots.

Your drawYin() method should look like the code below.

private fun drawYin(canvas: Canvas){
   val yinPath = Path().apply {
       addArc(
           bounds.right * 0.25f, // 0.25 = full arc visible. 0.5 = arc invisible
           bounds.exactCenterY(),
           bounds.right * 0.75f, // 0.75 = full arc visible. 0.5 = arc invisible
           bounds.bottom.toFloat(),
           270f,
           -180f
       )

/*            arcTo(
           0f,
           0f,
           bounds.right.toFloat(),
           bounds.bottom.toFloat(),
           90f,
           -180f,
           false
       )

       arcTo(
           bounds.right * 0.25f,
           0f,
           bounds.right * 0.75f,
           bounds.exactCenterY(),
           270f,
           180f,
           false
       )*/
   }

   canvas.drawPath(yinPath, yinPaint)
}

Your app now should look like the screenshot below, with the first Yin arc the only thing being drawn.

Screenshot_1666231309.png

To animate the first arc to invert on itself, we need to perform three main tasks:

  1. As the animation progresses, compress the arc over time until its width reaches zero. You can compress the arc by moving its top left and top right coordinates together.
  2. When its width is at zero, invert the sweep angle as well.
  3. Animates its width back to the original width.

Let us try some simple hard-coded samples first before using it in an animation. Comment out the code for your first arc and try the one below.

val isInverted = false // Keeps track of whether the drawable is inverted
val animatedFraction = 0.25f // animation progress
val animatedValue = 0.10f

addArc(
   bounds.right * (0.25f + animatedValue), // 0.25 = full arc visible. 0.5 = arc invisible
   bounds.exactCenterY(),
   bounds.right * (0.75f - animatedValue), // 0.75 = full arc visible. 0.5 = arc invisible
   bounds.bottom.toFloat(),
   270f,
   if (animatedFraction < 0.5f)
       if (isInverted) 180f
       else -180f
   else
       if (isInverted) -180f
       else 180f
)

The code above produces the following image.

Screenshot_1666232310.png

I highly recommend you to play around with isInverted, animatedFraction, and animatedValue to get a feel of the code.

Below are some sample runs.

val isInverted = true // Keeps track of whether the drawable is inverted
val animatedFraction = 0.25f // animation progress
val animatedValue = 0.10f

Screenshot_1666232473.png

val isInverted = false // Keeps track of whether the drawable is inverted
val animatedFraction = 0.85f // animation progress
val animatedValue = 0.20f

Screenshot_1666232536.png

Once you are comfortable with the code, move the three variables into the class constructor.

class YinYang(
   private val animatedValue: Float,
   private val isInverted: Boolean,
   private val animatedFraction: Float = 0f
): Drawable() {

Also, remove the commented out code for the first arc to keep your code clean. We do not need it anymore.

In onCreate(), use the code below to create an animator.

val yinYang = findViewById<ImageView>(R.id.yin_yang).apply {
   setImageDrawable(YinYang(0f, isInverted))
}

yinYang.setOnClickListener {
   ValueAnimator.ofFloat(0f, 0.25f, 0f).apply {
       duration = 2_000L
       addUpdateListener { animator ->
           yinYang.setImageDrawable(
               YinYang(
                   animator.animatedValue as Float,
                   isInverted,
                   animatedFraction
               )
           )
       }
       doOnEnd {
           isInverted = !isInverted
       }
       start()
   }
}

In your MainActivity, add the isInverted property.

private var isInverted = false

Our arc animation depends on the fractions from 0 -> 0.25 for the compress animation, and then from 0 -> 0.25 for the inflate animation. So we will give it that value as the animation fraction goes from 0 -> 1. In summary, this means;

  1. At animation fraction zero, we pass zero as the animated value as well.
  2. At animation fraction 0.25, we pass in 0.125 as the animated value. This is when arc compression is happening.
  3. At animation fraction 0.5, we pass in 0.25 as the animated value. Arc finishes compressing at this point.
  4. At animation fraction 0.75, we pass in 0.125 as the animated value. This is when arc inflation is happening.
  5. At animation fraction 1, we pass in 0.25 as the animated value. Arc finishes inflating.

When we run the code, the app should behave similarly to the animation below.

animated_1st_yin_arc.gif

Animate the Yin

The second arc can be kept as-is (it does not move anyway), so you can remove it from the commented out section. After the second arc is enabled, your animation should behave similarly to the code below.

animated_2nd_yin_arc.gif

The third arc needs to be changed, so you can remove the old commented out version and add the new version below. It is very similar to the first arc, only the sweep angles are different.

arcTo(
   bounds.right * (0.25f + animatedValue),
   0f,
   bounds.right * (0.75f - animatedValue),
   bounds.exactCenterY(),
   270f,
   if (animatedFraction < 0.5f)
       if (isInverted) -180f
       else 180f
   else
       if (isInverted) 180f
       else -180f,
   false
)

After adding the third arc, we complete the Yin

animated_yin.gif

Animate The Yang Dot

The current version of the Yang Dot is no longer good for our animation, so you can remove it from your code.

To animate the Yang Dot circularly moving up and down with the Yin Yang animation, we need to do three main things:

  1. Draw an arc with the starting position as the current position of the specific Dot. The arc must end with the ending position of the Dot for the current animation. The arc acts as a guide/ruler for the Dot.
  2. Find the position on the arc at a specific animation fraction.
  3. Use that position to draw the Yang Dot.

In the drawYangDot() function, add the code below into it to draw an arc. Note that this arc is only visible now for easy visualization. It should not be drawn on the screen at the final step.

val yangDotPath = Path().apply {
    addArc(
        bounds.right * 0.25f,
        bounds.bottom * 0.25f,
        bounds.right * 0.75f,
        bounds.bottom * 0.75f,
        if (isInverted) 270f
        else 90f,
        if (isInverted) 180f
        else -180f
    )
}

//Use for debugging
canvas.drawPath(
    yangDotPath,
    Paint().apply {
        color = Color.RED
        style = Paint.Style.STROKE
        strokeWidth = 10f
    }
)

When we run the app, we can see it in red below.

Screenshot_1666234596.png

Also, inside drawYangDot(), append the code below to draw the Yang Dot.

// Finds current position on arc at animation frame
val yangDotPosition = FloatArray(2)
PathMeasure(yangDotPath, false).apply {
   getPosTan(length * animatedFraction, yangDotPosition, null)
}

canvas.drawCircle(
   yangDotPosition[0],
   yangDotPosition[1],
   bounds.right * 0.07f,
   yangPaint
)

To find the position of a Path, you can use PathMeasure like above.

You can now review the animation and then remove the call to draw the arc afterwards.

animated_yang_dot.gif

Animate The Yin Dot

For the Yin Dot, follow the same strategy used for the Yang Dot.

private fun drawYinDot(canvas: Canvas){
   val yinDotPath = Path().apply {
       addArc(
           bounds.right * 0.25f,
           bounds.bottom * 0.25f,
           bounds.right * 0.75f,
           bounds.bottom * 0.75f,
           if (isInverted) 90f
           else 270f,
           if (isInverted) 180f
           else -180f
       )
   }

   //Use for debugging
   canvas.drawPath(
       yinDotPath,
       Paint().apply {
           color = Color.BLUE
           style = Paint.Style.STROKE
           strokeWidth = 10f
       }
   )

   val yinDotPosition = FloatArray(2)

   PathMeasure(yinDotPath, false).apply {
       getPosTan(length * animatedFraction, yinDotPosition, null)
   }

   canvas.drawCircle(
       yinDotPosition[0],
       yinDotPosition[1],
       bounds.right * 0.07f,
       yinPaint
   )
}

animated_yin_yang_dot_debug.gif

Remove the debug call to see the almost final result.

animated_yin_yang_no_color.gif

Animate Alternating Color

To animate from one color to another based on animation fraction, we can use the method blendARGB() from the class ColorUtils. Replace your yinPaint and yangPaint with the new versions below if you want your halves to alternate in color as well.

private val yinPaint = Paint().apply {
   color =  if (isInverted) {
       ColorUtils.blendARGB(
           Color.WHITE,
           Color.BLACK,
           animatedFraction
       )
   } else {
       ColorUtils.blendARGB(
           Color.BLACK,
           Color.WHITE,
           animatedFraction
       )
   }
}

private val yangPaint = Paint().apply {
   color =  if (isInverted) {
       ColorUtils.blendARGB(
           Color.BLACK,
           Color.WHITE,
           animatedFraction
       )
   } else {
       ColorUtils.blendARGB(
           Color.WHITE,
           Color.BLACK,
           animatedFraction
       )
   }
}

animated_yin_yang_black_white.gif

Summary

Congratulations, we have learned how to animate an alternating Yin Yang symbol in this series of tutorials. The full project code can be found at https://github.com/dmitrilc/DaniwebAnimateAlternatingYinYangSymbol.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.