Android: Pannable and zoomable views-Part 1

Vinay Shenoy
Aug 8, 2017 · 9 min read

Note: This article assumes the reader has basic familiarity with drawing of custom views

Gestures are an often unused, but powerful mechanism to surface functionality in apps. When used correctly, they can make interacting with your app seem natural and intuitive. They also serve as a common language that users learn once, and can use anywhere when they see familiar patterns.

Let’s take a very common use case: Interacting with images. Take the Google Photos app for example, it lets you interact with images by using gestures like Pinch-To-Zoom and moving the zoomed photo around with your finger. Without those gestures, interacting with images would be a massive pain! You also never have to think about the gestures, because practically every photo app since the beginning of time has had them.

So how DOES one go about implementing gestures like panning and zooming, anyway?

Luckily, when it comes to images, Chris Banes’ excellent PhotoView library has you covered. Just plop it in, and you’re good to go!

But images are far from the only use case for gestures. There are many cases where we need to create custom views in our apps, and these custom views might benefit from taking advantage of gestures. This is exactly what I’ll be talking about in this multi-part series: How to implement multi-touch gestures in custom views.


Enter the Matrix

Before we get to the actual code, one of the most important concepts to be familiar with is the Matrix. A matrix is extensively used in computer graphics to represent and manipulate transformations of objects. But what is a matrix, exactly? This is what Wikipedia says:

In mathematics, a matrix (plural matrices) is a rectangular array of numbers, symbols, or expressions, arranged in rows and columns.

The Android framework already uses matrices in its UI framework, and it provides a useful class which does the heavy lifting and gives us a toolbox of methods. We will use it to work with basic properties which most developers are already familiar with: translation, scale and rotation.

Let’s explore some of the basic Matrix operations using some sample code!

Here’s a custom View that draws a Rect in the centre of the view, and draws a circle on top of that to mark the centre of the View. We’ll be applying our Matrix transformations on the rect.

If you take a look at line #83 in the code snippet above, that is where the actual magic happens. After initialising the content rect, we use the matrix to apply the transformations it encodes from the content rect to the mapped rect. The mapped rect is the one that is drawn to the canvas. If we run the app, this is how it looks:

Now that we have the basic setup, let’s move on to playing around with some of the Matrix operations!


I Like To Move It, Move It: Translation

Normally, if you want to move a Rect by some value, say dX and dY, you’d call rect.offset(dX, dY) and you’ll be done. Looking at the documentation for Matrix, we see that there are two similar APIs, preTranslate() and postTranslate which will let you achieve the same thing. Let’s go ahead and use preTranslate for now. We’ll come to the difference between the two APIs in a bit, when applying multiple transformations onto the same matrix.

Let’s move the rect to the right by half its width. If we have done this correctly, the left edge of the rect should pass through the centre of the centre marker.

contentTransformMatrix.preTranslate(content.width() / 2F, 0F);

Adding the above line before calling mapRect in onSizeChanged() gives us the following result:

Let’s add another translation to the Matrix now and move it down by half the rect’s height. This should make the top-left corner of the rect match the centre of the centre marker.

contentTransformMatrix.preTranslate(0F, content.height() / 2F);

Adding the above line below the previously added line gives us the following result (as expected):

Now, you might have noticed that we could have done the second translation as part of the first translation itself, but we did not. This was to illustrate a couple of things about the nature of Matrix operations:

  • All the pre and post methods of the Matrix class perform the current change while maintaining the transformation that the matrix already encodes, i.e, none of the previous transformations are lost(Unless you specifically choose to clear them)
  • The difference between the pre and post methods should be evident now, from the naming. All the pre variations will perform the new operation BEFORE the transformations the matrix already has, and then apply the transformation present in the matrix, whereas the post variants will apply the new operation AFTER the current transformation in the matrix
  • Depending on the transformations you are performing, pre or post may or may not make a difference. For example, performing two translations one after the other will have the same result regardless of whether you use preTranslate or postTranslate. But it can make a difference if your matrix includes scaling transformations as well, which brings me to the next section.

Honey, I Shrunk The Rect: Scaling

Scaling operations are dead simple. Since we’ve already used the preTranslate method earlier, let’s just go ahead and a method to scale the rect by 20%.

contentTransformMatrix.preScale(1.2F, 1.2F);

And that’s it! Scaling is done, right? Er, not quite…

The rect HAS become bigger, but it has also moved! Why would that happen? The reason is because we have not provided a reference point to the Matrix around which to perform the scale. The point is referred to as the Pivot. This point remains unchanged after performing matrix operations (except translations).

Let’s go ahead and select the top-left corner of the rect as the pivot for the scale operation. When the scale operation is applied, what we expect is that the top-left corner remains coinciding with the centre marker, but the rect itself is 20% larger.

We could use the left and top properties of the content rect, but keep in mind that we have already applied translations on the matrix. The pivot point we provide has to take into account the current transformation on the matrix.

So what we need to get is the left and top properties of the rectangle AFTER the translation is applied, but BEFORE the scale is applied. We have already used this method earlier to apply the matrix to the content rect. Let’s use it now and get the left and top properties to use as the pivot.

contentTransformMatrix.mapRect(mappedContent, content);
contentTransformMatrix.preScale(1.2F, 1.2F, mappedContent.left, mappedContent.top);

Let’s run this and see what we get!

Close, but not yet! Why does the top-left corner not coincide with the centre marker?

The reason is quite simple; We have translated the rect by half its width and height along X and Y, but after scaling, the actual width and height of the rect are 20% larger than they were before scaling, so obviously the translation values do not take the updated scale into account. What are our options to fix this?

Well, we could translate it again, but that is pretty cumbersome. Let’s look at another option. Remember earlier, when we talked about how pre and post operations are done with respect to the matrix’s current transformation? This is when this comes in handy.

We have used a preScale operation which will perform the scaling first, and then apply the translation values which the matrix has. Instead of using preScale, if we use the postScale operation instead, the translation will also be scaled appropriately and we should end up with the correct result. Let’s make the change and see what we get!

contentTransformMatrix.postScale(1.2F, 1.2F, mappedContent.left, mappedContent.top);

Phew! We finally got what we expected!

Before we move on to the third and final operation, let’s take a note of the important points about this section.

  • Whenever you do matrix operations, ALWAYS consider the pivot. Selecting the right pivot is crucial, and is something that often trips up people who begin working with matrixes
  • Pivots need to take the current transformation of the matrix into account. Using one of the map functions of the Matrix class can be very helpful in calculating pivot points when chaining operations
  • Choosing pre or post methods to perform operations can affect the final matrix. If your transformation doesn’t come out as expected, recheck the operations you’re using and confirm that the operations are selected correctly

Done? Let’s move on to the last and final part!


You Spin Me Round: Rotation

Armed with our new-found knowledge about pivots and pre vs post methods, we’ll just use the handy postRotate method, again using the top-left corner of the content rect as the pivot to add a clockwise rotation of 10 degrees.

contentTransformMatrix.mapRect(mappedContent, content);
contentTransformMatrix.postRotate(10F, mappedContent.left, mappedContent.top);

Let’s run this, and…. dang it! What’s wrong now?

This is completely unexpected! What could be the problem?

The problem lies with the fact that we’re mapping the content to another Rect and drawing it. Let’s take a look at the documentation of the mapRect method

Apply this matrix to the src rectangle, and write the transformed rectangle into dst. This is accomplished by transforming the 4 corners of src, and then setting dst to the bounds of those points.

Ah, so that’s the problem. A rect is always rectangular, and cannot be rotated. mapRect will transform the points, then use those transformed points to create a new rect that will encompass all those four points.

Now, there are a couple of ways to do what we need to. One way is to skip adding the rotation in the matrix and do the rotation on the canvas. But that is a topic for part 2 of this article, and defeats the purpose of using a single Matrix for transformations. The other way is to change what we’re drawing instead, which brings us to this really useful class in the Android graphics framework called Path.

The intricacies of Path drawing are out of the scope of this article, but suffice it to say that using a Path will allow us to compose something to draw out of smaller building blocks. Plus, it also has a transform(Matrix) method that will allow us to apply a Matrix to the Path.

We’ll need to make a couple of changes to the end of our onSizeChanged method to initialise the Path we’re going to draw with our content rect.

drawPath.reset();
drawPath.addRect(content, Path.Direction.CW);
drawPath.transform(contentTransformMatrix);

Let’s also update our onDraw method to draw the Path instead of the mapped Rect. Make sure you remove the call to the previous drawRect since we aren’t using it anymore.

canvas.drawPath(drawPath, contentPaint);

Let’s run this and see what we get!

Alright! Looks like we’re done with rotation as well! Let’s crack open a beer!


What Next?

You can take a look at the final MatrixViewTest class HERE. I urge you to play around with matrices and paths a little and get a feel for what you can achieve using them since this article barely scratches the surface.

In part 2 of this series (coming soon), we’ll build a simple app that uses matrices to build a app that implements panning and zooming in a custom view! Follow me (the link is right up at the top of this page) to get notified when I post something new.

Ciao

Vinay Shenoy

Written by

RPG Gamer, Rock & Metal lover, Android developer, Avid reader, foodie and tech enthusiast.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade