ConstraintLayout / VectorDrawable

Dynamic VectorDrawable Sizing

A few weeks ago I was chatting with Kaushik Gopal (half of the excellent Fragmented Podcast team) and he raised an interesting yet quite tricky question: Is there a recommended way of resizing VectorDrawables programmatically. I must confess that I did not know the answer to this but it struck me as a useful thing to be able to do, so I endeavoured to discover some tricks for doing this.

My initial thoughts were to look at the public Java APIs for VectorDrawable to see if anything could be done there. For those that are familiar with the VectorDrawable Java API it will come as no surprise that the options here are somewhat limited. VectorDrawable is designed to be inflated from XML and there are no public APIs for manipulating the contents of a VectorDrawable. Whilst it might be possible to subclass VectorDrawable and override getInstrinsicWidth() and getIntrinsicHeight() to override the size that the VectorDrawable will return during the measurement pass, this felt somewhat hacky and it would be quite messy to have to manually wrap each VectorDrawable inside the subclass. However, this did lead me to look at Alex Lockwood’s Kyrie library which is a superset of VectorDrawable and AnimatedVectorDrawable and this is certainly a great option if you’re looking at programmatically changing VectorDrawable and AnimatedVectorDrawable object. I happened to be at a conference with Alex at the time, and he suggested simply hosting the VectorDrawable inside an ImageView and using appropriate scaleType values to dynamically size the vectors in exactly the same was as we can scale bitmaps. I am extremely grateful to Alex for steering me towards a much simpler path to solving this – I was looking at a far more complicated approach and ignoring the power that we already have within Android layout framework.

It will come as no surprise to many that I immediately turned to ConstraintLayout as being the most powerful and flexible way of layout management and in doing so discovered that it was not necessary to use scaleType on a view to dynamically scale a VectorDrawable – just creating a weighted chain and then applying a dimension ratio enables us to proportionally scale a VectorDrawable based on a function of the layout.

In this case we have two ImageViews containing different VectorDrawables in a weighted chain with each ImageView given an equal weight. This effectively divides the available space in half. The first uses the intrinsic height of the VectorDrawable by specifying android:layout_height="wrap_content" and the second uses a proportional height by specifying android:layout_height="0dp" and then applying a dimension ratio calculate the height based upon the width app:layout_constraintDimensionRatio="H,1:1":

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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/activity_main"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <ImageView
    android:id="@+id/imageView1"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    android:contentDescription="@null"
    app:layout_constraintBottom_toBottomOf="@id/imageView2"
    app:layout_constraintEnd_toStartOf="@+id/imageView2"
    app:layout_constraintHorizontal_chainStyle="spread"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@id/imageView2"
    app:srcCompat="@drawable/ic_size_1" />

  <ImageView
    android:id="@+id/imageView2"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_margin="8dp"
    android:contentDescription="@null"
    app:layout_constraintDimensionRatio="H,1:1"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toEndOf="@+id/imageView1"
    app:layout_constraintTop_toTopOf="parent"
    app:srcCompat="@drawable/ic_size_2" />
</android.support.constraint.ConstraintLayout>

We can use Layout Inspector in Android Studio to verify that both ImageViews have the same width, and the first is clearly being rendered at its intrinsic size of 24dp x 24dp, whereas the second is being scaled so that its width fills the available space, and the height is scaled in unison thanks to the dimension ratio that we specified:

That might seem like we have solved the problem but actually this only covers cases where the width and height of the VectorDrawable are identical – in other words the aspect ratio of the VectorDrawable is 1:1. Let’s drop in a third ImageView containing a VectorDrawable with an aspect ratio of 5:4 (its intrinsic dimensions are 50dp x 40dp):

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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/activity_main"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <ImageView
    android:id="@+id/imageView1"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    android:contentDescription="@null"
    app:layout_constraintBottom_toBottomOf="@id/imageView2"
    app:layout_constraintEnd_toStartOf="@+id/imageView2"
    app:layout_constraintHorizontal_chainStyle="spread"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="@id/imageView2"
    app:srcCompat="@drawable/ic_size_1" />

  <ImageView
    android:id="@+id/imageView2"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_margin="8dp"
    android:contentDescription="@null"
    app:layout_constraintDimensionRatio="H,1:1"
    app:layout_constraintEnd_toStartOf="@id/imageView3"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toEndOf="@+id/imageView1"
    app:layout_constraintTop_toTopOf="parent"
    app:srcCompat="@drawable/ic_size_2" />

  <ImageView
    android:id="@+id/imageView3"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_margin="8dp"
    android:contentDescription="@null"
    app:layout_constraintDimensionRatio="H,1:1"
    app:layout_constraintBottom_toBottomOf="@id/imageView2"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toEndOf="@+id/imageView2"
    app:layout_constraintTop_toTopOf="@id/imageView2"
    app:srcCompat="@drawable/ic_size_3" />
</android.support.constraint.ConstraintLayout>

Layout Inspector shows that this is getting distorted to having a 1:1 ratio:

If we know the aspect ratio of the VectorDrawable then it is easy enough to set the dimension ratio accordingly:

  <ImageView
    android:id="@+id/imageView3"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_margin="8dp"
    android:contentDescription="@null"
    app:layout_constraintDimensionRatio="H,5:4"
    app:layout_constraintBottom_toBottomOf="@id/imageView2"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toEndOf="@+id/imageView2"
    app:layout_constraintTop_toTopOf="@id/imageView2"
    app:srcCompat="@drawable/ic_size_3" />

This now maintains the correct aspect ratio of the image, but does highlight a bug in ConstraintLayout whereby the sizes of the other ImageViews get altered by making this change:

Although this specific bug makes this particular implementation somewhat impractical at the moment, the actual principle that we’re basing things on is still sound – use the layout to manage the dimensions of the image rather than relying on the intrinsic width and height of the VectorDrawable. For example using fixed dimensions in the layout_width & layout_height attributes would not be as responsive to the layout size changing, but would still permit some external control of the VectorDrawable size.

Once this bug is resolved, specifying a dimension ratio will work for many cases, but there may be occasions where the aspect ratio may not be known at compile time. In this case we can determine and specify the dimension ratio programmatically based upon the intrinsic width & height of a Drawable:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        ConstraintSet().apply {
            clone(activity_main)
            imageView3.drawable.also { drawable ->
                setDimensionRatio(
                        R.id.imageView3, 
                        "v,${drawable.intrinsicWidth}:${drawable.intrinsicHeight}"
                )
            }
            applyTo(activity_main)
        }
    }
}

The results of this are identical to doing this manually in the layout including the same ConstraintLayout bug, but it can be useful for cases where the aspect ratio of the image is not known until run time.

This example only performs this change once when the layout has been inflated. If you need to change the image within an ImageView then you will need to perform this operation whenever the image changes.

Aside from the ConstraintLayout issue this gives us a really nice mechanism for dynamically sizing our VectorDrawables based upon the layout.

Once again, huge thanks to Kaushik for asking the question which prompted this article, and to Alex for steering me on to a much more sensible path.

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.