Gradient / VectorDrawable

Elliptical Vector Gradients

We’ve had gradient support in VectorDrawable since API 24 and I have written about them previously. In those articles I covered an problem importing sweep gradients because sweep gradients are not supported in SVG, which is a failing of SVG rather than any Android specifics. However there is another edge case which is supported in SVG but the Asset Studio tool in Android Studio does not handle, but with a bit of understanding we can manually implement. I was first asked about this by a designer based in Sweden named Olga Zhernova and I made a brief suggestion of what is to follow and she was able to achieve what she wanted. So thanks to Olga for asking an interesting question! In this post we’ll take a look at the issue and see how we can overcome it.

The image to the left shows a specific kind of radial gradient – rather than the bounds of the gradient being circular, the width and height are different resulting in an elliptical shape to the gradient which starts at white, and transitions to green. In some graphic design tools it is possible to create these elliptical gradients, and I was able to create one in Sketch. The gradient tool in Sketch perfectly shows this elliptical shape:

If I export this to an SVG we can open it in Chrome and the elliptical gradient is still there:

If I now import this in to Android Studio using Vector Studio, then even in the preview of the VectorDrawable before we complete the import we can see that the elliptical shape has been lost and we revert to a standard circular radial gradient:

The reason for this may seem obvious to those familiar with RadialGradient in Android which is used when rendering gradients directly to a Canvas, and is the basis for the radial gradient in VectorDrawable. RadialGradient only permits us to specify a single value as the radius of the gradient meaning that it will only render circular gradients.

At first glance this may appear to be fairly insurmountable – if RadialGradient does not support asymmetric gradients then it makes sense that Asset Studio cannot handle them on import. However, just because RadialGradient only supports symmetrical gradients does not mean that we cannot render them. Canvas allows us to apply independent X and Y scaling and if we do apply different X and Y scale factors prior to rendering using the RadialGradient shader, then we can effectively render an asymmetric radial gradient.

We can also apply this same principle to VectorDrawable to get an asymmetric radial gradient by applying the scale by using a group. To understand how to do this, let’s first take a look at the VectorDrawable which I imported from SVG (which lost the elliptical gradient):

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:aapt="http://schemas.android.com/aapt"
  android:width="100dp"
  android:height="100dp"
  android:viewportHeight="100"
  android:viewportWidth="100">

  <path android:strokeColor="#FF1E9618"
    android:strokeWidth="0.5"
    android:pathData="M50,5 a45,45 0 1 0 0.1,0 z" >
    <aapt:attr name="android:fillColor">
      <gradient
        android:centerX="70"
        android:centerY="30"
        android:startColor="#FFFFFFFF"
        android:endColor="#FF1E9618"
        android:gradientRadius="22.5"
        android:type="radial"/>
    </aapt:attr>
  </path>
</vector>

I should mention that I slightly tweaked the path datafrom what was imported because it will hopefully make what follows a little easier to follow. The original path data was M50,50m-45,0a45,45 0,1 1,90 0a45,45 0,1 1,-90 0 and I converted it to a single arc rather than two semicircular arcs: M50,5 a45,45 0 1 0 0.1,0 z

Anyone familiar with using radial gradients within VectorDrawable should have an idea of how this will look, and that it will have a symmetrical, circular radial gradient – which ties in with the preview:

We now wrap this inside a group with scaleY="2" and then tweak the centreY and gradientRadius attributes by halving each of them to compensate for the Y scaling added to the group:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:aapt="http://schemas.android.com/aapt"
  android:width="100dp"
  android:height="100dp"
  android:viewportHeight="100"
  android:viewportWidth="100">
  <group android:scaleY="2">
    <path
      android:fillType="evenOdd"
      android:pathData="M50,2.5 a45,45 0 1 0 0.1,0 z"
      android:strokeColor="#1E9618"
      android:strokeWidth="1">
      <aapt:attr name="android:fillColor">
        <gradient
          android:centerX="70"
          android:centerY="15"
          android:endColor="#FF1E9618"
          android:gradientRadius="11.25"
          android:startColor="#FFFFFFFF"
          android:type="radial" />
      </aapt:attr>
    </path>
  </group>
</vector>

This now looks like this:

So we now have an elliptical gradient, but we have also distorted the circular outline, which is not what we wanted. One solution would be to change the path itself to fill a rectangle, and then apply a clip-path before, and outside of the group:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:aapt="http://schemas.android.com/aapt"
  android:width="100dp"
  android:height="100dp"
  android:viewportHeight="100"
  android:viewportWidth="100">

  <clip-path
    android:pathData="M50,2.5 a45,45 0 1 0 0.1,0 z" />

  <group android:scaleY="2">
    <path
      android:fillType="evenOdd"
      android:pathData="M0,0H100V100H0z"
      android:strokeColor="#1E9618"
      android:strokeWidth="1">
      <aapt:attr name="android:fillColor">
        <gradient
          android:centerX="70"
          android:centerY="15"
          android:endColor="#FF1E9618"
          android:gradientRadius="11.25"
          android:startColor="#FFFFFFFF"
          android:type="radial" />
      </aapt:attr>
    </path>
  </group>
</vector>

At a first glance this looks fine:

However, if you look carefully at the edges of the circle they are quite jagged. This is because clip-path renders at pixel level meaning that it controls whether a given pixel can be painted or not. A path on the other hand, renders at a sub-pixel level meaning that for a pixel on the perimeter of the circle, the actual colour that it is painted will be determined by the proportion of that pixel which falls inside the circle. This is known as anti-aliasing and results in edges which appear much smoother to the human eye. It’s the lack of this anti-aliasing (which we lose with clip-path) which causes these jaggies.

In order to get rid of the jaggies, a better solution is to actually tweak the path data of the circle to account for the change to the Y scaling. In this case the Y component of the move command needs halving from M50,5 to M50,2.5 and the radius of the arc needs halving from a45,45 0 1 0 0.1,0 to a45,22.5 0 1 0 0.1,0:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:aapt="http://schemas.android.com/aapt"
  android:width="100dp"
  android:height="100dp"
  android:viewportHeight="100"
  android:viewportWidth="100">
  <group android:scaleY="2">
    <path
      android:fillType="evenOdd"
      android:pathData="M50,2.5 a45,22.5 0 1 0 0.1,0 z"
      android:strokeColor="#1E9618"
      android:strokeWidth="1">
      <aapt:attr name="android:fillColor">
        <gradient
          android:centerX="70"
          android:centerY="15"
          android:endColor="#FF1E9618"
          android:gradientRadius="11.25"
          android:startColor="#FFFFFFFF"
          android:type="radial" />
      </aapt:attr>
    </path>
  </group>
</vector>

It now looks like this:

So we now have an elliptical gradient, inside a symmetrical circle, with no jaggies!

So we can manually implement an asymmetric radial gradient by using an asymmetrical scaling in a group,. and then manually tweaking the gradient and path data parameters to compensate for that scaling.

Once again, my thanks to Olga Zhernova for asking the question which prompted this post.

The source code for this article is available here.

© 2018, Mark Allison. All rights reserved.

Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.