Gradient / VectorDrawable

VectorDrawable Gradients – Part2

One of the frustrations of working with VectorDrawable is having to tell designers: “Sorry gradients aren’t supported” when they provide assets which contain gradients which we need to import as VectorDrawable. The options are to either remove the gradients, or to import bitmap images instead. However that has all changed and we now have gradient support in API24 and later. In this short series of posts we’ll take a look at how to use them.

In the previous post we looked at linear gradient support in VectorDrawable and found that all was good with the world, subject to running API 24 or later. However, things get a little more complicated if we use the other two kinds of gradients that are supported on Android: Radial and Sweep gradients. Let’s begin by taking a look at four different gradients that I created in Sketch. The top row are the two linear gradients that we looked at in the previous article, the bottom left is a radial gradient, and the bottom right is a sweep gradient. If I export these as a PNG it looks like this:

However if I also export this as an SVG and we open the SVG in Chrome we instantly see a problem:

Where did the sweep gradient go? The very simple explanation to that is that the SVG specification does not support sweep gradients – only linear and radial gradients. So although both Sketch and VectorDrawable support sweep gradients, the intermediate format that we need to use to bridge between them does not. There are a couple of ways to work around this. The simplest would be to use bitmap formats instead of VectorDrawable – we’ve already seen how Sketch can export to a bitmap format such as PNG without any problem. The second way, which requires a bit more effort, is to work closely with the designer, understand how they are trying to use a sweep gradient, pull the SVG across as a VectorDrawable, and then manually re-implement the sweep gradient.

A sweep gradient requires a centre point to be defined:

<?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="96dp"
  android:height="96dp"
  android:viewportHeight="100"
  android:viewportWidth="100">

  <path
    android:pathData="M1,1 H99 V99 H1Z"
    android:strokeColor="?android:attr/colorAccent"
    android:strokeWidth="2">
    <aapt:attr name="android:fillColor">
      <gradient
        android:centerX="50"
        android:centerY="50"
        android:type="sweep">
        <item
          android:color="?android:attr/colorPrimary"
          android:offset="0.0" />
        <item
          android:color="?android:attr/colorPrimaryDark"
          android:offset="1.0" />

      </gradient>
    </aapt:attr>
  </path>
</vector>

There is one small gotcha here which had me scratching my head for a while. The latest version of Android Studio (at the time of writing) is 3.1 Canary 7, and in this the gradient is not rendering correctly in the preview panel on my iMac Retina 5K:

I have included diagonal lines to show where the centre point should be. However if we actually render this on a device, then everything is as it should be. There’s a rendering bug in Android Studio:

For radial gradients things are slightly more complex, but don’t despair because there is a much simpler fix than for sweep gradients. Whilst a radial gradient renders correctly in Chrome, when we import it to Android Studio using the Vector Asset tool the radial gradient gets rendered as a flat fill:

If we look at radial fill within the vector itself, we can see a problem:

<path
  android:fillType="evenOdd"
  android:pathData="M10,155h135v135h-135z">
  <aapt:attr name="android:fillColor">
    <gradient
      android:centerX="-94.10123"
      android:centerY="77.5"
      android:gradientRadius="67.5"
      android:type="radial">
      <item
        android:color="#FF1E9618"
        android:offset="0.0" />
      <item
        android:color="#FF156912"
        android:offset="1.0" />
    </gradient>
  </aapt:attr>
</path>

The centre point of the gradient should be the exact centre of the path, however we can see how the X coordinate of this is positioned outside of the viewport, and the Y coordinate is outside the bounds of the pathData. So the radial gradient is in fact being rendered, just outside the area that we’re interested in. We can manually fix this by adjusting the coordinates of the centre:

<path
  android:fillType="evenOdd"
  android:pathData="M10,155h135v135h-135z">
  <aapt:attr name="android:fillColor">
    <gradient
      android:centerX="77.5"
      android:centerY="222.5"
      android:gradientRadius="67.5"
      android:type="radial">
      <item
        android:color="#FF1E9618"
        android:offset="0.0" />
      <item
        android:color="#FF156912"
        android:offset="1.0" />
    </gradient>
  </aapt:attr>
</path>

But it would be good to understand why this is happening as we may be able to find an easier solution to the problem if we do (Spoiler Alert: there is an easier solution!). Why does the VectorDrawable have this weird offset for the centre point? There’s a clue if we look at the raw SVG for this radial fill that is generated by Sketch:

<radialGradient id="gradients-b" fx="50%" fy="50%" gradientTransform="matrix(0 1 -1.04241 0 1.021 0)">
  <stop offset="0%" stop-color="#1E9618"/>
  <stop offset="100%" stop-color="#156912"/>
</radialGradient>

The issue appears to be that the Vector Asset tool is incorrectly applying the transformation matrix defined in gradientTransform for the default gradientUnits. I won’t do a deep dive here, but discussions of how these interact is detailed here. Judging by how this gets correctly rendered in both Chrome and the macOS Finder preview, it appears a fair assumption that it is the Vector Asset tool which is getting things slightly wrong. Of course tools other than Sketch may actually specify things differently and not confuse the Vector Asset tool so you may find that radial gradients are imported correctly from SVGs generated from other tools such as Adobe Illustrator. An issue has been raised for this here.

There is a very quick, hacky fix that we can do to resolve this without having to do any maths as we had to if we manually tweaked the centre point: Remove the gradientTransform element:

<radialGradient id="gradients-b" fx="50%" fy="50%">
  <stop offset="0%" stop-color="#1E9618"/>
  <stop offset="100%" stop-color="#156912"/>
</radialGradient>

If we do this and re-import the SVG using the Vector Asset tool then we get this:

<path
  android:fillType="evenOdd"
  android:pathData="M10,155h135v135h-135z">
  <aapt:attr name="android:fillColor">
    <gradient
      android:centerX="77.5"
      android:centerY="222.5"
      android:gradientRadius="67.5"
      android:type="radial">
      <item
        android:color="#FF1E9618"
        android:offset="0.0" />
      <item
        android:color="#FF156912"
        android:offset="1.0" />
    </gradient>
  </aapt:attr>
</path>

If we use this along with the sweep gradient which we manually created earlier we can now render all of our gradients correctly:

So although there are some issues that we’ll face if we need to use radial or sweep gradients, gradient support in VectorDrawable is now a reality and hopefully radial gradient import will be fixed before too long.

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.

3 Comments

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.