Discussion on Replacing Plugin Active Install Growth Data Continues Behind Closed Doors

Earlier this month WordPress.org meta contributors removed the active install growth chart from plugins, sending plugin developers who relied on this data into a state of dismay and outrage. The commit cited “insufficient data obfuscation” but there was no clear communication about when and where this decision had been made. Developers demanded more transparency around the charts’ removal but received no clear answers.

Multiple opportunities to communicate the details behind the decision were deliberately forgone, as speculation mounted. Several contributors not directly involved in the conversations prematurely insisted it was removed due to a security or privacy concern, but Samuel Otto Wood has unequivocally confirmed that it was neither of these things.

In a recent appearance on the WPwatercooler podcast, Wood elaborated on the decision, which he says was made in May through private channels via Slack DMs in a discussion initiated by Matt Mullenweg.

“The reason is really quite simple,” Wood said. “It was removed because by and large, nobody was using them. Nobody was using the chart itself. By and large, the chart was not useful to the majority, and it didn’t really fit the purpose we had for it, that we had in mind when we implemented it.”

Wood said the active growth chart was intended to just show growth or decline of a plugin on a weekly basis, but the data wasn’t working as intended:

People wanted that feedback on whether plugin’s growing, whether it’s shrinking, et cetera. And that’s valuable information for developers to have, it’s valuable information users to know. But it really wasn’t working as that.

The data that it provided was a percentage based data, and it was a very weak percentage based data. So by and large, the majority of use of that data was people scraping the data and using it to work backwards to the exact quote, exact numbers

That was entirely the problem was that people were largely using it to get those numbers. Now, that’s not itself bad, but a, the reverse math didn’t work. It was wrong for a number of reasons, mainly because we were doing such a way obfuscating the data in such a way that it made that number wrong.

Second, Actually, it’s kind of funny. It actually always gave numbers a bit too high, so it was giving people the wrong impression. Third, it really, people trusted it as an active number, as a number of active cells to the point where, to the point where they, they relied on it to make decisions and things like that. It was not a good idea.

Although Otto was not involved in working on the project at the time, he was privy to the discussion and relayed some of the details:

I read through all that discussion and we worked, they worked on it for a long, Scott and several people tried various things before removing it. They adjusted the values, they adjusted numbers. They, they went through a ridiculous amount of iteration and in the end, none of it worked. People were still using it even though it was giving them basically garbage. So finally removing it was the only thing to do. We did have a plan for replacing it. We just didn’t have a plan for replacing it immediately. Nevertheless, giving them active install count numbers that are wrong is more harmful, we felt, to both users and developers interests than simply not giving them at all. So that’s why it was removed.

The concern podcast host Sé Reed and guest Matt Cromwell highlighted was that the decision was communicated in such a way that it suggested it was security related. Since it was not a sensitive security or privacy issue, Reed asked why was it handled in a private chat instead of the meta channel when the decision had such a profound impact on developers being able to track the trajectory of their plugins.

Since the inaccuracy of the charts was well-known to those more intimately acquainted with the problem, Wood said its removal was “not quite the big deal” that everyone else ended up perceiving it to be. They did not anticipate the firestorm the charts’ removal would create in the trac ticket where developers were pleading to have them restored.

“The physical visual chart itself is not so instrumental to the way I operate things,” GiveWP founder Matt Cromwell said. “But it’s the act of removing it without any conversation whatsoever.

“And what does that mean for the long run of data about plugins on.org and the viability of their, of us, continuing to have them? That’s the real question. It’s an indicator of an underlying problem that isn’t getting better.”

This incident has sparked discussions about what kind of partnership plugin developers should expect from WordPress.org, and whether it’s time they looked for support from one another instead of the platform, as Eric Karkovack suggested on The WP Minute. In light of plugin developers losing more valuable data that hasn’t been replaced, Alex Denning, managing director of Ellipsis, a digital marketing agency, makes the case that WordPress.org is ineffective for plugin distribution in 2022. He contends that new WordPress plugins are not passing the 100k, 500k , or 1m+ install thresholds and the directory isn’t giving plugins organic reach.

The focus of the ticket has changed from calling on WordPress.org to bring back the active growth charts to be more about brainstorming helpful plugin stats and insights that plugin developers would like to see. It is still receiving angry and frustrated comments from developers who believe the data should belong to the community.

“I cannot emphasize enough that conversations about what to replace the active growth chart with should be happening in a public Slack channel or on a Trac ticket,” Equalize Digital CEO Amber Hinds said. “This data should belong to the community and the community should be able to participate in deciding how (or not) to display it.”

The reasons that purportedly necessitate obfuscation have not been clearly explained, but many participants in the discussion have urged WordPress.org to simply publish the raw data so it can be accessed and processed independently of the platform. @Starbuck suggests the community would then be able to create sites that render the data in meaningful and interesting ways.

WordPress developers want far more data than was previously available. Hinds requested an assortment of data points that may or may not be possible:

Things that tell us if our readme and other ranking factors are on track:

  • Number of searches (or impressions) for target keywords
  • Average ranking for target keywords for timeframe (month)
  • Conversion rates from impression to install for target keywords

Things that tell us if we may have a problem with our plugin:

  • Number of deactivations per timeframe (month, preferably week)
  • Number of deletions per timeframe (month, preferably week)
  • Average time from activation to deactivation or deletion

Things for better testing of releases:

  • Top 20 plugins also active
  • Top 20 themes also active
  • PHP versions (percentage)
  • WordPress versions (percentage)

Atarim CEO Vito Peleg suggested some other tools for monitoring growth/decline, to which Matt Mullenweg responded that some of the ideas were “very doable:”

  • Time to churn (to deactivate) signals good/bad onboarding, UI/UX
  • Repeat installs – how many users (anonymized) install on multiple sites for community opp & advocacy
  • Time to result: dev can choose 1 single hook to trigger as “result” and the calculation checks how long from install to get there. By changing the placement of the hook devs can optimize entire flows.
  • Inner page tracker: which/how many inner plugin pages users visit
  • PHP ver distribution, general country-based installs, active install to review ratio

Wood confirmed that the active install growth charts are not coming back in their previous form and that the endpoint people were scraping before will remain disabled. He said those involved in the private discussion are monitoring the Trac ticket for feedback.

“What’s going to happen is, that the active install count instead of being rounded to the nearest digit is going to be changed,” Wood said. “I don’t know the exact break points cutoffs, but as an example show individual up to 50, then round to nearest 10 until thousand and nearest hundred until 10,000, for example. So that we are making the active install count much more fine grained than it has been. So in that sense, yes, we’re giving you the data. It’s not going to be exact numbers, but it’s going be much better than it was before. We’re still working on doing that.”

Android Native – Animate Alternating Yin Yang Symbol – Part 1

Introduction

Taking what we already learned from the previous tutorials about drawing custom shapes and animating along elements along a Path. In this tutorial, we will combine what we learned about animation so far to create an alternating Yin Yang symbol.

To those unfamiliar with the Yin Yang symbol, let us quickly visit the anatomy of the Yin Yang symbol itself.

YinYangExplanation.jpg

Based on the information from Wikipedia, in the Yin Yang symbol, Yin refers to the dark side, while Yang refers to the brightside.

For the rest of the tutorial, the following terms will be used:
Yin: the dark side.
Yang: the bright side.
Yin Dot: the black dot contained in the Yang side.
Yang Dot: the white dot contained in the Yin side.
Yin Yang Curve: the S-shaped curve that separates the Yin and the Yang.

Goals

At the end of the tutorial, you would have learned:

  1. How to animate an alternating Yin Yang symbol.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
Prerequisite Knowledge
  1. Intermedia Android.
  2. Basic Android animation.
  3. Custom Drawable.
  4. Custom Path.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the code inside activity_main.xml with the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:background="@color/teal_200"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <ImageView
            android:id="@+id/yin_yang_background"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintDimensionRatio="1:1"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:srcCompat="@tools:sample/backgrounds/scenic" />
    
     </androidx.constraintlayout.widget.ConstraintLayout>

The attributes

android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"

on the ImageView will constraint its height equal to its width.

The Plan

Normally, we can draw both the Yin and the Yang manually, but we can actually take a shortcut and skip drawing the complex teardrop-shape for the Yang entirely. To save time, we paint the entire background with Color.WHITE to make it the Yang. For the Yin, we can simply draw it with Color.Black.

Yin_Yang_Plan.png

Because we only have to draw the Yin, we also only have to animate the Yin side later on.

Draw The Yang (Bright)

Because the Yang side is simply a circular background with one solid color, we will start with it first.

  1. Replace your onCreate() with the method below.

     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    
        val yinYang = findViewById<ImageView>(R.id.yin_yang).apply {
            setImageDrawable(YinYang())
        }
     }
  2. Create your custom Drawable called YinYang with the code below.

     class YinYang: Drawable() {
        private val yangPaint = Paint().apply {
            color = Color.WHITE
        }
    
        override fun draw(canvas: Canvas) {
            drawYang(canvas)
            drawYin(canvas)
            drawYangDot(canvas)
            drawYinDot(canvas)
        }
    
        private fun drawYang(canvas: Canvas){
        }
    
        private fun drawYin(canvas: Canvas){
        }
    
        private fun drawYangDot(canvas: Canvas){
        }
    
        private fun drawYinDot(canvas: Canvas){
        }
    
        override fun setAlpha(alpha: Int) {
            // no op
        }
    
        override fun setColorFilter(colorFilter: ColorFilter?) {
            // no op
        }
    
        @Deprecated("Deprecated in Superclass")
        override fun getOpacity() = PixelFormat.OPAQUE
     }
  3. Because the drawing for the Yang, Yin, Yin Dot, and Yang Dot can get pretty complex, I have separated each part into its own private functions. We will work on them one by one.

  4. Notice the yangPaint instance variable at the top of the class definition. We will use it to draw both the Yang and the Yang Dot.

  5. The Yang must be a perfect circle with the radius as wide as half the width of its container. To draw the Yang, we use the implementation below.

     private fun drawYang(canvas: Canvas){
        canvas.drawCircle(
            bounds.exactCenterX(),
            bounds.exactCenterY(),
            bounds.exactCenterX(),
            yangPaint
        )
     }

Screenshot_1666224779.png

  1. Inside each Drawable implementation, the bounds are available via getBounds() (Java) or just the property syntax bounds (Kotlin). This bounds property contains the coordinates of the Drawables container, such as the center or the top, left, right, bottom coordinates. Using the coordinates of the exact center of the container bounds, we were able to calculate the circle radius, and (x,y) center, which were provided to the drawCircle() method.
Summary

We are completed with the first part of the tutorial, please continue on part two here.

Android Native – Animate Alternating Yin Yang Symbol – Part 2

This is the second part of a series. The first part can be found here.

Draw The Yin

Now that we have the Yang drawn, we will continue onto the Yin.

  1. Add the Paint for the Yin into the YingYang class. We will use a stroke style initially to draw. When are happy with the drawing, we can comment it out to make the code draw with the fill style (default).

     private val yinPaint = Paint().apply {
        color = Color.BLACK
        style = Paint.Style.STROKE
        strokeWidth = 10f
     }
  2. There are 3 arcs that we must draw in total for the teardrop-shaped Yin. The image below shows the arcs in order and different colors to make it easy to understand.
    Yin_arcs.png

  3. The green arrows show the flow of the contour.

  4. To draw the first arc, use 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
            )
        }
    
        canvas.drawPath(yinPath, yinPaint)
     }

Screenshot_1666226949.png
5. There might be too much going on here from a single arc. Though I expected that you already know how to draw custom shapes, the picture below attempts to describe it.
Screenshot_1666226866.png
6. Whenever drawing an arc or a circle, it is best to visualize an invisible rectangle in mind. The green square does not actually appear in the app; it is only there to help with visualization. Using the bounds of the parent container, we calculate the coordinates for the rectangle.
7. The starting angle of the circle is 270 degrees and sweeps backwards 180 degrees, creating the arc in the picture. As far as the Path class is concerned, the location of the 0 (zero) degree on a circle is the 3 o'clock position on the wall clock. If the sweep angle is positive, the arc draws clock-wise; if the sweep angle is negative, the arc draws counter clock-wise. In our code snippet, we start at the 12 oclock position and draws backwards to the 6 oclock position.
8. Now let us move on to draw the second arc.

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

       canvas.drawPath(yinPath, yinPaint)

    }

Screenshot_1666228089.png
9. For the second arc, it continues the first arc, so it starts at 90 degrees and then sweep backwards 180 degrees.
Screenshot_1666228089_copy.png
10. Finally, we draw the third and last arc.

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

Screenshot_1666228630.png
11. The third arc would continue where the second arc ended, which is at the top of the Yin Yang. It starts at 270 degrees and sweeps forward 180 degrees.
Screenshot_1666228630_copy.png
12. As the last step, comment out the stroke style and width for the yinPaint.

       private val yinPaint = Paint().apply {
           color = Color.BLACK
    /*        style = Paint.Style.STROKE
           strokeWidth = 10f*/
       }

Once the fill style takes effect, we have our Yin drawn like below. Since we do not have to draw the Yang teardrop, we are technically done with the Yin and the Yang.

Screenshot_1666228957.png

Draw The Yang Dot

The Yang Dot is the white dot contained inside the Yin. When it does not need to be animated, it is quite simple to draw.

private fun drawYangDot(canvas: Canvas){
   canvas.drawCircle(
       bounds.exactCenterX(),
       bounds.bottom * 0.75f,
       bounds.right * 0.07f,
       yangPaint
   )
}

Screenshot_1666229603.png

We can calculate the position of the Yang dot by using container bounds as well.

Screenshot_1666229603_copy.png

For the Yang Dots radius of bounds.right * 0.07f, I simply chose what feels right to me, not too big and not too small.

Draw The Yin Dot

Using a similar tactic to how we drew the Yang Dot, you can draw the Yin Dot using the code below.

private fun drawYinDot(canvas: Canvas){
   canvas.drawCircle(
       bounds.exactCenterX(),
       bounds.bottom * 0.25f,
       bounds.right * 0.07f,
       yinPaint
   )
}

Screenshot_1666230137.png

Summary

We are completed with the second part of the tutorial and finished drawing the Yin Yang. If you are interested in animating it, please continue to part three here.

Android Native – Animate Alternating Yin Yang Symbol – Part 3

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.

Android Native – Animate View Along Path – Part 2

Introduction

Welcome to part two of the tutorial. Let us continue to learn how to animate views along a Path. Part one can be found here.

Important Coordinates

Both the sun and the moon in our app can occupy any one of the three important coordinates on the screen:

  • Active planet coordinate: this the is (x,y) coordinate of the sun or the moon when the animation is complete.
  • Hidden coordinate on the right bound: this is the coordinate of the sun or the moon that is setting (disappearing from view).
  • Hidden coordinate on the left bound: this is the coordinate of the sun or the moon that is rising (appearing into view),
  • quadTo() control coordinates: this is the coordinate that controls the curve of the Path drawn by the quadTo() method. I felt that it was important for the sun and the moon to rise/fall on a curve, which looks much more organic than flying straight in from the left or right side.

To be able to get these coordinates correctly, we can get them after the views have been drawn by the system. In MainActivity, add the instance properties below.

// Size is added to x,y coordinates to make the planet go off screen
private var planetSize = Pair(0, 0)

// Location of planet when it is shown on screen
private var activeXY = Pair(0f, 0f)

// Location of planet when hidden outside left or right bounds
private var hiddenLeftXY = Pair(0f, 0f)
private var hiddenRightXY = Pair(0f, 0f)

// Control points for quadTo() function
private var risingControlXY = Pair(0f, 0f)
private var settingControlXY = Pair(0f, 0f)

In onCreate(), before the buttons on-click listener call, add the code below to initialize the coordinates.

// Initiates coordinates after the sun is drawn
sun.post {
   planetSize = Pair(sun.width, sun.height)
   activeXY = Pair(sun.x, sun.y)
   hiddenLeftXY = Pair(
       0f - planetSize.first,
       activeXY.second + planetSize.second
   )
   hiddenRightXY = Pair(
       activeXY.first * 2 + planetSize.first,
       activeXY.second + planetSize.second
   )
   risingControlXY = Pair(
       activeXY.first - activeXY.first * 0.75f,
       activeXY.second
   )
   settingControlXY = Pair(
       activeXY.first + activeXY.first * 0.75f,
       activeXY.second
   )
}
Rising And Setting Paths

Now that we have the important coordinates to create our Path, let us create the rising and setting paths for our planets. In the on-click listener, add the code below.

// Both sun and moon can use this animator
// Animates the planet setting
val settingPath = Path().apply {
   moveTo(activeXY.first, activeXY.second)
   quadTo(
       settingControlXY.first,
       settingControlXY.second,
       hiddenRightXY.first,
       hiddenRightXY.second
   )
}

// Both sun and moon can use this animator
// Animates the planet rising
val risingPath = Path().apply {
   moveTo(hiddenLeftXY.first, hiddenLeftXY.second)
   quadTo(
       risingControlXY.first,
       risingControlXY.second,
       activeXY.first,
       activeXY.second
   )
}
Animate Along A Path

We can set a Views x and y using ObjectAnimator. Add the code below inside the on-click listener.

// Animates the sun rising or setting
val sunAnimator = ObjectAnimator.ofFloat(
   sun,
   "x",
   "y",
   if (isNight) risingPath else settingPath
)

// Animates the moon rising or setting
val moonAnimator = ObjectAnimator.ofFloat(
   moon,
   "x",
   "y",
   if (isNight) settingPath else risingPath
)

Last but not least, add these Animators into the AnimatorSet.

AnimatorSet().apply {
   // Play animations together
   playTogether(
       containerAnimator,
       cloudAnimator,
       sunAnimator,
       moonAnimator,
   )

   duration = 2000L
   start()
}
Play Animations Together

The code we have so far set up the animations to play together. Play the animation now and the app should behave like the animation below.

Daniweb_animated_day_night.gif

Play Animations Sequentially

If you would like, you can also play the animations sequentially.

           AnimatorSet().apply {
               // Play animations separately
               playSequentially(
                   containerAnimator,
                   cloudAnimator,
                   sunAnimator,
                   moonAnimator
               )

               // Play animations together
/*                playTogether(
                   containerAnimator,
                   cloudAnimator,
                   sunAnimator,
                   moonAnimator,
               )*/

               duration = 2000L
               start()
           }

Daniweb_animated_day_night_sequential.gif

Summary

We have learned how to animate a View along a Path in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebCombineAnimationsAnimatorSet.

Android Native – Animate View Along Path – Part 1

Introduction

In the last tutorial, we learned how to draw complex shapes using a Path object. Using the concepts from the previous tutorial, we will now attempt to animate Views to move along a custom Path.

Goals

At the end of the tutorial, you would have learned:

  1. How to animate Views to move along a Path.
  2. How to choreograph animations using AnimatorSet.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
Prerequisite Knowledge
  1. Intermediate Android.
  2. Basic Animation.
  3. Drawing complex shapes using Path.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the content of activity_main.xml with the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/switch_day_night"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Switch"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    
        <ImageView
            android:id="@+id/house"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_marginBottom="32dp"
            android:background="@drawable/ic_baseline_house_24"
            app:layout_constraintBottom_toTopOf="@+id/switch_day_night"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />
    
            <ImageView
                android:id="@+id/cloud"
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:layout_marginStart="48dp"
                android:layout_marginBottom="16dp"
                android:background="@drawable/ic_baseline_cloud_24"
                app:layout_constraintBottom_toTopOf="@+id/house"
                app:layout_constraintStart_toStartOf="parent" />
    
            <ImageView
                android:id="@+id/sun"
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:layout_marginBottom="48dp"
                android:background="@drawable/ic_baseline_wb_sunny_24"
                app:layout_constraintBottom_toTopOf="@+id/house"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent" />
    
        <ImageView
            android:id="@+id/moon"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:background="@drawable/ic_baseline_mode_night_24"
            app:layout_constraintBottom_toTopOf="@+id/house"
            app:layout_constraintEnd_toStartOf="parent" />
    
     </androidx.constraintlayout.widget.ConstraintLayout>
  3. Add the vector drawable below as ic_baseline_cloud_24.xml.

     <vector android:height="24dp" android:tint="#002AFF"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96z"/>
     </vector>
  4. Add the vector drawable below as ic_baseline_house_24.xml.

     <vector android:height="24dp" android:tint="#E08757"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M19,9.3V4h-3v2.6L12,3L2,12h3v8h5v-6h4v6h5v-8h3L19,9.3zM10,10c0,-1.1 0.9,-2 2,-2s2,0.9 2,2H10z"/>
     </vector>
  5. Add the vector drawable below as ic_baseline_mode_night_24.xml.

     <vector android:height="24dp" android:tint="#E08757"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M19,9.3V4h-3v2.6L12,3L2,12h3v8h5v-6h4v6h5v-8h3L19,9.3zM10,10c0,-1.1 0.9,-2 2,-2s2,0.9 2,2H10z"/>
     </vector>
  6. Add the vector drawable below as ic_baseline_wb_sunny_24.xml.

     <vector android:height="24dp" android:tint="#FF6F00"
        android:viewportHeight="24" android:viewportWidth="24"
        android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
        <path android:fillColor="@android:color/white" android:pathData="M6.76,4.84l-1.8,-1.79 -1.41,1.41 1.79,1.79 1.42,-1.41zM4,10.5L1,10.5v2h3v-2zM13,0.55h-2L11,3.5h2L13,0.55zM20.45,4.46l-1.41,-1.41 -1.79,1.79 1.41,1.41 1.79,-1.79zM17.24,18.16l1.79,1.8 1.41,-1.41 -1.8,-1.79 -1.4,1.4zM20,10.5v2h3v-2h-3zM12,5.5c-3.31,0 -6,2.69 -6,6s2.69,6 6,6 6,-2.69 6,-6 -2.69,-6 -6,-6zM11,22.45h2L13,19.5h-2v2.95zM3.55,18.54l1.41,1.41 1.79,-1.8 -1.41,-1.41 -1.79,1.8z"/>
     </vector>
Project Overview

Our starter project contains quite a few elements: ImageViews that contain images for a sun, a moon (not visible yet), the cloud, a house, and finally a button to switch the scene between day and night.

Screenshot_1665895933.png

Upon completion of the project, every time the switch button is pressed, we would have four different animations:

  1. The sun rising from the left or setting to the right.
  2. The moon rising from the left or setting to the right.
  3. The background color animated between black and white.
  4. The cloud color animated between blue and gray.

The hardest animation to achieve here is the sun and the moon rising and setting following a path. We will start with the easiest animations first, which are the animating the sun and clouds colors.

Because the day/night logic is bound to many animations, lets keep track of the current state using a simple variable in MainAcitivty.

// Simple state for which kind of animation needs to be activated
private var isNight = false
Animate Background Color

Append the code below to onCreate() to get references to elements in the layout.

val button = findViewById<Button>(R.id.switch_day_night)

val container = findViewById<ConstraintLayout>(R.id.container)
val cloud = findViewById<ImageView>(R.id.cloud)
val sun = findViewById<ImageView>(R.id.sun)
val moon = findViewById<ImageView>(R.id.moon)

The background color of the container ConstraintLayout can be animated using the ValueAnimator.ofArgb() method. The code below will bind the Switch Button to start the animation of the background color.

button.setOnClickListener {
   // Animates the background color from night to morning
   val containerAnimator = ValueAnimator.ofArgb(
       if (isNight) Color.BLACK else Color.WHITE,
       if (isNight) Color.WHITE else Color.BLACK
   ).apply {
       addUpdateListener {
           container.setBackgroundColor(it.animatedValue as Int)
       }
   }

   AnimatorSet().apply {
       // Play animations together
       playTogether(
           containerAnimator
       )

       duration = 2000L
       start()
   }

   isNight = !isNight
}

Note that I am using AnimatorSet to start the animation instead of starting the background color change animation separately; the reasoning for this is because this makes it easy to manage multiple animations later on.

Daniweb_animated_background_color.gif

Animate Image Tint

Next, we will animate the cloud color as well. Because the cloud is a drawable that is assigned to an ImageView, we cannot simply change the background color. But what we can do is to modify the tint of the image itself. We can also use the same ofArgb() method from the ValueAnimator class used previously.

The code snippet below will animate the cloud and background color on button click.

button.setOnClickListener {
   // Animates the background color from night to morning
   val containerAnimator = ValueAnimator.ofArgb(
       if (isNight) Color.BLACK else Color.WHITE,
       if (isNight) Color.WHITE else Color.BLACK
   ).apply {
       addUpdateListener {
           container.setBackgroundColor(it.animatedValue as Int)
       }
   }

   // Animates the cloud color from night to morning
   val cloudAnimator = ValueAnimator.ofArgb(
       if (isNight) Color.GRAY else Color.BLUE,
       if (isNight) Color.BLUE else Color.GRAY
   ).apply {
       addUpdateListener {
           cloud.background.setTint(it.animatedValue as Int)
       }
   }

   AnimatorSet().apply {
       // Play animations together
       playTogether(
           containerAnimator,
           cloudAnimator
       )

       duration = 2000L
       start()
   }

   isNight = !isNight
}

Daniweb_animated_background_color_and_cloud.gif

Summary

We have learned how to animate background color and image tint in this first part of the tutorial. Please continue to the second part to learn how to animate the sun and moon along a path.

Android Native – Draw Complex Shapes Using The Path Class

Introduction

In the last tutorial, we learned how to draw basic shapes on Android using premade methods from the graphics library.

In this tutorial, we step up the difficulty a notch by learning how to draw using the Path class. The Path class allows for drawing of technically any shape imaginable, as a consequence, it is also a bit more complex to use compared to premade methods in the Canvas class.

Goals

At the end of the tutorial, you would have learned:

  1. How to draw complex shapes using Path.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
Prerequisite Knowledge
  1. Intermediate Android.
  2. How to draw basic shapes using the Canvas class.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the code in MainActivity.kt with the code below.

     class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            findViewById<ImageView>(R.id.imageView_myImage)
                .setImageDrawable(ComplexShapes())
        }
     }
    
     class ComplexShapes : Drawable() {
        private val paint: Paint = Paint().apply {
            // Create your paint here
            style = Paint.Style.STROKE
            strokeWidth = 10f
            color = Color.RED
        }
    
        override fun draw(canvas: Canvas) {
    
        }
    
        override fun setAlpha(alpha: Int) {
            // Required but can be left empty
        }
    
        override fun setColorFilter(colorFilter: ColorFilter?) {
            // Required but can be left empty
        }
    
        @Deprecated("Deprecated by super class")
        override fun getOpacity() = PixelFormat.OPAQUE
     }
  3. Replace the code inside activity_main.xml with the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <ImageView
            android:id="@+id/imageView_myImage"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:srcCompat="@tools:sample/backgrounds/scenic" />
     </androidx.constraintlayout.widget.ConstraintLayout>
Understanding The Path Class

There are a few important concepts that you might encounter when working with Path.

  • Contour: I am not an artist myself, but for you non-artists out there, you can think of this as a counter of when a brush touches the surface and then lifted. Every time a brush starts drawing at a (x,y) coordinate and finishes at another (x,y) coordinate, it counts as one contour.
  • new contour vs append to a contour: some of the methods of the Path class will continue a previous contour, while others will start a new contour.
  • moveTo() method: this method sets a starting point for a new contour. When a Path object is first created, its position is set to (0x,0y), so if you want to start the first contour at a different coordinate, you will have to use moveTo() to move the origin to the coordinate that you want.
Draw A Line

We will start with drawing a line as a warm-up exercise. Add the implementation below as ComplexShapess draw() method.

override fun draw(canvas: Canvas) {
   val path = Path().apply {
       lineTo(200f, 200f)
   }

   canvas.drawPath(path, paint)
}

The lineTo() method draws a straight line from the (0,0) origin of the ImageView to the (200, 200) coordinate that we specified. lineTo() appends to the default contour.

Screenshot_1665791590.png

Let us use moveTo() to move the origin point to somewhere that is not touching the corner of the screen, this will prevent the image being drawn outside the screens bounds.

override fun draw(canvas: Canvas) {
   val path = Path().apply {
       moveTo(100f, 100f)
       lineTo(300f, 300f)
   }

   canvas.drawPath(path, paint)
}

Screenshot_1665791793.png

Draw A Triangle

We already have a straight line, so we add one more line, and then use the close() method to end the current contour (think of this as lifting the brush when you are done with your current stroke).

override fun draw(canvas: Canvas) {
   val path = Path().apply {
       // Draws triangle
       moveTo(100f, 100f)
       lineTo(300f, 300f)
       lineTo(100f, 300f)
       close()
   }

   canvas.drawPath(path, paint)
}

Screenshot_1665792470.png

Draw A Bridge

Next up, lets attempt to draw a bridge. This is slightly more complex because it contains 6 strokes, including an arc.

To make an arc, you can use the arcTo() method. The most important thing that you need to know about this method is that it will draw an oval inside an imaginary rectangle.

override fun draw(canvas: Canvas) {
   val path = Path().apply {
       // Draws triangle
       moveTo(100f, 100f)
       lineTo(300f, 300f)
       lineTo(100f, 300f)
       close()

       // Draws bridge
       moveTo(100f, 400f)
       lineTo(600f, 400f)
       lineTo(600f, 700f)
       lineTo(500f, 700f)
       // bottom is 900 because the system draws the arc inside an
       // imaginary rectangle
       arcTo(200f, 500f, 500f, 900f, 0f, -180f, false)
       lineTo(100f, 700f)
       close()
   }

   canvas.drawPath(path, paint)
}

Screenshot_1665794129.png

Draw A Quarter Moon

The last shape that we will draw for this tutorial would be a quarter moon. For this shape, we will need two curves. We can re-use arcTo(), but because we already use that method previously, we will try some new methods this time.

override fun draw(canvas: Canvas) {
   val path = Path().apply {
       // Draws triangle
       moveTo(100f, 100f)
       lineTo(300f, 300f)
       lineTo(100f, 300f)
       close()

       // Draws bridge
       moveTo(100f, 400f)
       lineTo(600f, 400f)
       lineTo(600f, 700f)
       lineTo(500f, 700f)
       // bottom is 900 because the system draws the arc inside an
       // imaginary rectangle
       arcTo(200f, 500f, 500f, 900f, 0f, -180f, false)
       lineTo(100f, 700f)
       close()

       // Draws quarter moon
       moveTo(100f, 800f)
       addArc(100f, 800f, 600f, 1_300f, 90f, -180f)
       quadTo(450f, 1_050f, 350f, 1_303f)
   }

   canvas.drawPath(path, paint)
}

To draw the outer curve, we use addArc(), and to draw the inner curve, we use the quadTo() method.

Screenshot_1665855335.png

Summary

We have learned how to draw complex shapes in this tutorial. In my opinion, while the ability to draw the custom shapes are nice, if the shape is too complex, then it is still better to make them in a vector editor such as Inkscape or Adobe Illustrator.

The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidDrawComplexShapes.

Android Native – Draw Basic Shapes On Canvas

Introduction

Even though the built-in vector library included in Android Studio contains many icons readily for use, you might eventually run into a situation where a custom icon is needed. In this tutorial, we will learn how to create our own icons from basic shapes drawn on a Drawables Canvas.

Goals

At the end of the tutorial, you would have learned:

  1. How to draw basic shapes using custom Drawable.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
Prerequisite Knowledge
  1. Intermedia Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the code in activity_main.xml with the code below.

     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <ImageView
            android:id="@+id/imageView_myImage"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:srcCompat="@tools:sample/backgrounds/scenic" />
     </androidx.constraintlayout.widget.ConstraintLayout>

At this stage, our sample application only displays a blank screen.

  • (Optional) Go to Developer options and enable Pointer Location. This will allow you to see the screen coordinates when drawing.
Overriding Drawable

Before we can draw shapes, we will need to create a custom Drawable for our ImageView. Drawables children are required to override the method draw(), which is where the drawing can start.

Copy and paste the following custom Drawable, either in the file MainActivity.kt or as a different file.

class BasicShapes : Drawable() {
   private val paint: Paint = Paint().apply {
       // Create your paint here
       style = Paint.Style.STROKE
       strokeWidth = 10f
       color = Color.CYAN
   }

   override fun draw(canvas: Canvas) {
       // Do your drawing here
       canvas.drawRect(
           RectF(100f, 100f, 200f, 300f),
           paint
       )
   }

   override fun setAlpha(alpha: Int) {
       // Required but can be left empty
   }

   override fun setColorFilter(colorFilter: ColorFilter?) {
       // Required but can be left empty
   }

   @Deprecated("Deprecated by super class")
   override fun getOpacity() = PixelFormat.OPAQUE
}

Drawable requires us to override many different methods, but we can just focus on the draw() method and leave everything else as default.

The Paint object located in our custom implementation is also important. We need it to be able to draw anything.

Even though we do not have any useful implementation of draw() yet, it is fine to use BasicShapes as the drawable of the ImageView. Append the code below to onCreate().

findViewById<ImageView>(R.id.imageView_myImage)
   .setImageDrawable(BasicShapes())
Drawing A Rectangle

One more thing that we need to understand before we can draw on a Canvas is the coordinate system. A Views top left corner always have a (0x, 0y) coordinates, which also means that the bottom right corner contains (maxX, maxY) coordinates.

Screen_Shot_2022-10-14_at_9.54.46_AM.png

In most cases, we will have to provide the coordinates when drawing on Canvas.

To draw a Rectangle, we would use the drawRect() method from the Canvas object passed to onDraw().

override fun draw(canvas: Canvas) {
   // Do your drawing here
   canvas.drawRect(
       Rect(
           100, // distance from left of view
           100, // distance from top of view
           400, // distance from left of view
           300 // distance from top of view
       ),
       paint
   )
}

The code above will draw a rectangle (check code comments), as the screenshot below illustrates.

Screenshot_1665761209.png

Draw More Basic Shapes

Once you have figured out the coordinate system, drawing other shapes is only a matter of applying basic geometry on the appropriate methods.

  • A circle can be drawn using drawCircle().
  • An oval can be drawn using drawOval().
  • A square can be drawn using drawRect() with appropriate dimensions.

The code below adds a circle, an oval and a square to our application.

override fun draw(canvas: Canvas) {
   // Do your drawing here
   canvas.drawRect(
       Rect(
           100, // distance from left of view
           100, // distance from top of view
           400, // distance from left of view
           300 // distance from top of view
       ),
       paint
   )

   canvas.drawCircle(300f, 500f, 100f, paint)
   canvas.drawOval(
       RectF(100f, 700f, 500f, 800f),
       paint
   )
   // square
   canvas.drawRect(
       Rect(
           500, // distance from left of view
           100, // distance from top of view
           700, // distance from left of view
           300 // distance from top of view
       ),
       paint
   )
}

The app should look like the screenshot below.

Screenshot_1665762591.png

Summary

We have learned how to draw basic shapes in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidDrawBasicShapesOnCanvas.

Android Native – Animate Sprites Using AnimationDrawable

Introduction

Android includes many options to add animations to your app. In this tutorial, we will learn how to add a type of animation called frame-by-frame animation into our Android app.

Goals

At the end of the tutorial, you would have learned:

  1. How to add frame-by-frame animation to an Android app.
Tools Required
  1. Android Studio. The version used in this tutorial is Android Studio Dolphin | 2021.3.1.
Prerequisite Knowledge
  1. Intermediate Android.
Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Download the free Santa sprites from Game Art 2d.

  3. Unpack the archive and import all files in the png directory to the project (keep the same names that Android Studio generated for you).

  4. Copy and paste all the <string> resources below into your projects strings.xml file.

     <string name="idle">Idle</string>
     <string name="walk">Walk</string>
     <string name="run">Run</string>
     <string name="jump">Jump</string>
     <string name="die">Die</string>
     <string name="slide">Slide</string>
  5. Replace all the content of activity_main.xml with the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <ImageView
            android:id="@+id/image_santa"
            android:layout_width="0dp"
            android:layout_height="300dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:src="@drawable/idle__1_" />
    
        <Button
            android:id="@+id/idle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="@string/idle"
            app:layout_constraintBottom_toTopOf="@id/walk"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/image_santa" />
    
        <Button
            android:id="@+id/walk"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/walk"
            app:layout_constraintBottom_toTopOf="@id/run"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/idle" />
    
        <Button
            android:id="@+id/run"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/run"
            app:layout_constraintBottom_toTopOf="@id/jump"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/walk" />
    
        <Button
            android:id="@+id/jump"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/jump"
            app:layout_constraintBottom_toTopOf="@id/slide"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/run" />
    
        <Button
            android:id="@+id/slide"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/slide"
            app:layout_constraintBottom_toTopOf="@id/die"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/jump" />
    
        <Button
            android:id="@+id/die"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/die"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/slide" />
     </androidx.constraintlayout.widget.ConstraintLayout>
Project Overview

Our starter application contains a single screen with 6 buttons, allowing the user to start animations of the following actions

  1. Idle.
  2. Walk.
  3. Run.
  4. Jump.
  5. Slide.
  6. Die.

Screenshot_1665435838.png

There are two steps that we need to do next to allow the animation to work:

  1. Set up the animation resources that describe the frame-by-frame image.
  2. Set up button listeners to activate the animation.
Set Up Animation Resource XML

The type of animation that we are aiming for can be achieved by loading images in a sequence. We can either define this animation in code using the AnimationDrawable class or using a pre-defined Frame Animation Resource. The frame animation resource starts with an <animation-list> tag, and then each image is defined in an <item> tag.

To create the frame animation resource for the idle action, create a new file called idle.xml and copy and paste the code below into it.

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:drawable="@drawable/idle__1_" android:duration="75" />
   <item android:drawable="@drawable/idle__2_" android:duration="75" />
   <item android:drawable="@drawable/idle__3_" android:duration="75" />
   <item android:drawable="@drawable/idle__4_" android:duration="75" />
   <item android:drawable="@drawable/idle__5_" android:duration="75" />
   <item android:drawable="@drawable/idle__6_" android:duration="75" />
   <item android:drawable="@drawable/idle__7_" android:duration="75" />
   <item android:drawable="@drawable/idle__8_" android:duration="75" />
   <item android:drawable="@drawable/idle__9_" android:duration="75" />
   <item android:drawable="@drawable/idle__10_" android:duration="75" />
   <item android:drawable="@drawable/idle__11_" android:duration="75" />
   <item android:drawable="@drawable/idle__12_" android:duration="75" />
   <item android:drawable="@drawable/idle__13_" android:duration="75" />
   <item android:drawable="@drawable/idle__14_" android:duration="75" />
   <item android:drawable="@drawable/idle__15_" android:duration="75" />
   <item android:drawable="@drawable/idle__16_" android:duration="75" />
</animation-list>

Each <item> must contain the source to the actual drawable resource (the images that we imported earlier). The duration attribute dictates how long to show a frame, in milliseconds.

If you switch to Split/Design view, you can also preview the animation.

Screen_Shot_2022-10-10_at_5.50.14_PM.png

Now that we already know how to define the idle animation resource, create the rest of the animations with the names below.

  1. walk.xml.

     <?xml version="1.0" encoding="utf-8"?>
     <animation-list xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:drawable="@drawable/walk__1_" android:duration="75" />
        <item android:drawable="@drawable/walk__2_" android:duration="75" />
        <item android:drawable="@drawable/walk__3_" android:duration="75" />
        <item android:drawable="@drawable/walk__4_" android:duration="75" />
        <item android:drawable="@drawable/walk__5_" android:duration="75" />
        <item android:drawable="@drawable/walk__6_" android:duration="75" />
        <item android:drawable="@drawable/walk__7_" android:duration="75" />
        <item android:drawable="@drawable/walk__8_" android:duration="75" />
        <item android:drawable="@drawable/walk__9_" android:duration="75" />
        <item android:drawable="@drawable/walk__10_" android:duration="75" />
        <item android:drawable="@drawable/walk__11_" android:duration="75" />
        <item android:drawable="@drawable/walk__12_" android:duration="75" />
        <item android:drawable="@drawable/walk__13_" android:duration="75" />
     </animation-list>
  2. run.xml.

     <?xml version="1.0" encoding="utf-8"?>
     <animation-list xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:drawable="@drawable/run__1_" android:duration="75" />
        <item android:drawable="@drawable/run__2_" android:duration="75" />
        <item android:drawable="@drawable/run__3_" android:duration="75" />
        <item android:drawable="@drawable/run__4_" android:duration="75" />
        <item android:drawable="@drawable/run__5_" android:duration="75" />
        <item android:drawable="@drawable/run__6_" android:duration="75" />
        <item android:drawable="@drawable/run__7_" android:duration="75" />
        <item android:drawable="@drawable/run__8_" android:duration="75" />
        <item android:drawable="@drawable/run__9_" android:duration="75" />
        <item android:drawable="@drawable/run__10_" android:duration="75" />
        <item android:drawable="@drawable/run__11_" android:duration="75" />
     </animation-list>
  3. jump.xml.

     <?xml version="1.0" encoding="utf-8"?>
     <animation-list xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:drawable="@drawable/jump__1_" android:duration="75" />
        <item android:drawable="@drawable/jump__2_" android:duration="75" />
        <item android:drawable="@drawable/jump__3_" android:duration="75" />
        <item android:drawable="@drawable/jump__4_" android:duration="75" />
        <item android:drawable="@drawable/jump__5_" android:duration="75" />
        <item android:drawable="@drawable/jump__6_" android:duration="75" />
        <item android:drawable="@drawable/jump__7_" android:duration="75" />
        <item android:drawable="@drawable/jump__8_" android:duration="75" />
        <item android:drawable="@drawable/jump__9_" android:duration="75" />
        <item android:drawable="@drawable/jump__10_" android:duration="75" />
        <item android:drawable="@drawable/jump__11_" android:duration="75" />
        <item android:drawable="@drawable/jump__12_" android:duration="75" />
        <item android:drawable="@drawable/jump__13_" android:duration="75" />
        <item android:drawable="@drawable/jump__14_" android:duration="75" />
        <item android:drawable="@drawable/jump__15_" android:duration="75" />
        <item android:drawable="@drawable/jump__16_" android:duration="75" />
     </animation-list>
  4. slide.xml

     <?xml version="1.0" encoding="utf-8"?>
     <animation-list xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:drawable="@drawable/slide__1_" android:duration="75" />
        <item android:drawable="@drawable/slide__2_" android:duration="75" />
        <item android:drawable="@drawable/slide__3_" android:duration="75" />
        <item android:drawable="@drawable/slide__4_" android:duration="75" />
        <item android:drawable="@drawable/slide__5_" android:duration="75" />
        <item android:drawable="@drawable/slide__6_" android:duration="75" />
        <item android:drawable="@drawable/slide__7_" android:duration="75" />
        <item android:drawable="@drawable/slide__8_" android:duration="75" />
        <item android:drawable="@drawable/slide__9_" android:duration="75" />
        <item android:drawable="@drawable/slide__10_" android:duration="75" />
        <item android:drawable="@drawable/slide__11_" android:duration="75" />
     </animation-list>
  5. die.xml.

     <?xml version="1.0" encoding="utf-8"?>
     <animation-list xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:drawable="@drawable/dead__1_" android:duration="75" />
        <item android:drawable="@drawable/dead__2_" android:duration="75" />
        <item android:drawable="@drawable/dead__3_" android:duration="75" />
        <item android:drawable="@drawable/dead__4_" android:duration="75" />
        <item android:drawable="@drawable/dead__5_" android:duration="75" />
        <item android:drawable="@drawable/dead__6_" android:duration="75" />
        <item android:drawable="@drawable/dead__7_" android:duration="75" />
        <item android:drawable="@drawable/dead__8_" android:duration="75" />
        <item android:drawable="@drawable/dead__9_" android:duration="75" />
        <item android:drawable="@drawable/dead__10_" android:duration="75" />
        <item android:drawable="@drawable/dead__11_" android:duration="75" />
        <item android:drawable="@drawable/dead__12_" android:duration="75" />
        <item android:drawable="@drawable/dead__12_" android:duration="75" />
        <item android:drawable="@drawable/dead__13_" android:duration="75" />
        <item android:drawable="@drawable/dead__14_" android:duration="75" />
        <item android:drawable="@drawable/dead__15_" android:duration="75" />
        <item android:drawable="@drawable/dead__16_" android:duration="75" />
        <item android:drawable="@drawable/dead__17_" android:duration="75" />
     </animation-list>
Start The Animation In Code

To use the animations that we have defined, first, in onCreate(), get the reference to the ImageView that displays Santa.

val santaImage = findViewById<ImageView>(R.id.image_santa)

We have six buttons in total, calling findViewById() 6 different times would be a chore, so let us use a Map to reduce repetitive code and improve readability.

// Add Button IDs and Animation IDs into map for easy looping.
val animationMap = mapOf(
   Pair(R.id.idle, R.drawable.idle),
   Pair(R.id.walk, R.drawable.walk),
   Pair(R.id.run, R.drawable.run),
   Pair(R.id.jump, R.drawable.jump),
   Pair(R.id.slide, R.drawable.slide),
   Pair(R.id.die, R.drawable.die)
)

for ((buttonId, animationId) in animationMap){
   findViewById<Button>(buttonId).setOnClickListener {
       santaImage.setImageResource(animationId)
       (santaImage.drawable as AnimationDrawable).start()
   }
}

The two most important lines of code above are

santaImage.setImageResource(animationId)
(santaImage.drawable as AnimationDrawable).start()
  1. Every time we need to play a different animation, we need to replace the image resource with the corresponding animation resource.
  2. We then cast the drawable to AnimationDrawable and then call start() to start the animation.
Run The App

We are now ready to run the app, it should behave similarly to the animation below.

Daniweb_animated_Sprites.gif

Summary

We have learned how to animate sprites in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAnimateSpritesAnimationDrawable.

Hello Quantum…👋…Goodbye Shared Hosting!

For the last 20 years, the dominant form of hosting has been shared. Something like 20-50 sites stuck on one dedicated server running WHM/cPanel and working out at between $3-5 per site, per month. Sounds like your setup? You’re not alone.

Every site shares the same IP address, every site is vulnerable to one of the other sites hogging all the resources, every site can be damaged by one hack, every site is in the same location.

We know this because we’ve surveyed our members at length and – even though nobody likes it – it’s hard to argue with that cost per site, especially when compared to standard managed WordPress hosting costs.

Well, that changes today. With Quantum.

Quantum: A Leap Ahead on Shared Hosting!

Quantum Hosting vs Shared Hosting
Quantum Hosting vs Shared Hosting

Every WPMU DEV Quantum plan comes with:

  • Dedicated and compartmentalized resources
  • Individual IP addresses
  • Choice of 5 worldwide locations: US East, US West, Netherlands, Germany, Singapore
  • 10GB storage & 500GB/m bandwidth
  • Uncapped visits, domain specific email
  • All pro WPMU DEV plugins
  • Optimized, cached, protected managed WP

For $4/m (or from $3.60/m if you pre-pay).

Yes, you read that right. That’s $4/m.

Not “in your first year”, “discounted” or “with a 24 month plan paid upfront”.

Just $4/m as a flat rate.

Oh, and if migrating 20 (or 100+ sites) sounds like a pain, then don’t stress it because we will migrate your sites for you, for free.

Exclusive to Agency

There are a few conditions that make Quantum able to be a thing.

First up, and perhaps most importantly, unlike the rest of our hosting offers, Quantum is exclusive to our Agency plan.

This excludes, for the first time, all our legacy memberships with the exception of lifetime members (which can only be earned, not bought).

This is both to thank our Agency level members and so that we can provide the same level of 24/7 expert support to this plan that we are committed to providing to every WPMU DEV member.

Find out more about our membership levels here and if you have any questions (or would like to talk about transitioning from a Legacy to an Agency plan) please contact our sales team, they’ll be happy to help.

To make Quantum lean and fast, we’ve also introduced these parameters:

  • While there are no limits on themes or theme builders, we have limited plugins to the top 40-50 most popular, with the exception of WooCommerce, as it’s just too resource heavy
  • Staging, Multisite and a range of hosting tools are not available with this plan, for the same reasons, find out more on the Quantum page.
  • Due to these limitations we cannot allow downgrades to Quantum.

We’ve put these in place to ensure that your experience using Quantum, and that of your clients, is out of this world good.

And, of course, if you do need more resources, tools or to remove all limits, you can do that by simply upgrading a Quantum plan to one of our Bronze hosting plans ($12/m or $10.80/m with pre-pay).

Give Quantum a Go

If you are already an Agency level WPMU DEV member you can spin up a site now and give it a go in your Hosting area.

Also, all Agency memberships come with $144 hosting credit per year, so your first 3 Quantum sites will actually be for free!

Hub Hosting Quantum
Agency member? Select Quantum and give your clients an in-credit-able hosting experience!

As with all of our products, it comes with a money-back guarantee, so if you don’t like it / it doesn’t meet your needs, then you can just cancel your plan and ask us for a refund.

We reckon you will like it though :)

If you are not an Agency member you can try it out for free now, with a 7 day free trial, and give Quantum a spin while you are at it.

Quantum Hosting Plan
Quantum hosting plan…engineered for WordPress and priced for resellers!

As above, if you don’t like it, just let us know and we will refund you any costs incurred.

But we really do think that you’ll actually want to stay!

We’re so excited to bring you a product that we think has the capacity to radically change how WordPress has been hosted for the better (and hey, even make the internet a better place as a result).

Any questions or comments please contact our sales team, we’re standing by and waiting to hear from you.

Using OAuth in API Integrations With Python, REST, and HL7 FHIR

OAuth is often employed in processes requiring permissions to be granted to front-end applications and end users. Yet what we typically need in API systems integrations is a way to secure connections between the integration middleware and backend systems without a need for any ongoing human interactions.

OAuth can be a good choice for that scenario. This article shows how it can be achieved in Python with backend systems using REST and HL7 FHIR.

How To Integrate Grafana in Our Internal Tools/Admin Panels Using AuthProxy

Grafana is an amazing charting tool, as it can provide beautiful charts and visualization for data. Integrating Grafana charts with internal tools can unlock great potential. Internal tools are custom dashboards, admin panels, and CRUD apps that enable teams to automate processes.

Grafana comes with built-in authentication. To view its dashboard, the user has to go through its login screen.

rel=”ugc” vs rel=”ugc,nofollow”

According to this page we switched links within our forum posts from using rel="nofollow" to rel="ugc". However, Do you think that it makes sense to use rel="nofollow,ugc", or is the nofollow part already implied? Does anyone have any anecdotal evidence as to whether they've noticed a real world difference between the two?

Is it possible to do this node.js code with .NET??

I really need help and i would apreciate if someone can help me with this I need to do the same than here but with .NET, instead of node js. I never worked with .NET before.

Is my first time working with tokens and APIs. So i kinda need help with this

Question: Is it possible to do this with .NET? if it's not possible, i would like to know it. thanks.

UH API Behavior - Error handling

. Sample API request with retry for refreshing the token (if the app is using axios and MSAL.js).

get: async (url, retry = true) => {
        const _url = url;
        const headers = {
            Authorization: `Bearer ${sessionStorage.getItem("user_token")}`
        };
        const _retry = retry;
        const axiosInstance = axios.create({
            headers,
            responseType: "application/json"
        });
        axiosInstance.interceptors.response.use(
            (res) => {                return res;
            },
            async (error) => {
                // debugger;
                const status = error.response ? error.response.status : null;
                if (status === 401 && _retry) {
                    sessionStorage.removeItem("user_token");
              const hasToken = await  refreshToken();                  
                        return httpClirefreshTokenentServiceWithRefreshToken.get(
                            _url,
                            false
                        );                    
                }
                return Promise.reject(error);
            }
        );
        return new Promise((resolve, reject) => {
            axiosInstance
                .get(_url)
                // .post(url, data, headers)
                .then((response) => {
                    if (response.status === 200) {
                        resolve(response.data);
                    } else {
                        resolve(response);
                    }
                })
                .catch((e) => {
                    reject(e);
                });
        });
    },

IBM App Connect Operators

If you’re relatively new to Kubernetes, or not entirely sure what an Operator is, and why IBM created one for IBM App Connect, this post is for you. 

You do not have to use the Operator to use IBM App Connect in containers. However, we aim to show how the Operator significantly simplifies Kubernetes deployment.

Data Modeling in Cassandra and Astra DB

What does it take to build an efficient and sound data model for Apache Cassandra and DataStax Astra DB? Where would one start? Are there any data modeling rules to follow? Can it be done consistently time and time again? The answers to these and many other questions can be found in the Cassandra data modeling methodology.

In this post, we present a high-level overview of the data modeling methodology for Cassandra and Astra DB, and share over half a dozen complete data modeling examples from various real-life domains. We apply the methodology to create Cassandra and Astra DB data models for IoT, messaging data, digital library, investment portfolio, time series, shopping cart, and order management. We even provide our datasets and queries for you to try.