Happy November 2016! Today we will be creating a simple rectangular reveal animation within an Android app. This animation can be modified in many different ways to fit your needs. For our example, we will make it so a part of an image is revealed when the user taps on an area within the view. This could be extended into a game, an opener for your app, and much more!
This is by no means a final implementation to use in a real project, but is a start for it. In a real project, performance needs to be taken into consideration, especially when using large images. Here’s a video displaying what we will be building:
Let’s start by setting up our XML file.
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/mainView" android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:id="@+id/outerLayer" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" android:background="@android:color/white"> <FrameLayout android:id="@+id/innerLayer" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@id/image" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" android:scaleType="centerCrop" android:src="@drawable/puppies"/> </FrameLayout> </RelativeLayout> </RelativeLayout>
The RelativeLayout is the view on which we will be performing the actual reveal animation. The FrameLayout provides a container for the ImageView, so the ImageView does not jump around as the RelativeLayout is being resized.
Now let’s setup our project and give the ImageView an explicit height and width. If the ImageView has a match_parent height and width, then the image will resize as the RelativeLayout resizes. First, we get the screen dimensions by getting the size of the display, and then we set the height and width for the ImageView to match the screen size – your needs for sizing the ImageView might be different. Depending on the aspect ratio of your image, you might need to worry about sizing the image proportionately to the screen, so the image is not stretched.
package com.example.yelenabelikova.touchresizeanimation; import android.app.Activity; import android.graphics.Point; import android.os.Bundle; import android.view.Display; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; import android.widget.RelativeLayout; public class MainActivity extends Activity { private RelativeLayout mMainView; private RelativeLayout mOuterLayer; private ImageView mImage; private static int DURATION = 300; private static int OFFSET_COORD_X = 900; private static int OFFSET_COORD_Y = 900; private int mWidth; private int mHeight; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mMainView = (RelativeLayout) findViewById(R.id.mainView); mOuterLayer = (RelativeLayout) findViewById(R.id.outerLayer); mImage = (ImageView) findViewById(R.id.image); setScreenDimensions(); mImage.getLayoutParams().width = mWidth; mImage.getLayoutParams().height = mHeight; mImage.requestLayout(); } public void setScreenDimensions() { Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(); display.getSize(size); mWidth = size.x; mHeight = size.y; } }
Now let’s create the RevealAnimation class. We are going to extend the Animation class and override some methods to help accomplish the custom animation. This setup gives us an animation that resizes the outer layer RelativeLayout. However, since the view is centeredInParent, this always happens from the center. Let’s also set up an OnTouchListener in the MainActivity, so will be able to start the animation when the user is touching the screen. The deltaHeight and deltaWidth variables are the difference between starting and ending dimensions.
package com.example.yelenabelikova.touchresizeanimation; import android.view.View; import android.view.animation.Animation; import android.view.animation.Interpolator; import android.view.animation.Transformation; import android.widget.FrameLayout; public class ResizeAnimation extends Animation { private int startHeight; private int deltaHeight; private int startWidth; private int deltaWidth; private View outerView; private View innerView; public ResizeAnimation(View outerView, View innerView) { this.outerView = outerView; this.innerView = innerView; } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { outerView.getLayoutParams().height = (int) (startHeight + deltaHeight * interpolatedTime); outerView.getLayoutParams().width = (int) (startWidth + deltaWidth * interpolatedTime); outerView.requestLayout(); } public void setStartParams(int startHeight, int endHeight, int startWidth, int endWidth) { this.startHeight = startHeight; deltaHeight = endHeight - startHeight; this.startWidth = startWidth; deltaWidth = endWidth - startWidth; } @Override public void setDuration(long durationMillis) { super.setDuration(durationMillis); } @Override protected void ensureInterpolator() { super.ensureInterpolator(); } @Override public void setInterpolator(Interpolator i) { super.setInterpolator(i); } @Override public boolean willChangeBounds() { return true; } }
In MainActivity.java
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Code from previous steps goes here... mResizeAnimation = new ResizeAnimation(mOuterLayer, mImage); mResizeAnimation.setDuration(DURATION); mMainView.setOnTouchListener(mOnTouchListener); } private View.OnTouchListener mOnTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startRevealAnimation(offsetX, offsetY); return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: return false; } return false; } }; private void startRevealAnimation(float translationX, float translationY) { mResizeAnimation.setStartParams(1, OFFSET_COORD_Y, 1, OFFSET_COORD_X); mOuterLayer.startAnimation(mResizeAnimation); }
To get the animation to happen where we want it to (wherever a touch event happens), let’s modify the OnTouchListener to capture the x and y coordinates of the touch event. Let’s then modify the RevealAnimation class to use those coordinates to translate the relative layout to the correct location.
In RevealAnimation.java
public void setStartParams(int startHeight, int endHeight, int startWidth, int endWidth, float translationX, float translationY) { this.startHeight = startHeight; deltaHeight = endHeight - startHeight; this.startWidth = startWidth; deltaWidth = endWidth - startWidth; outerView.setTranslationX(translationX); outerView.setTranslationY(translationY); }
In MainActivity.java
private View.OnTouchListener mOnTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { float touchX = event.getX(); float touchY = event.getY(); float offsetX; float offsetY; offsetX = touchX - mWidth / 2; offsetY = touchY - mHeight / 2; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startRevealAnimation(offsetX, offsetY); return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: return false; } return false; } };
Now we have a new problem. The ImageView is visible when RelativeLayout animates, but only partially because the ImageView is still centered with the height and width of its parent. If you started the animation perfectly from the center, the image would be aligned with where the reveal animation is occurring. To fix this issue, we have to update the margins of the ImageView every time we start the animation, so it gets offset in a way that perfectly matches the offset of the RelativeLayout.
public void setStartParams(int startHeight, int endHeight, int startWidth, int endWidth, float translationX, float translationY) { // Code from previous steps goes here... FrameLayout.LayoutParams mImageParams = (FrameLayout.LayoutParams) innerView.getLayoutParams(); mImageParams.setMargins((int)(-1 * translationX), (int)(-1 * translationY), 0, 0); innerView.setLayoutParams(mImageParams); innerView.requestLayout(); }
Now we have a reveal animation that does what we want it to do, in the correct place. The final step is to set up the animation that happens when the touch event is over. Since we do not need to worry about translating the RelativeLayout or modifying the margins on the ImageView, we simply need to change the height and width of the RelativeLayout to get the reveal window to collapse.
RevealAnimation.java
public void setCancelParams(int startHeight, int endHeight, int startWidth, int endWidth) { this.startHeight = startHeight; deltaHeight = endHeight - startHeight; this.startWidth = startWidth; deltaWidth = endWidth - startWidth; }
MainActivity.java
private void cancelRevealAnimation() { RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mOuterLayer.getLayoutParams(); mResizeAnimation.setCancelParams(params.height, 1, params.width, 1); mOuterLayer.startAnimation(mResizeAnimation); }
private View.OnTouchListener mOnTouchListener = new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // Code from previous steps goes here... case MotionEvent.ACTION_DOWN: startRevealAnimation(offsetX, offsetY); return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: cancelRevealAnimation(); return false; } // Code from previous steps goes here... };
And there you go! You should now have the desired interaction: tapping on the view to reveal part of the full image, and then close it by tapping again. There’s a lot of interesting ways this could be used within your app, and if you happen to use this, we’d love to see it in action! You can get in touch with us on Twitter or by contacting us via email.