Circular Reveal Transition for New Activity

Circular reveal transition for new activity

After looking for a solution for half a day without a result, I came up with an own implementation. I'm using a transparent activity with a matching root layout.
The root layout is a view which can then be revealed with createCircularReveal().

My code looks like this:

Theme Definition in styles.xml

<style name="Theme.Transparent" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>

Activity Definition in AndroidManifest.xml

<activity
android:name=".ui.CircularRevealActivity"
android:theme="@style/Theme.Transparent"
android:launchMode="singleTask"
/>

then I declared a layout for my activity (I've chosen DrawerLayout, so that I can have a NavDrawer. Every layout should work here.)

<android.support.v4.widget.DrawerLayout
android:id="@+id/drawer_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<FrameLayout
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/honey_melon"
>

<!-- Insert your actual layout here -->

</FrameLayout>

</android.support.v4.widget.DrawerLayout>

Important is the FrameLayout with the id root_layout. This view will be revealed in the activity.

Finally I implemented CircularRevealActivity and overwrote onCreate():

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
overridePendingTransition(R.anim.do_not_move, R.anim.do_not_move);

setContentView(R.layout.activity_reveal_circular);

if (savedInstanceState == null) {
rootLayout.setVisibility(View.INVISIBLE);

ViewTreeObserver viewTreeObserver = rootLayout.getViewTreeObserver();
if (viewTreeObserver.isAlive()) {
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
circularRevealActivity();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
rootLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
rootLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
}
}
}

It was important to put circularRevealActivity() into a OnGlobalLayoutListener, because the view needs to be drawn for the animation.

circularRevealActivity() looks like Ishaan's proposal:

private void circularRevealActivity() {

int cx = rootLayout.getWidth() / 2;
int cy = rootLayout.getHeight() / 2;

float finalRadius = Math.max(rootLayout.getWidth(), rootLayout.getHeight());

// create the animator for this view (the start radius is zero)
Animator circularReveal = ViewAnimationUtils.createCircularReveal(rootLayout, cx, cy, 0, finalRadius);
circularReveal.setDuration(1000);

// make the view visible and start the animation
rootLayout.setVisibility(View.VISIBLE);
circularReveal.start();
}

Edit 1

The definition for R.anim.do_not_move was added. However, it should work without that line too, if your design does not specify default transitions for activities. Let me know

R.anim.do_not_move:

<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromYDelta="0"
android:toYDelta="0"
android:duration="@android:integer/config_mediumAnimTime"
/>
</set>

Animate activity change as circular reveal

The reveal animation does not work against windows. You can emulate it by targeting the contents of the window. I did this and it worked fine:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
ViewGroup viewGroup = (ViewGroup) (findViewById(android.R.id.content).getParent());
viewGroup.setTransitionGroup(true);
RevealTransition transition = new RevealTransition();
transition.addTarget(viewGroup);
getWindow().setEnterTransition(transition);
}

You have to start the activity with ActivityOptions.makeSceneTransitionAnimation.

One caveat -- I'm actually uncertain whether it is legal to use getParent() on android.R.id.content. I did that so that my action bar would be targeted as well. You should check that out yourself. I was only doing a quick test to see if reveal will work.

I know that this is a performance issue, but you will likely want to assign your window background to your content as well so that the circular reveal has a hard line. The window background color fades in. You can adjust the fade duration so that it doesn't fade in as fast as the reveal, but I didn't see a visual issue in my test -- maybe because of the colors I used.

How to circular reveal the entire activity

After a lot of research and android source code reading, I figured out how to do this. It's in Scala but you should translate that to Java easily.

The following are the most important parts.

CircularRevealActivity.scala:

override protected def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
val window = getWindow
val decor = window.getDecorView.asInstanceOf[ViewGroup]
// prevent fading of background
decor.setBackgroundColor(android.R.color.transparent)
if (Build.version >= 21) {
window.setEnterTransition(circularRevealTransition)
window.setReturnTransition(circularRevealTransition)
// decor.setTransitionGroup(true) won't work
for (i <- 0 until decor.getChildCount) {
val child = decor.getChildAt(i).asInstanceOf[ViewGroup]
if (child != null) child.setTransitionGroup(true)
}
if (savedInstanceState == null) {
val intent = getIntent
val x = intent.getFloatExtra(EXTRA_SPAWN_LOCATION_X, Float.NaN)
if (!x.isNaN) {
val y = intent.getFloatExtra(EXTRA_SPAWN_LOCATION_Y, Float.NaN)
if (!y.isNaN) circularRevealTransition.spawnLocation = (x, y)
}
}
}
}

CircularReveal.scala:

@TargetApi(21)
class CircularReveal(context: Context, attrs: AttributeSet = null)
extends Visibility(context, attrs) {
var spawnLocation: (Float, Float) = _
var stopper: View = _
private val metrics = new DisplayMetrics
private lazy val wm = context.getSystemService(Context.WINDOW_SERVICE)
.asInstanceOf[WindowManager]
private def getEnclosingCircleRadius(x: Float, y: Float) =
math.hypot(math.max(x, metrics.widthPixels - x),
math.max(y, metrics.widthPixels - y)).toFloat

override def onAppear(sceneRoot: ViewGroup, view: View,
startValues: TransitionValues, endValues: TransitionValues) = {
wm.getDefaultDisplay.getMetrics(metrics)
val (x, y) = LocationObserver.getRelatedTo(spawnLocation, view)
new NoPauseAnimator(ViewAnimationUtils
.createCircularReveal(view, x.toInt, y.toInt, 0,
getEnclosingCircleRadius(x, y)))
}
override def onDisappear(sceneRoot: ViewGroup, view: View,
startValues: TransitionValues, endValues: TransitionValues) = {
wm.getDefaultDisplay.getMetrics(metrics)
val (x, y) = if (stopper == null)
LocationObserver.getRelatedTo((metrics.widthPixels * .5F,
metrics.heightPixels.toFloat), view)
else LocationObserver.getRelatedTo(stopper, view)
new NoPauseAnimator(ViewAnimationUtils
.createCircularReveal(view, x.toInt, y.toInt,
getEnclosingCircleRadius(x, y), 0))
}
}


Related Topics



Leave a reply



Submit