Use the BlindsView effect from the Xperia™ lockscreen

On recent Xperia™ devices, you may have noticed our new and eye-catching Lockscreen which transforms into horizontal blinds when your finger touches the screen. Now we’ll show you how you could add a similar graphical effect to your own app using a custom Android™ ViewGroup. The BlindsView Tutorial will provide you with powerful tools for creating some very eye-catching graphical effects and transitions. Read on for the full BlindsView tutorial by Johan Henricson, software project manager at Sony.

Johan Henricson
Johan Henricson, Software Project Manager at Sony.

The BlindsView effect – the foundation for the new Xperia™ lockscreen
Today I want to share with you some guidance on how to create your own, similar BlindsView effect using nothing but a custom Android View, and some clever hacks to it. After studying the tutorial, I hope you will have gained some inspiration and valuable insights regarding how you can create some very eye-catching effects and transitions by manipulating views, or parts of them. After reading, you should have all the tools you need to create your own blinds effect, spectacular activity transition, a “Flipboard”-like function, or any other new and creative feature that you can think of.

Background of the BlindsView feature
The concept of the blinds started out as an idea from our designers and evolved with the development team. Compared to the previous lockscreen, we wanted to make better use of the great HD display but also make it easier for the user to unlock the phone without putting too much focus on how to unlock.

We removed the slide to unlock-slider, all notifications (which were made accessible from the status bar instead), and the vignette we used to apply before. We wanted to keep, but touch up, the easy-access music controls, the fast-capture camera launcher, as well as the clock.

New Xperia lockscreen showing music and camera icons, clock, and blinds features.

New Xperia™ lockscreen showing music and camera icons, clock, and blinds features.

Removing all the stuff really allows the great display to shine, letting the user show off his or her own pictures in great clarity and size on the lockscreen. When the user touches the screen and moves a finger around, the picture splits into pieces, giving a feeling of looking underneath and inside of the phone. We thought the effect resembled window blinds or venetian blinds being pushed around – hence the name.

In technical terms, we needed a ViewGroup that would let developers to put the lockscreen content in it, as they would in any layout, and not be concerned with the shredding into blinds. The ViewGroup would have to allow its child items behave and react to touch just as they normally would, but when it was touched outside of any child Views’ bounds, it would immediately switch into BlindsView mode and show the blinds as the finger is moved around. The result was a very playful one, and even now, I can’t help but mess around with the blinds on my Xperia™ ZL during the day, even when I should be focused on some important meeting.

Tutorial prerequisites and preparations
The tutorial assumes some basic knowledge of implementing custom Android Views and apps, but nothing too advanced. You’ll also find the finished project containing all source code as well as a reference app available for download. All code and the reference app is released under BSD 3 license. I should also clarify that we will not be recreating the complete, production quality lockscreen in this tutorial. There are things that can be optimised and developed further than what is shown in this example. You should see the tutorial as a proof of concept, highlighting some useful graphical tips and tricks.

Before you go about creating your own lockscreen implementation, it’s good to have as clear a view as possible of what it is you want to achieve. You might want to start by downloading and running the lockscreen apk sample or play around with the Xperia™ lockscreen to understand what it is you’re aiming for.

Example end result of BlindsView tutorial.

Example end result of BlindsView tutorial.

Getting started
For convenience, I’ll start out from a handy little project I created called PlainCustomViewGroup, which you can download from this article. If you’re anything like me, you’re a tad lazy and prefer spending your precious time doing fun and visible stuff rather than doing the same basic setup over and over. So after having set up one too many projects containing nothing but a basic custom View and a reference Activity to sport it, I took the time to create a few basic projects, or boilerplates if you will, which I use as baselines for new projects and prototypes.

So, let’s start by creating a new Android project from the existing source PlainCustomViewGroup, and rename it BlindsViewTutorial. After you build and run the project, you should be able to produce an app that looks like this:

PlainCustomViewGroup example.

PlainCustomViewGroup example.

Logcat should give the following printout:

D/PlainCustomViewGroup(1701): onLayout         (assign sizes and locations to all children)
D/PlainCustomViewGroup(1701): onLayout         (assign sizes and locations to all children)
D/PlainCustomViewGroup(1701): dispatchDraw     (dispatching draw calls to all children)
D/PlainCustomViewGroup(1701): drawCustomStuff  (doing the custom drawing of this ViewGroup)

Take a moment too look through the base code to make sure you understand and agree with it.

When we’re all done, the BlindsView project will be quite demanding, and to make use of the powerful hardware in the Xperia™ Z and Xperia™ ZL, we’re going to want the application to use hardware acceleration. To let Android know, we’ll add the following to the application tag in AndroidManifest.xml:

android:hardwareAccelerated ="true"

It’s good to do this at this early stage, since it puts some constraints on what methods we can use when drawing later.

Support for this attribute was first introduced in Android 3.0 (Honeycomb, API level 11), so we add that as well to the manifest:

<uses-sdk android:minSdkVersion ="11" />

The hardware acceleration is the only reason to require API level 11, so if you want to support older versions, the rest of the tutorial still applies. You’ll just have to do without the hardware acceleration. If you set minSdkVersion to 14 or greater, hardware acceleration is enabled by default and nowadays it’s quite useful, so that’s also an option.

The last thing we need to do before moving on to the good stuff is to rename all instances of PlainCustomVievGroup to something more suitable (pretty soon, it won’t be so plain), such as “BlindsView”. Eclipse’s rename function (Right-click, Refactor, Rename or Alt+Shift+R) is, of course, a life-saver for this.

Transforming the BlindsView project
Okay, now we’re ready to start the transformation from the generic template project into something real and valuable. Let’s begin by adding a more inspiring background image than the plain circle. We will clear out the drawCustomStuff method and fill it with the following:

        private Bitmap mUndistortedBitmap ;         private Canvas mUndistortedCanvas;
        private Drawable mBgDrawable ;

        private void drawCustomStuff(Canvas canvas) {
                if (LOG_ON ){
                        Log. d(LOG_TAG,
                                "drawCustomStuff (doing the custom drawing of this ViewGroup)");
                }

                mUndistortedBitmap = Bitmap.createBitmap(getWidth(), getHeight(),
                        Bitmap.Config. ARGB_8888);
                mUndistortedCanvas = new Canvas(mUndistortedBitmap );

                if (mBgDrawable != null) {
                        mBgDrawable.draw(canvas);
                }
        }


Of course, you can use whatever image you want. I will use an image of a has-been dandelion (which you may recognise as a Sony background image shipped with our Xperia™ phones) since it has some nice colours in it and includes both bright, dark and mid-tones. This is useful to make sure our solution works well in as many cases as possible, and to verify that the brights and darks don’t disappear. You can find this image embedded in the attached project. The image of your choice should be popped into a drawable folder to be fetched when needed. To avoid hogging more memory than necessary, I’ll use a large version (1140×1280) only for large devices (it goes into a res/drawable-large folder) and a smaller one (720×640) as default (put in the res/drawable folder). That should cover both portrait and landscape cases for most devices.

Since we’ll want to take care of the drawing of the background ourselves, we also need to override the setBackground method and keep the bg drawable as a member. We’ll also add a few lines to centre the image on screen:

        public void setBackground(int id) {
                mBgDrawable = (BitmapDrawable) getResources().getDrawable(id);
                centerBgDrawable();
        }

        @Override
        public void setBackground(Drawable background) {
                mBgDrawable = (BitmapDrawable) background;
                centerBgDrawable();
        }

        private void centerBgDrawable() {
                if (mBgDrawable != null) {
                        final DisplayMetrics dm = getResources().getDisplayMetrics();
                        mBgDrawable.setTargetDensity(dm);
                        mBgDrawable.setGravity(android.view.Gravity.CENTER);
                        mBgDrawable.setBounds(0, 0, dm.widthPixels , dm.heightPixels);
                }
                postInvalidate();
        }

And finally we will call the new public method from the Activity:

        final BlindsView blindsView = (BlindsView) findViewById(R.id.blindsview );
        blindsView.setBackground(R.drawable. dandelion);

This may seem like a blunt way of selecting the appropriate bg resolution and positioning it appropriately on screen but it’s more than enough for this purpose. Also, at this point, it will seem a bit ineffective to draw in this fashion and the naming doesn’t quite make sense yet, but don’t worry – it will all be clear in a minute. Hang in there!

Dandelion background image from Xperia™ smartphone.

Dandelion background image from Xperia™ smartphone.

Handling layout
Now is a good time to pop some Views into the ViewGroup so that we can see how they look while we fiddle with the drawing of the group later. You can handle the laying out of your child Views to suit your specific needs in the onLayout method but in most cases it will probably be more convenient to piggyback on one of Android’s stock layout classes. In this example, I will use the simple LinearLayout so I’ll extend that instead of ViewGroup:

        public class BlindsView extends LinearLayout

Since I trust LinearLayout to do the layout on its own, I will also remove the onLayout method that came with the template project. This is a quite convenient approach and is worth elaborating on a little bit: it should be noted that throughout this tutorial, we only override methods originating from View or ViewGroup. For all intents and purposes, the BlindsView can be considered to extend ViewGroup, but to be able to reuse the layouting and other behaviour from Android’s stock layouts, it’s more convenient to extend one of them. So if you prefer, say a RelativeLayout, all you need to do is change this line into …extends RelativeLayout instead.

Now, let’s add some content to the BlindsView. Since we now extend LinearLayout, we can add them in xml and even use LinearLayout attributes such as Orientation on the BlindsView. In my example, I will add three Textviews and two Buttons stacked on top of each other, but as far as the layouting is concerned, this is a LinearLayout like any other and you can put absolutely anything you want in it. We should now have a layout file looking something like this:

<com.sonymobile.ui.BlindsView.BlindsView xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/blindsview"
 android:layout_width= "match_parent"
 android:layout_height= "match_parent"
 android:gravity= "center"
 android:orientation= "vertical" >
 <TextView .... />
 <TextView .... />
 <TextView .... />
 <Button .... />
 <Button .... />
 </com.sonymobile.ui.BlindsView.BlindsView >

To be able to see the children, we will also need to adjust the drawing order. In the dispatchDraw method, we call super.dispatchDraw to draw the children onto the Canvas, then draw our own custom stuff (for exampe, the background image) on top of it. So let’s move the following call to the very end of drawCustomStuff:

super.dispatchDraw(canvas);

To improve code readability, let’s also extract this undistorted background and child drawing into a little method of its own. Since we will be drawing either in a transformed fashion, when touched, or in the usual plain way when not touched, let’s name it drawUndistorted to distinguish between the two cases:

        private void drawUndistorted(Canvas canvas) {
                if (mBgDrawable != null) {
                        mBgDrawable.draw(canvas);
                }
                super.dispatchDraw(canvas);
        }

That should do it – you should now be able to see the TextViews and buttons sitting on top of the flower and the seeds, looking something like this:

BlindsView with buttons.

BlindsView dandelion screen with TextViews buttons.

Logic and mechanism behind BlindsView behaviour
Now, that’s all fine but so far we’ve done little more than create a more complex and slightly worse performing version of the LinearLayout. Let’s go to work with the logic for the Blinds that we’re aiming for! The basic idea is to perform the undistorted draw (that is, without interfering with the stock drawing mechanism) to a Bitmap and store that Bitmap. Then, when we want to apply the blinds effect (when the user is touching an empty or touch insensitive space in the layout), we will reuse the stored image, apply a bunch of transforms to it and draw the transformed version onto the screen.

To achieve this, we’ll need to store a snapshot of the original, undistorted version of the ViewGroup, hence the mUndistortedBitmap and mUndistortedCanvas we created earlier.

In pseudo code, this gives us a draw pass looking as follows:

        draw undistorted to undistorted canvas and store it
        if(blinds mode)
                draw transformed version of the undistorted bitmap to screen canvas
        else
                draw undistorted version to screen canvas

This has to be done every time the current drawing is invalidated, which translates into “on every single frame” if we are in the blinds mode, since it needs to react to the finger being moved around. Doing two draw passes (undistored and transformed) rather than one (undistorted or transformed) effectively cuts performance in half, which is obviously unreasonable. Therefore, we need to make sure not to redraw the original bitmap more often than we have to (most of the time nothing will have changed between two frames at a 60fps drawing rate). We will change the logic to only run the undistorted draw calls on the first frame of the blinds mode or when a new bitmap is needed or when not in blinds mode. That gives us the following structure:

        if( !blinds mode  ||  ( blinds mode && new undistorted bitmap needed )  )
                if(!blinds mode)
                        draw undistorted to screen canvas
                else
                        draw undistorted to undistorted canvas and store it
        if(blinds mode)
                draw transformed version of undistorted bitmap to screen canvas.

The “new undistorted bitmap needed” condition is typically only true the very first time, to initiate the bitmap, but it will also come in handy when, for some reason, you need to tell the view to refresh its undistorted bitmap (if you know one of the child views changed, for example).

At this first stage, let’s assume we are always in BlindsView mode and that we always need to re-initialize the undistorted Canvas and Bitmap. That gives the following drawCustomStuffdrawUndistorted and drawBlinds methods:

        private void drawCustomStuff(Canvas screenCanvas) {
               if (LOG_ON ) {
                       Log. d(LOG_TAG,
                               "drawCustomStuff (doing the custom drawing of this ViewGroup)");
               }
               final boolean isInBlindMode = true;
               final boolean initBmpAndCanvas = true;
               if (!isInBlindMode || (isInBlindMode && initBmpAndCanvas)) {
                       // Draw normally
                       if (isInBlindMode && initBmpAndCanvas) {
                               mUndistortedBitmap = Bitmap.createBitmap(getWidth(), getHeight(),
                                       Bitmap.Config. ARGB_8888);
                               mUndistortedCanvas = new Canvas(mUndistortedBitmap );
                       }
                       Canvas canvasToDrawTo = isInBlindMode ? mUndistortedCanvas : screenCanvas;
                       drawUndistorted(canvasToDrawTo);
               }
               if (isInBlindMode) {
                       // Draw blinds version
                       drawBlinds(screenCanvas);
               }
        }
        private void drawUndistorted(Canvas canvas) {
        Log. d( LOG_TAG, "Performing undistorted draw" );
                if (mBgDrawable != null) {
                       mBgDrawable.draw(canvas);
        }
        super.dispatchDraw(canvas);
        }
        private void drawBlinds(Canvas canvas) {
                // FIXME: Draw transformed, not undistorted!
        Log. d( LOG_TAG, "Performing draw in blinds mode (well, not really, but it will be!)" );
                drawUndistorted(canvas);
        }

As you can see, we have yet to implement the transformed drawing but to show something on screen, for now we will route the call to use drawUndistorted. This should look the same as before on screen and the flow can be verified by looking at the logcat output.

Creating the custom drawing
Now it’s time to get started on the actual blinds. To represent the set of blinds, we will create the following items:

  • A new class called BlindInfo to hold data and state related to a single blind.
  • An ArrayList called mBlindSet within the BlindsView holding BlindInfo for each and every blind.
  • A method called setupBlinds for setting up the model.

In this example, all blinds are assumed to have the same width (full-screen) and height, and the setupBlinds method will look like this:


        private ArrayList<BlindInfo> mBlindSet = null;
        private void setupBlinds (int blindHeight) {
                if (blindHeight == 0) {
                        throw new IllegalArgumentException("blindHeight must be >0");
                }
                ArrayList<BlindInfo> bi = new ArrayList<BlindInfo>();
                int accumulatedHeight = 0;
                do {
                        bi.add( new BlindInfo(0, accumulatedHeight, getWidth(),
                                        accumulatedHeight + blindHeight));
                        accumulatedHeight += blindHeight;
                } while (accumulatedHeight < getHeight());
                mBlindSet = bi;
        }

We also need to define how high a Blind should be. You will probably want different pixel values on different devices with various screen resolutions and physical sizes. A convenient way of doing this is to define a value in density independent pixels (dp). There are other approaches that may work well in various applications. For example, you could assign various heights for different screen sizes or resolutions, or you could define it as a fraction of the screen height. It all depends on your demands, really. I will define a height value of 37 dips. So I will create a file named dimens.xml and add the following to it:

        <? xml version ="1.0" encoding= "utf-8" ?>
        < resources>
        <dimen name= "blindHeight"> 37dp </dimen >
        </ resources>

As you can see, in order for this method to be able to do its job, the BlindsView must have had its height and width assigned. A good time to call it will be immediately after a size is assigned or changed, so we will  implement onSizeChanged and use it to initialize a new set of blinds (using the scaled height we just defined) and replace any existing one with it:


        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
                super.onSizeChanged(w, h, oldw, oldh);

                setupBlinds((int) getResources().getDimension(R.dimen.blindHeight));
                if (LOG_ON ) {
                        Log. d(LOG_TAG,
                                "onLayout. Layout properties changed - blinds set rebuilt. New set contains "
                                + mBlindSet.size() + " blinds");
                        }
        }

At this point, a single blind is defined by its bounds so the BlindInfo class will hold the bounds and some getter methods. Also, at this point, we could just as well have used a Rect, but more fields will be added later:


/**
 * Dumb, stateless data holder describing a Blind, as used by BlindsView.
 */
public class BlindInfo {
        private final Rect mBounds;
        public BlindInfo(int l, int t, int r, int b) {
                mBounds = new Rect(l, t, r, b);
        }
        public int getHeight() {
                return mBounds .height();
        }
        public int getWidth() {
                return mBounds .width();
        }
        public int getLeft() {
                return mBounds .left ;
        }
        public int getRight() {
                return mBounds .right ;
        }
        public int getTop() {
                return mBounds .top ;
        }
        public int getBottom() {
                return mBounds .bottom ;
        }
}


The logcat should now have the following output:

D/BlindsView(2259): onLayout. Layout properties changed – blinds set rebuilt. New set contains 16 blinds

When the app is started, setting a breakpoint after the call to setupBlinds also allows you to take a look at the blinds data we just created.

Starting the drawing
Now we can start the drawing, creating something cool on the screen rather than just in the logs –because at the end of the day, that tends to impress people more. This is a big step, so hang in there. To start off, we will add a drawBlind method and make sure to call it once for each and every Blind:


        private void drawBlinds(Canvas canvas) {
                // Draw each blind in order, starting from the top one
                for (BlindInfo blind : mBlindSet) {
                        drawBlind( blind, canvas);
                }
        }

But first things first– the implementation of drawBlind is where the sweetness will happen. It reads a part of the undistorted bitmap (a Rect named src) corresponding to the particular blind, manipulates it, and then draws it onto the desired part of the target canvas (another Rect called dst). At this point, src and dst could well have been one and the same, since a blind will occupy the same space on the screen after it’s transformed, as in it does in the original drawing. But we are going to manipulate the blinds by transforming the Canvas in a minute. Therefore we translate the target Canvas to have its origin (0,0) coincide with the dead centre of the blind that’s currently being drawn. Therefore, src pinpoints a slice of the original bitmap while the dst always targets a Rect centered in (0,0).

This may take a minute to wrap your head around if you’re not used to custom drawing but it’s a convenient and common practice to move your canvas, do your drawing in translated coordinates, then restore the canvas to its original position. In code, this is:


        private void drawBlind(BlindInfo info, Canvas canvas) {
                // Info
                final int width = info.getWidth();
                final int height = info.getHeight();
                final int coordX = info.getLeft();
                final int coordY = info.getTop();

                // TODO: Temp. Used to be able to see onscreen where different blinds
                // begin and end.
                final int alpha = (int) (255 * (0.5d + 0.5d * Math.random()));
                mBlindPaint.setAlpha(alpha);

                canvas.save();
                canvas.translate((coordX + (width / 2f)), (coordY + (height / 2f)));

                final Rect src = new Rect(coordX, coordY, (coordX + width),
                        (coordY + height));
                final RectF dst = new RectF(-(width / 2f), -(height / 2f), width / 2f, height / 2f);
                canvas.drawBitmap( mUndistortedBitmap, src, dst, mBlindPaint );

                canvas.restore();
                if (LOG_ON ) {
                        Log. d(LOG_TAG, "Drew blind with size " + width + " by " + height
                                + " px and alpha: " + alpha + " at coordinates " + coordX
                                + ", " + coordY);
                }
        }

As you can see, an alpha parameter was added. This gives the Blind a randomised opacity between 127 (semi-transparent) and 255 (fully opaque) and is just to illustrate the effect of drawing in separate parts.

To do the drawing, we also need a Paint to define what the outcome will look like. There’s no need to create a new Paint for every frame, so we will keep one as a member and initialize it in init():

        private Paint mBlindPaint;
        private void init() {
                ...
                mBlindPaint = new Paint();
                mBlindPaint.setStyle(Paint.Style.FILL);
                mBlindPaint.setAntiAlias(true);
                mBlindPaint.setFilterBitmap(true);
        }

Now you should be able to run the app and get it looking like this:

Blinds with randomised opacity.

Blinds with randomised opacity.

And the logs should confirm what’s going on by printing out:

D/BlindsView(3846): onLayout. Layout properties changed – blinds set rebuilt. New set contains 16 blinds
D/BlindsView(3846): dispatchDraw     (dispatching draw calls to all children)
D/BlindsView(3846): drawCustomStuff  (doing the custom drawing of this ViewGroup)
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 230 at coordinates 0, 0
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 178 at coordinates 0, 50
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 129 at coordinates 0, 100
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 221 at coordinates 0, 150
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 252 at coordinates 0, 200
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 210 at coordinates 0, 250
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 195 at coordinates 0, 300
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 145 at coordinates 0, 350
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 214 at coordinates 0, 400
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 219 at coordinates 0, 450
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 175 at coordinates 0, 500
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 223 at coordinates 0, 550
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 149 at coordinates 0, 600
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 159 at coordinates 0, 650
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 251 at coordinates 0, 700
D/BlindsView(3846): Drew blind with size 480 by 50 px and alpha: 130 at coordinates 0, 750

Note that rotating your device will show you how the view layouts perform and create new blinds.

Pretty neat, we finally have something to show for our efforts! This latest step is quite a big one and having completed it provides a good time to reflect upon what we have accomplished. So far, we’ve created a custom view, filled it with content and put together a mechanism that uses a stock Android layout to do the layouting and drawing. Then we have taken this drawing, shredded it into pieces which we have applied individual transformations (the alpha change) to and finally we have drawn them on screen. This pretty much completes the structure and makes it reasonably easy to add more transformations, alter the drawing, and change the appearance. And that’s exactly what we will do in the remaining steps; apply more transformations, then control how the transformations are applied and optimise so that we don’t have to draw twice on every frame.

Applying and optimising transformations – tilting and lighting
Tilting

To give the blinds a tilted effect, let’s start with the model part. We will add x, y and z rotation variables (representing rotation around the x, y, and z axis, respectively) to the BlindInfo set of properties:

        private float mRotationX, mRotationY , mRotationZ;

        public void setRotations(float xRotation, float yRotation, float zRotation) {
                mRotationX = xRotation;
                mRotationY = yRotation;
                mRotationZ = zRotation;
        }

        public float getRotationX() {
                return mRotationX ;
        }

        public float getRotationY() {
                return mRotationY ;
        }

        public float getRotationZ() {
                return mRotationZ ;
        }

Back in the drawBlind method in BlindsView, we will read the params:

        final float xRotation = info.getRotationX();
        final float yRotation = info.getRotationY();
        final float zRotation = info.getRotationZ();

To give them a quick and dirty non-zero value, let’s also add a simple loop to the end of onLayout (just after the set has been created) that just goes through all blinds and assigns each of them an x rotation spanning from -55 to +55 degrees:

        // FIXME: Assign properly where appropriate
                for (int i = 0; i < mBlindSet.size(); i++) {
                float x = (-55f + (float)i / ((float)mBlindSet.size() - 1f) * 110f);
                mBlindSet.get(i).setRotations(x, 0f, 0f);
        }

To give the visual impression of the tilt, let’s make use of the Camera class. We create a Camera instance to keep as a member:

        private final Camera mCamera = new Camera();

Then we make use of the instance in the drawBlind method. First, we need to make sure to save and restore its state before and after any transformations are applied, just like we do with the canvas:

        (mCamera.save(); mCamera.restore();).

This ensures that we have a well defined state to start from, on every frame and for every blind.

Applying the tilt transformation to the Camera is as easy as:

        mCamera .rotateX(xRotation);

What this means is that we have taken a Camera and told it to rotate around the x axis to get a tilted projection of the blind we are currently working with. Now, we want the camera to tell us what that looks like, that is what transformation we need to apply to our Canvas to give us that same view. Before hardware acceleration was implemented, that would have meant calling:

        mCamera.applyToCanvas(canvas)

But in this case, that’s a NOP (No Operation), meaning we need to work around it by pulling the Matrix from the Camera and concatenating it with the Canvas’ existing one:

        Matrix cameraMatrix = new Matrix();
        mCamera.getMatrix(cameraMatrix);
        canvas.concat(cameraMatrix);

So let’s add these lines before the drawing takes place. Now, run the app and pat yourself on the back as you behold the semi-transparent trapezoidal wonder that you have created!

Now that we can see it’s working, let’s remove the random alpha setting in drawBlind. If you want alpha adjustments of your blinds, keep it to the BlindInfo class instead in exactly the same way we did with rotations. Now we should have a screen looking like this:

Blinds with tilted effect.

Blinds with tilted effect.

Your drawBlind method should now look similar to this:

        private void drawBlind(BlindInfo info, Canvas canvas) {
                // Read params
                final int width = info.getWidth();
                final int height = info.getHeight();
                final int coordX = info.getLeft();
                final int coordY = info.getTop();
                final float xRotation = info.getRotationX();
                final float yRotation = info.getRotationY();
                final float zRotation = info.getRotationZ();

                // Prepare Canvas and Camera
                canvas.save();
                mCamera.save();
                canvas.translate((coordX + (width / 2f)), (coordY + (height / 2f)));

                // Apply transformations
                mCamera.rotateX(xRotation);

                Matrix cameraMatrix = new Matrix();
                mCamera.getMatrix(cameraMatrix);
                canvas.concat(cameraMatrix);

                // Draw
               final Rect src = new Rect(coordX, coordY, (coordX + width),
                       (coordY + height));
               final RectF dst = new RectF(-(width / 2f), -(height / 2f), width / 2f,
                       height / 2f);
               canvas.drawBitmap( mUndistortedBitmap, src, dst, mBlindPaint );

               // Restore Canvas and Camera
               mCamera.restore();
               canvas.restore();

               if (LOG_ON ) {
                       Log. d(LOG_TAG, "Drew blind with size " + width + " by " + height
                               + " px with rotation (" + xRotation + ", " + yRotation
                               + ", " + zRotation + ") (x,y,z) at coordinates " + coordX
                               + ", " + coordY);
               }
        }


Lighting

To give the impression of depth, let’s add some lighting. I will not go into too much specifics on this but rather, refer you to Anders Ericsson’s excellent tutorial covering this subject in greater detail. You can find this under the “Let there be light” section of his article Making your own 3D list – Part 2.

With few minor tweaks, such as the addition of the angular offset and the cranking up of max levels to suit our needs, we end up with the following, which is added to BlindsView.java:

        // Lighting effect shamelessly nicked from Anders:
        // http://developer.sonymobile.com/wp/2010/05/31/android-tutorial-making-your-own-3d-list-part-2/ “Let there be light”


        /** Ambient light intensity */
        private static final int AMBIENT_LIGHT = 55;

        /** Diffuse light intensity */
        private static final int DIFFUSE_LIGHT = 255;

        /** Specular light intensity */
        private static final float SPECULAR_LIGHT = 70;

        /** Shininess constant */
        private static final float SHININESS = 255;

        /** The max intensity of the light */
        private static final int MAX_INTENSITY = 0xFF;

        /** Light source angular offset*/
        private static final float LIGHT_SOURCE_ANGLE = 38f;

        private LightingColorFilter calculateLight(float rotation) {
                rotation -= LIGHT_SOURCE_ANGLE;
                final double cosRotation = Math.cos (Math.PI * rotation / 180);
                int intensity = AMBIENT_LIGHT + (int) ( DIFFUSE_LIGHT * cosRotation);
                int highlightIntensity = (int) (SPECULAR_LIGHT * Math.pow(cosRotation,
                                SHININESS));

                if (intensity > MAX_INTENSITY) {
                        intensity = MAX_INTENSITY;
                }
                if (highlightIntensity > MAX_INTENSITY) {
                        highlightIntensity = MAX_INTENSITY;
                }

                final int light = Color.rgb (intensity, intensity, intensity);
                final int highlight = Color.rgb (highlightIntensity, highlightIntensity,
                                highlightIntensity);

                return new LightingColorFilter(light, highlight);
        }

As you can see, the tilting and lighting takes us quite a long way already!

Blinds with tilting and lighting.

Blinds with tilting and lighting.

To tilt or not to tilt
The purpose of the BlindsView is to act as a normal ViewGroup when not disturbed, but as soon as the user touches an empty area in it, it should shred. As of now, we always shred the view which is of course pretty useless. So let’s fix that. We’ve already prepared for it in drawCustomStuff; we only apply the effect if isInBlindMode is true, which it always is. It’s time now to make sure we only go into “blinds mode” when it’s appropriate.

When a touch event occurs in a ViewGroup, the ViewGroup checks to see if any of its child Views are hit and gives them the chance to handle the MotionEvent (unless the ViewGroup intercepts it). The child View’s onTouchEvent method is invoked and the child can react and let the parent know whether or not the event was handled. If they handle it and consume the event, this is where the story ends but if they weren’t hit or did not want to handle the event, the ViewGroup’s onTouchEvent is invoked, repeating the process (remember that ViewGroup extends View and may contain nested ViewGroups so this may reiterate a multitude of times). Also, if the view did handle the down event, it will also receive further events from the same series (move, up etc.). So, if we want to find a spot for handling the case when the BlindsView itself, rather than its buttons, is being touched; onTouchEvent is where we want to be.

You can read more about the handling of touch events in the Android Developers site.

Let’s override onTouchEvent and add a simple printout to verify this. Since we want to handle the event and receive all events, we will return true:

        @Override
        public boolean onTouchEvent(MotionEvent event) {
                Log. d(LOG_TAG, "Touch event in BlindsView!" );
                return true ;
        }

Looking at the logcat output while putting your finger down and moving it around will confirm how this works. Touching any of the passive areas (the texts or the background image) will trigger a log output, while touching any of the buttons (that will handle the touch events themselves) will not.

Now, in this example, we keep it simple and consider ourselves as being in Blinds mode when, and only when, the user touches the BlindsView and keep it at two states: pressed or not pressed. In an actual application, you may want to take better care of the transitions between states and track not only the current finger state, but implement a controller to give the whole system more of a fluid feel, but for the sake of this tutorial, a simpler and easier approach will do just fine.

So let’s turn the onTouchEvent into something like the below and upgrade the isInBlindMode variable we created in drawCustomStuff to a member that is set here and read in drawCustomStuff. After the state update, we invalidate the View to force a redraw.


        @Override
        public boolean onTouchEvent(MotionEvent event) {
                switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                       mIsInBlindMode = true ;
                       break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                       mIsInBlindMode = false ;
                       break;
                default:
                       // no change
                       break;

                }
                invalidate();
                return true ;
        }

At this point let’s also finalize drawCustomStuff by implementing the condition for the initBmpAndCanvas. We can reuse the old Bitmap and Canvas unless they are null (that is, if this is the first frame) or if the Bitmap was recycled:

        final boolean initBmpAndCanvas = (mIsInBlindMode && (!(mUndistortedBitmap != null && !mUndistortedBitmap
                           .isRecycled())));

And there you have it – a ViewGroup which acts as a LinearLayout during normal circumstances, but instantly shreds the contents into tilted and separately lit blinds when touched. Not bad, huh?

Blinds in normal view, then shredded.

Blinds in normal view, then shredded.

Finger-following Transforms
It’s still not all we wish for, though. When touched, all blinds instantly toggle to their predefined angles and stay there until the finger is lifted. To make it a bit neater still, we’ll start calculating transformations based on where the view is touched. Once again, this is a proof-of-concept implementation and we’ll keep the mechanism a rather simple one. When the finger is put down or as it moves around, we’ll calculate angles for all blinds and then draw them like this. We’ll add the move event and a call to a new method in onTouchEvent:

        public boolean onTouchEvent(MotionEvent event) {
                switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_MOVE:
                        mIsInBlindMode = true ;
                        calculateBlindRotations(event.getY());
                ...
        }

In the new method, we will loop through all blinds, measuring their respective distances to the touch coordinate and based on that distance assign it a rotation around its x axis. The distance is defined as the distance between the blind’s pivot coordinate and the current touch coordinate. By normalizing the distance, that is, putting it in relation to a maximum distance at which blinds may be affected, we can design how blinds should behave by using a function graph drawing tool to create a transfer function valid in the range from 0 to 1. The transfer function will describe how a portion of the distance (ranging from 0.0 to 1.0) translates into a rotation factor (also ranging from 0 to 1).

I recommend using a function graph drawing tool such as RECHNEROnline’s function graph to design the profile. I will use f(x)=-((x-0.55)*2)^2+1 where x is the normalized distance and f(x) is the rotation factor. By making sure the rotation is zero when distance is one or larger, we are guaranteed a smooth transition between the affected blinds and those too far away. I will use the same function on both sides (above and below) the touch event and by making sure zero distance translates to zero rotation in both cases, I am guaranteed that a blind will transfer nice and smoothly when I move my finger and the blind transfers from being above the touch to being below it. I really encourage you to play around with this function and see the difference it makes.

Since we declared a maximum radius from the touch event, outside of which no blinds shall be affected, there’s no point in updating each and every blind every time the finger moved ever so slightly so if the distance is too large, we’ll skip that.

It should be noted at this point that it’s not all that super nice to run these calculations on the UI thread each and every time the finger moves, since you will delay any drawing from happening for as long as the calculations run, which can be severe as it will make touch events queue up and performance could potentially drop noticeably if you were to swipe fast enough. You may want to run the calculating and state updating in a synchronised fashion on a worker thread instead, to make sure you are always ready to draw when required. Let’s keep that out of the scope of this tutorial, though. The actual performance gain is small in this application as the math so far is rather cheap and threading doesn’t really relate that closely to custom drawing in Android. If you miss it, let me know and I’ll try to find the time to add it in the future.

So let’s move on to getting the calculateBlindRotation method looking like so:

        private void calculateBlindRotations(float yPos) {
                        float currentBlindPivotY;
                float normalizedVerticalDistanceFromTouch;

                for (BlindInfo currentBlind : mBlindSet) {
                        currentBlind = mBlindSet.get(i);
                        currentBlindPivotY = currentBlind.getTop()
                                   + ( float) currentBlind.getHeight() / 2f;
                        normalizedVerticalDistanceFromTouch = Math
                                   . abs((yPos - currentBlindPivotY)
                                               / mMaxAffectRadius);

                        float xRotation = 0;
                        // Only rotate if within valid range
                        if (normalizedVerticalDistanceFromTouch <= 1f) {

                                // rot(d) = -((d-0.55)*2)^2+1 where 0<=d
                                final double normalizedRotationX = Math
                                              . max(0d,
                                                         (-Math. pow(
                                                                     ((normalizedVerticalDistanceFromTouch - 0.55f) * 2f),
                                                                     2) + 1));

                                // Blind above touch means negative angle
                                if ((currentBlindPivotY < yPos)) {
                                        xRotation = ( float) -(CONFIG_MAX_ROTATIONX * normalizedRotationX);
                                } else {
                                        xRotation = ( float) (CONFIG_MAX_ROTATIONX * normalizedRotationX);
                                }
                        }
                        currentBlind.setRotations(xRotation, 0f, 0f);
                }
        }

We’ll also need to add a new config constant for setting the maximum rotation (in absolute terms):

        private static final float CONFIG_MAX_ROTATIONX = 45f; 

And we’ll also need a new value for the maximum distance:

dimens.xml:

        <dimen name="touchEffectRadius"> 101dp<d/dimen >

BlindsView.java:

        private static float mMaxAffectRadius;
        ...
        private void init() {
                ...
                mMaxAffectRadius = getResources().getDimension(R.dimen.touchEffectRadius );
        }

Now is also a good time to clean out that temporary hack that sets rotations in onLayout. It’s always rewarding to be allowed to remove a FIXME line :). What’s even more rewarding is to gaze at what we have accomplished at this point:

Blinds transformed based on where the view is touched.

Blinds transformed based on where the view is touched.

Camera distance
When using the Camera, it’s always a good idea to use the setLocation method to specify the distance from camera to canvas. x and y can usually be left at their default zero values (because of the canvas translation earlier) but the distance along the z axis is worth some attention. It has quite some impact on how the Blinds skew when rotated.

So another config constant is added:

        private static final float CONFIG_CAMERA_DISTANCE_Z = -35;

And the constant is utilized before the canvas translate call in drawBlind:

        // Prepare Canvas and Camera
        canvas.save();
        mCamera .save();
        mCamera .setLocation(0, 0, CONFIG_CAMERA_DISTANCE_Z);
        canvas.translate((coordX + (width / 2f)), (coordY + (height / 2f)));

This requires api level 12 (HC MR1) so we’ll have to bump that number up from 11 to 12 in manifest.xml as well.

Blinds with Config Camera Distance constant added.

Blinds with Config Camera Distance constant added.

Plugging in more Camera and Canvas transforms
Okay, so we’re getting quite close to the finished BlindsView. We have a powerful frame set up and it will be fairly simple to pop in more transformations to dramatically alter the look by making the blinds scale, rotate, move, change colours or whatever other crazy things you may have in mind. Regardless of which, it will come down to three actions:

1. Calculating how to transform.

2. Storing sufficient information in the BlindsInfo.

3. Making use of it while drawing.

Rotation around Y
Let’s start with the lowest hanging fruit. We already have a mechanism for making the blinds rotate around the x-axis to respond to a user pressing or dragging along the y axis. Now it’s time to make use of the x coordinate to give the blinds a spin around the y axis. Let’s take the same steps as last time; we start in onTouchEvent and the routing of the event to calculate BlindRotations, which needs to take two args now (or we could just take the event itself):

        calculateBlindRotations(event.getX(), event.getY());

In the method, where we had variables for coordinate and rotation, we need to duplicate those for handling the new dimension:

        private static final float CONFIG_MAX_ROTATIONX = 45f;
        private static final float CONFIG_MAX_ROTATIONY = 15f;
        ...
        private void calculateBlindRotations( float xPos, float yPos) {
                float currentBlindPivotY;
                ...
                float xRotation = 0;
                float yRotation = 0;
                ...
                        currentBlind.setRotations(xRotation, yRotation, 0f);

I want the touched blind to tilt the most and then I want the effect to gradually decline over the others in the same way the x-rotation did, but in a linear fashion. So I’ll reuse normalizedVerticalDistanceFromTouch (which is known to range from 0 to 1) from the x rotation transform. I also want the blinds to tilt more, the closer to the screen edge I touch, so for that reason, I want to define the pivot point in the center and use the distance from that to the touch point to decide on the tilt angle:

                // -1 <= normalizedHorizontalDistanceFromPivot <= 1
                final float normalizedHorizontalDistanceFromPivot = ((xPos / getWidth()) - 0.5f) / 0.5f;
                // 0 <= linearDeclineFactor <= 1
                final float linearDeclineFactor = 1 - normalizedVerticalDistanceFromTouch;
                yRotation = CONFIG_MAX_ROTATIONY
                                * normalizedHorizontalDistanceFromPivot
                                * linearDeclineFactor;

The BlindInfo class already had the yRotation field so in this case, it’s just a matter of setting it.

In drawBlind, we will mimic the x rotation case by adding mCamera .rotateY(yRotation); before the rotateX call. Note that the order the rotations are applied matters a lot, which is why we can’t use Camera.rotate(x,y,z) even though it could potentially be faster.

Scale
While we’re at it, we could also apply a scaling factor to the blinds. Let’s follow the same pattern.

Adding an attribute, a setter and a getter to Blindinfo:

        private float scale = 1f;
        ...
        public void setScale(float s) {
                mScale = s;
        }

        public float getScale() {
                return mScale ;
        }

Next, assign a value limited by a config constant in BlindsView:

        private static final float CONFIG_MIN_SCALING = 0.97f;
        ...
        calculateBlindRotations() {
                ...
                // SCALING:
                // 1 at both end points, CONFIG_MIN_SCALING at center and
                // declining with the squared distance in between.
                scaling = 1f
                        - (1f - normalizedVerticalDistanceFromTouch
                                * normalizedVerticalDistanceFromTouch)
                        * (1f - CONFIG_MIN_SCALING);
        }
        currentBlind.setRotations(xRotation, yRotation, 0f);
        currentBlind.setScale(scaling);

Use the assigned value while in drawBlind:

        private void drawBlind() {
                ...
                final float scale = info.getScale();
                ...
                // Apply transformations
                mCamera.rotateY(yRotation);
                mCamera.rotateX(xRotation);
                canvas.scale( scale, scale , 0f, 0f);
                ...
        }

Using the exact same method, I’ll also apply a slight offset along the y axis to push all affected blinds a tiny bit downwards.

Other transformations of this type could include z-rotation, offsetting blinds x, y or z-wise (which is really effective) or applying an alpha value to the ones being pushed the furthest, or similar to that effect. I really encourage you to play around with this. It’s great fun and it’s fairly easy to create eye-catching and notable changes.

Blinds with scaling and rotating around x and y axis.

Blinds with scaling and rotating around x and y axis.

Handy tricks and optical illusions
We’re getting near the end of this tutorial but before we’re done, there is one more little trick I want to show you. While it certainly has its advantages to do all the drawing and transforming in the Java layer, using Canvas and Camera to manipulate a transformation matrix and a Canvas, one of the drawbacks is that we will never have “real 3D” using this. While we can operate on any object of any size and transform it in 3D space all objects will, in fact, be flat (stiff and with zero thickness). Essentially, we are operating on a set of cards in space.

There are a few tricks you can use to work around this limitation. To give the illusion of thickness, we’re going to apply a thin line at the bottom of each blind and apply another lighting effect to it, hence giving the illusion of the blind being beveled and make it feel thicker than it is. This will further improve the sense of depth.

The first thing we will need is another Paint object so let’s declare it together with the first one:

        private Paint mBlindPaint , mBlindStrokePaint;

We will initialize it in init() and add the new config variables and dimens:

dimens.xml:

        <dimen name="blindStrokeWidth"> 1.5dp</dimen >

BlindsView.java:

        private static final int CONFIG_BLINDSTROKE_BASECOLOR = Color.DKGRAY;
        private static final int CONFIG_BLINDSTROKE_ALPHA = 175;
        private static final int CONFIG_BLINDSTROKE_BEVEL_ANGLE = 45;
        private static float mConfigStrokeWidth;
                ...
        private void init() {
                        ...
                mConfigStrokeWidth = getResources().getDimension(R.dimen.blindStrokeWidth );
                mBlindStrokePaint = new Paint();
                mBlindStrokePaint.setColor(CONFIG_BLINDSTROKE_BASECOLOR);
                mBlindStrokePaint.setAlpha(CONFIG_BLINDSTROKE_ALPHA);
                mBlindStrokePaint.setStrokeWidth(mConfigStrokeWidth);
                mBlindStrokePaint.setAntiAlias(true);
                mBlindStrokePaint.setFilterBitmap(true);
                        ...
        }

In drawBlind, right after we draw the bitmap part that is the Blind, we will calculate a new ColorFilter for the bevel and use it to draw a bottom stroke. This edge will always have a rotation numerically larger than the blind itself. You can configure the angle of the bevel to your liking, but for natural reasons it should be between 0 and 90 degrees. In order not to disturb the blinds that are unaffected by the transform, we only apply the stroke to blinds that are in the affected area (use BlindInfo to control whether or not the stroke shall be drawn):


        if (drawBottomStroke) {
                mBlindStrokePaint.setColorFilter(calculateLight(xRotation
                        + CONFIG_BLINDSTROKE_BEVEL_ANGLE ));
                canvas.drawLine(dst. left, (dst.bottom - mConfigStrokeWidth / 2f),
                        dst. right, (dst.bottom - mConfigStrokeWidth / 2f),
                        mBlindStrokePaint);
        }
Blinds with illusion of thickness.

Blinds with illusion of thickness.

Conclusion
So there you have it, that’s all! Or… maybe not after all. You can spend infinity tweaking config parameters to give it that exact look and feel you want. Another interesting little trick you can try is applying gradients on the blinds in order to make it seem like they cast shadows on each other, as we do in our real lockscreen.

And there is some actual work left to be done as well. I mean, if one side of the blind is beveled, why wouldn’t the other be? And wouldn’t it be cool if the app was translucent so you could see another behind it? Perhaps draw another picture on the rear side of the blinds? Would a rotation around the z axis add to the effect? Could you make the lighting look better by taking not only the x, but also the y (and z?) rotation into account when calculating the lighting?  I’ll leave that for you to explore!

I hope you enjoyed and were inspired by this article, I can’t wait to hear from you in the comments or see your creations flooding Google Play with cool apps using distorted view groups or custom views transforming in 3d space :)!

Finally, you can download the BlindsView tutorial project, which includes the full source code for the sample BlindsView APK and a BlindsView starter template to create you own project.

Bye!

Johan

More information

Comments 11

Sort by: Newest | Oldest | Most popular

  1. By Marin Dominiković

    #1

    Can please show us how to make shadows? BTW, awesome guide!

  2. By Tim Sneller

    #2

    PLEASE give us back the option of having a proper sliding lockscreen. Blindsview looks AWFUL, and cannot be customised. Not possible to slide in one direction for camera, and the other for unlock.

  3. By Lutfil Haziq

    #3

    Can you make a JB update for the Xperia Neo L,at least android version 4.1.2..i’m really bored with the ICS firmware.. -.- hope you can make it as the Xperia J

    • By Joe Padre

      #5

      Hi Sidharth,
      What version of Android are you using? The BlindsView feature is available on Xperia™ smartphones running the Android Jelly Bean 4.1.2 update.
      Best regards,
      Joe from Developer World

    • By Joe Padre

      #7

      Hi Wouter,
      That’s great. Let us know how it goes!
      Best regards,
      Joe from Developer World

  4. By ketan parmar

    #8

    Hi, wow good tutorial, looking forward for more tutorials…
    Thank You guys

    • By Joe Padre

      #9

      Hi Ketan,
      Great to hear your feedback. Thanks for letting us know.
      Best regards,
      Joe from Developer World

  5. By Gabz Hype

    #10

    Hi. Is this applicable on the Xperia Neo L? And another question.. When are we Xperia owners going to get an update on the Neo L? I’m experiencing “sleep death” on my phone. I hope that you could answer my questions and act on my problem immediately. Thank you.

    • By Joe Padre

      #11

      Hi Gabz,
      The BlindsView feature is currently not applicable on Xperia neo L. We cannot comment on future products or releases, but please follow the software updates news on the Sony News Room (http://blogs.sonymobile.com/) for information on SW updates to Xperia phones. Sorry to hear about the “sleep death” issue on your phone, please call your local Xperia care (http://www.sonymobile.com/global-en/support/xperia-care/assisted/) to open a trouble ticket.
      Best regards,
      Joe from Developer World

1-11 of 11 comments.