Introduction /

The full name should be Circular reveal animation, hereinafter referred to as reveal animation, because the Android system only provides this Circular reveal animation!

Reveal animations are special transitions between views, or even between interfaces.

View of the AndroidPlatform has a ViewAnimationUtils class, which is the only access to expose animation provided by the system. The only interface exposed is as follows:

Construct an Animator object, which is a RevealAnimator class object, using its static createCircularReveal method to implement a cool animation!

Gifs are the perfect way to illustrate what disclosure means, so I wrote a small demo that works like this:

App’s revealing effect:

The Activity expose effect:

The expose effect of a normal View:

A few pictures are worth a thousand words! So that’s what we call revealing animation. Demo source code (Kotlin) I have put on Github, source code at this address is as follows:

Github.com/xiaofei-dev…

Let’s talk more about the implementation of this transition animation.

Wall crack recommendation combined with Demo to read this article, in addition to the Demo code annotations are very detailed, readers can try if only according to the Demo source annotations can understand the above effect behind all principles…… I still suggest you read the following text!

/ body /

Based on the API

Let’s start with the basic usage of the Expose animation Api.

The only interface exposed by the expose animation is a static method of the ViewAnimationUtils class called createCircularReveal. To see the full signature of this method, there are five parameters used to construct an expose animation inside the method to be returned to the use case:


createCircularReveal(View view, int centerX, int centerY, float startRadius, float endRadius) 

Copy the code

The first argument is a View, and it’s easy to understand that the object to apply the expose animation must be a View.

The second parameter is the X-axis coordinates of the circle revealing effect, and the third parameter is the Y-axis coordinates.

The third parameter is the start radius of the circular expose effect. Similarly, the third parameter is the end radius of the circular expose effect. The start radius is 0, and the end radius is the width or height of the View, which is a typical process of revealing from nothing. Terminating radius 0 is a process of unmasking from something to nothing.

With the Animator object returned by this method, we can control the View to expose the animation at any time.

Isn’t that easy?

View-level reveal animation

Let’s start by looking at the code for the simplest unmasking animation of a normal View (figure 3 above).

/* Appreveal │ ├─base │ ├─ secondactivity. kt │ ├─base │ │ ├─ext │ ActicityExtension. Kt │ ├─ util StatusBarUtil. Kt */Copy the code

The unmasking animation of a normal View is shown in SecondActivity in the Demo, which has the following layout:


<?xml version="1.0" encoding="utf-8"? >
<androidx.constraintlayout.widget.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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    tools:context=".SecondActivity">
    <! -- We're going to animate the blue background View to expose and unexpose -->
    <View
        android:id="@+id/viewBg"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:background="@android:color/holo_blue_bright"
        android:visibility="visible"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintVertical_bias="0.1"
        android:text="@string/app_second"
        android:textSize="30sp"/>

    <! -- Click this button to start unmasking, unmasking animation -->
    <Button
        android:id="@+id/btnReveal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/app_reveal_r"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> 

Copy the code

In SecondActivity, click the button in the middle to start the expose animation of the interface control with the ID of viewBg. The key code is as follows:

. / / the middle button click event btnReveal setOnClickListener {view - > / / the system to provide the revelation of the animation for the SDK version 5.0 and aboveif (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                return@setVal startRadius:Float Val endRadius:Floatif(viewbg.visibility == view.visible){// startRadius = viewbg.height.tofloat () endRadius = 0f}else{// Create something from nothing, StartRadius = 0f endRadius = viewbg.height. ToFloat ()} val location = IntArray(2) View.getlocationinwindow (location) // Build an expose object. Note that the center of the circle and how the start and end radii are calculated, should be easy to understand. Here do not do too much explanation val animReveal = ViewAnimationUtils. CreateCircularReveal (viewBg, location [0] + the width / 2, Location [1] + view.height/2, startRadius, endRadius) Animreveal.duration = 400 Animreveal.interpolator = LinearInterpolator() animreveal.addListener (onStart) AnimReveal.duration = 400 Animreveal.interpolator = LinearInterpolator() Animreveal.addListener (onStart  = { viewBg.visibility = View.VISIBLE },onEnd = {if(startRadius ! = 0f){ viewBg.visibility = View.INVISIBLE btnReveal.setText(R.string.app_reveal) }else{
                    viewBg.visibility = View.VISIBLE
                    btnReveal.setText(R.string.app_reveal_r)
                }
            })
            animReveal.start()
        } 

Copy the code

The specific interface effect of the code is as follows:

Basically, it’s easy to expose the animation for a View application in the interface. Let’s look at how to apply our cool (subjective) expose animation for switching between activities in the application.

Activity-level expose animation

First we encounter two problems:

  1. When the expose animation is used for Activity switching, which View should we apply the expose animation to (the application object of the expose animation must be a View)?

  2. When to start performing the reveal animation?

According to our Demo, answer one by one.

When the expose animation is used for an Activity switch, the most appropriate object is definitely the root view of the Activity’s associated Window, the actual root view, and yes, the DecorView of the Activity’s Window.

As for when to start a revealing animation, it’s never too early or too late. First, it should not be too early, if the current View is not attached to the Window to expose the application animation will throw an exception, and second, it should not be too late, otherwise it will seriously affect the visual effect of the animation.

In the author’s practice, the best time to start a revealing animation is when the view becomes visible to the user! We can perfect this by setting an OnGlobalLayoutListener callback to the View’s ViewTreeObserver

I put all the relevant implementation code in the Demo’s BaseActivity class. The use-case Activity just inherits BaseActivity to apply the expose animation when opened. Note here that for continuity of the animation we need to pass in the center of the Activity that reveals the start of the animation from its previous Activity via the Intent. This is not difficult to do. The key codes are as follows:

// Write the Activity expose effect in the Base class. If you want to expose the animation effect, inherit the abstract class BaseActivity:AppCompatActivity(){companion object {// Manually pass the previous interface's click location val CLICK_X = in the intent"CLICK_X"
        val CLICK_Y = "CLICK_Y"} private var onGlobalLayout : ViewTreeObserver.OnGlobalLayoutListener? Var mAnimReveal: Animator? Var mAnimRevealR: Animator? = null override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState) circularReveal(intent)} Private Fun circularReveal(Intent: Intent?) {// The unmasking animation provided by the system requires SDK version 5.0 or above. When we can't get the click area of the previous interface, we won't show the unmasking animation, because there is no suitable anchor point at this timeif(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || (intent? .sourceBounds == null && intent? .hasExtra(CLICK_X)? .not()? :true)) returnval rect = intent? .sourceBounds val v = window.decorView v.visibility = View.INVISIBLE @SuppressWarnings onGlobalLayout = object : ViewTreeObserver.OnGlobalLayoutListener{ override funonGlobalLayout() {// Now is the best time to start revealing animation mAnimReveal? .removeAllListeners() mAnimReveal? .cancel() mAnimReveal = ViewAnimationUtils.createCircularReveal(v, rect? .centerX()? :intent? .getIntExtra(CLICK_X, 0)? :0, rect? .centerY()? :intent? .getIntExtra(CLICK_Y, 0)? :0, 0f, v.height.toFloat() ) mAnimReveal? .duration = 400 mAnimReveal? .interpolator = LinearInterpolator() mAnimReveal?.addListener(onEnd = {onGlobalLayout?.let {// We need to remove the callback in time after the expose animation is finished V?) viewTreeObserver?) removeOnGlobalLayoutListener mAnimReveal? (it)}}), start ()}} / / view visible change of callback, In the callback is one of the best timing to set animation v.v iewTreeObserver. AddOnGlobalLayoutListener (onGlobalLayout)} / / Activtiy reveal (exit) animation, which exits transition animations, Private Fun circularRevealReverse(Intent: intent) Intent?){// The SDK version 5.0 or above is required for the disclosure animation provided by the systemif(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || (intent? .sourceBounds == null && intent? .hasExtra(CLICK_X)? .not()? :true)) {
            super.onBackPressed()
            return} val rect = intent? .sourceBounds val v = window.decorView mAnimRevealR?.removeAllListeners() mAnimRevealR?.cancel() mAnimRevealR = ViewAnimationUtils.createCircularReveal(v, rect?.centerX()?:intent?.getIntExtra(CLICK_X, 0)?:0, rect?.centerY()?:intent?.getIntExtra(CLICK_Y, 0)?:0, v.height.toFloat(), 0f ) mAnimRevealR?.duration = 400 mAnimRevealR?.interpolator = LinearInterpolator() mAnimRevealR?.addListener(onEnd = { V.visibility = view.gone super.onBackPressed()}) mAnimRevealR?.start()} // Apply the anti-expose animation Override Fun when fallbackonBackPressed() {circularRevealReverse(intent)} // Other code omitted, see the BaseActivity in Demo... }Copy the code

The code to open SecondActivity(inheriting BaseActivity) in MainActivity:

btnNext.setOnClickListener {view -> val intent = Intent(this, SecondActivity: : class. Java) val location = IntArray (2) the getLocationInWindow (location) / / click on the button as passed the center position coordinates Intent. PutExtra (CLICK_X, location[0] + view.width/2) Intent. PutExtra (CLICK_Y, location[0] + view.width/2) location[1] + view.height/2) startActivity(intent) }Copy the code

Let’s focus on the BaseActivity circularReveal method, which is called once in the onCreate method and is used to animate the Activity appropriately. Start building and executing the expose animation as soon as the DecorView is visible to the user.

There is a condition in a method: Intent? . SourceBounds == null, where intent? What is sourceBounds? In fact, when switching between activities in the current App, it is completely unnecessary to judge this condition. This code is written for the revealing effect of the entire App, which will be discussed at the end of the article (the picture of the revealing effect of the App above). The Intent class has a method with the following signature:

Use this method to return a Rect that stores the location of your application’s icon in the desktop Launcher, which I’ll cover in more detail and skip for now.

CircularRevealReverse, the method that performs the anti-expose animation, is called in the onBackPressed method.

The rest of the code should be well understood after the above matting and comments, and finally run on the real machine animation looks like this:

It is also important to note that the Activity Window has a background color (usually black), so we need to customize the Activity theme to ensure that the expose animation looks like this:

<! -- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <! -- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item> </style> <! < span style = "box-sizing: border-box! Important; word-wrap: break-word! Important;"trans_no_anim" parent="AppTheme"> <! -- Mask the default Activity transition animation --> <item name="android:windowAnimationStyle">@null</item> <! -- Implement transparent Activity window background --> <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
    </style> 

Copy the code

To use a expose animation for an Activity, you must set its theme to trans_NO_anim in the manifest file.

App level reveal animation

The last thing I want to talk about is the revelation animation application scenario.

Can we add a perfect reveal animation to the entire App?

The perfect reveal animation is the place we return to every time we press the Home button from the Android Launcher, also known as the Launcher. Notice that the Launcher is a separate APP in Android. When you click the APP icon from the desktop to launch the APP, the Launcher sends the location of the icon all the way to the APP’s Launcher Activity through the intent. Click our app icon in Google), no matter whether the app is started for the first time, no matter which interface it stays in, it can open or return to our app by revealing the animation. In our app, When we go back to the Launcher with the back button, we can use the anti-reveal animation to pack up our app. (We don’t use the anti-reveal animation when we go back to the Launcher with the Home button, because full screen gestures are popular in The Android system, which makes the anti-reveal animation look bad. The system’s default animation is best. It is known that the reveal animation must have a center, and this center must be the coordinate of the center of our application icon in the Launcher. As mentioned earlier, information about this coordinate can be obtained through the getSourceBounds() method of the Intent of a Launcher Activity (a null is what a non-launcher Activity gets from this method). The problem here is that the icon in the Launcher changes its position (manually dragged by the user)! Perfect results must be able to accommodate the launch icon whose coordinates change at any time. It would look something like this:

The answer is unfortunately, in my own practice, the existing API provided by Android does not meet the need to add a perfect expose animation to the entire App! Anyway, the author’s own attempt ended in failure! As you can see from the GIF above, I almost nailed it. The problem is that the location of the icon in the Launcher can change at any time (manually dragged by the user), and I can’t find a way to update the location of the icon in the Launcher in a timely manner! Let’s take a look at how the effect shown above is implemented. Do you remember the important BaseActivity code posted above? It is not completely posted, and there is code below it (note the code posted above) :

// Write the Activity expose effect in the Base class. If you want to expose the animation effect, inherit the abstract class BaseActivity:AppCompatActivity(){// omit the code posted above... . override funonDestroy() {// Release resources in time to ensure code robustness mAnimReveal? .removeAllListeners() mAnimReveal? .cancel() mAnimRevealR? .removeAllListeners() mAnimRevealR? .cancel() // Release resources in time to ensure code robustness onGlobalLayout? . Let {window. DecorView. ViewTreeObserver?. RemoveOnGlobalLayoutListener (it)}. Super onDestroy ()} / / this method is very important, If the position of our app's launch icon changes on the desktop, we can receive the new location information here. However, in my own practice, this is very limited. Intent?) {super.onnewintent (Intent) circularReveal(Intent)}}Copy the code

Notice the onNewIntent method, where we update the location of our app’s launch icon in the Launcher.

How is it updated?

When you click the icon on the desktop to start the application for the first time, the location of the icon is transmitted to the application Launcher Activity through the intent. If the application is already started and just goes back to the background, the onNewIntent method is called when you click the launch icon to return to the application.

Usually not. However, when you set the Launcher Activity launch mode from a manifest file to any mode other than standard, clicking the launch icon from the desktop will call back its onNewIntent method as long as the Launcher Activity is at the top of the stack. This method passes in an intent parameter that holds the latest coordinates of the application’s launch icon in the Launcher!

For example, we set the startup mode of MainActivity to singleTop:


<activity
    android:name=".MainActivity"
    android:launchMode="singleTop"> <! As long as the Activity's lanuchMode is not standard, when the Activity is at the top of its own task stack, The Activity's onNewIntent method is triggered each time the app's launch icon is clicked on the desktop --> <intent-filter> < Action Android :name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity> 

Copy the code

The effect shown above can be achieved.

The Launcher Activity must be at the top of the stack, and only the Launcher Activity may be called back to the onNewIntent method when clicked on the launch icon! The author has not found any other interface in the system to update or obtain the latest position information of the application icon!

Because of this, so I finally came to the conclusion that we can’t add a perfect revelation to the App animation effects, can only under the condition of limited (don’t go too care about the location of the application startup icon could be user to change it at any time) add a at first glance to App still reveal animation effects, as shown in the above!

That’s all.

Ali P6P7【 android 】 Advanced information sharing + salary job-hopping essential interview questions