I’m participating in nuggets Creators Camp # 4, click here to learn more and learn together!

preface

We encountered a problem with the Pause life cycle of a Fragment, so we investigated the Pause life cycle characteristics of the Fragment. It’s about this note.

We know that the life cycle of a Fragment depends on an Activity, so to explore the Pause process of a Fragment, we need to start with the Pause of the Activity.

Pause the process

You can see the code in onPause for FragmentActivity as follows:

@Override
protected void onPause(a) {
    super.onPause();
    mResumed = false;
    if (mHandler.hasMessages(MSG_RESUME_PENDING)) {
        mHandler.removeMessages(MSG_RESUME_PENDING);
        onResumeFragments();
    }
    mFragments.dispatchPause();
}
Copy the code

You can see that the last line of code starts the Fragment Pause process. MFragments is a FragmentControler object whose dispatchPause method has only one line of code

public void dispatchPause(a) {
    mHost.mFragmentManager.dispatchPause();
}
Copy the code

It calls the dispatchPause method of the FragmentManager

public void dispatchPause(a) {
    moveToState(Fragment.STARTED, false);
}
Copy the code

One line of code, moveToState. In FragmentManager, this method ends up calling another overloaded moveToState method

void moveToState(int newState, boolean always) {
    moveToState(newState, 0.0, always);
}

void moveToState(int newState, int transit, int transitStyle, boolean always) {
    if (mHost == null&& newState ! = Fragment.INITIALIZING) {throw new IllegalStateException("No host");
    }

    if(! always && mCurState == newState) {return;
    }

    mCurState = newState;
    if(mActive ! =null) {
        boolean loadersRunning = false;
        for (int i=0; i<mActive.size(); i++) {
            Fragment f = mActive.get(i);
            if(f ! =null) {
                moveToState(f, newState, transit, transitStyle, false);
                if(f.mLoaderManager ! =null) { loadersRunning |= f.mLoaderManager.hasRunningLoaders(); }}}if(! loadersRunning) { startPendingDeferredFragments(); }if(mNeedMenuInvalidate && mHost ! =null && mCurState == Fragment.RESUMED) {
            mHost.onSupportInvalidateOptionsMenu();
            mNeedMenuInvalidate = false; }}}Copy the code

Check whether there is a fragment, and if so, call the following function

void moveToState(Fragment f, int newState, int transit, int transitionStyle,
        boolean keepActive)
Copy the code

This method is quite complex, nearly 300 lines, so I won’t post all the source code.

Focus on this code

} else if (f.mState > newState) {
    switch (f.mState) {
        case Fragment.RESUMED:
            if (newState < Fragment.RESUMED) {
                if (DEBUG) Log.v(TAG, "movefrom RESUMED: " + f);
                f.performPause();
            }
        case Fragment.STARTED:
            if (newState < Fragment.STARTED) {
                if (DEBUG) Log.v(TAG, "movefrom STARTED: " + f);
                f.performStop();
            }
        case Fragment.STOPPED:
            if (newState < Fragment.STOPPED) {
                if (DEBUG) Log.v(TAG, "movefrom STOPPED: " + f);
                f.performReallyStop();
            }
        case Fragment.ACTIVITY_CREATED:
            if (newState < Fragment.ACTIVITY_CREATED) {
                if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f);
                if(f.mView ! =null) {
                    // Need to save the current view state if not
                    // done already.
                    if (mHost.onShouldSaveFragmentState(f) && f.mSavedViewState == null) {
                        saveFragmentViewState(f);
                    }
                }
                f.performDestroyView(); //destroy view
                if(f.mView ! =null&& f.mContainer ! =null) {
                    Animation anim = null;
                    if(mCurState > Fragment.INITIALIZING && ! mDestroyed) { anim = loadAnimation(f, transit,false,
                                transitionStyle);
                    }
                    if(anim ! =null) {
                        final Fragment fragment = f;
                        f.mAnimatingAway = f.mView;
                        f.mStateAfterAnimating = newState;
                        final View viewToAnimate = f.mView;
                        anim.setAnimationListener(new AnimateOnHWLayerIfNeededListener(
                                viewToAnimate, anim) {
                            @Override
                            public void onAnimationEnd(Animation animation) {
                                super.onAnimationEnd(animation);
                                if(fragment.mAnimatingAway ! =null) {
                                    fragment.mAnimatingAway = null;
                                    moveToState(fragment, fragment.mStateAfterAnimating,
                                            0.0.false); }}}); f.mView.startAnimation(anim); } f.mContainer.removeView(f.mView);//remove view
                }
                f.mContainer = null;
                f.mView = null;
                f.mInnerView = null;
            }
        case Fragment.CREATED:
Copy the code

The states are as follows

static final int INITIALIZING = 0;     // Not yet created.
static final int CREATED = 1;          // Created.
static final int ACTIVITY_CREATED = 2; // The activity has finished its creation.
static final int STOPPED = 3;          // Fully created, not started.
static final int STARTED = 4;          // Created and started, not resumed.
static final int RESUMED = 5;          // Created started and resumed.
Copy the code

You can see that RESUMED is the largest

When a page is RESUMED and the current state is code-puase you can see in the dispathPause method that newState is STARTED, then the first line of code above is true and goes to this level of logic.

Because the mState is RESUMED, it executes each case in turn (since there is no break in any case of the switch, it executes sequentially). But when the first case is executed, the Fragment’s performPause method is called,

You can see the method performPause in the Fragment source code

void performPause(a) {
    if(mChildFragmentManager ! =null) {
        mChildFragmentManager.dispatchPause();
    }
    mState = STARTED;
    mCalled = false;
    onPause();
    if(! mCalled) {throw new SuperNotCalledException("Fragment " + this
                + " did not call through to super.onPause()"); }}Copy the code

If there is no childFragment, call onPause directly.

If there is a childFragment, the dispatchPause method of its fragmentManager is called first. This enters the Pause process of the childFragment. This process is the same as above, in fact, a recursive process.

To sum up, the sequence of calls is shown

Pause the Fragment. It starts with the onPause of the Activity, and the onPause of the Fragment is called last.

Problems arise

Going back to the beginning, what was the problem we had?

First notice the following code in the last moveToState function

f.performDestroyView(); //destroy view
if(f.mView ! =null&& f.mContainer ! =null) {... f.mContainer.removeView(f.mView);//remove view
}
f.mContainer = null;
f.mView = null;
f.mInnerView = null;
Copy the code

Fragment: destory view fragment: deStory view fragment

There will be a problem!! When switching between a Fragment in an Activity, the view is removed before the Fragment’s Pause period has completely expired, while the Fragment is still displayed on the screen. If the activity theme is transparent and the Fragment has a SurfaceView or MapView view, it will display perspective during the remove process. And setting a background color for the Fragment container doesn’t help.

Note that this occurs during Pause, but since returning to the desktop is instantaneous, there is no problem. GetFragmentManager ().beginTransaction().replace(…)

If it was just a regular TextView or something like that it wouldn’t be like that. After repeated testing, it is found that when a TextView is removed (at the second underlined code) it will remain on the page and participate in a transition animation (if any); And SurfaceView, for example, when you remove that area of content will immediately disappear! This results in instant perspective.

Analyze the solution

This is due to the particularity of SurfaceView. In fact, when removing, SurfaceView is retained like other types of views, but all its drawn content is recycled (including the default background of SurfaceView – black), and there is no content, presenting a transparent phenomenon.

SurfaceView transparency can be resolved by setting the format of the SurfaceView

view.surfaceview.setZOrderOnTop(true)
view.surfaceview.holder.setFormat(PixelFormat.RGBA_8888)
Copy the code

GetFragmentManager ().beginTransaction().replace(…) Before the MapView or its container is set to INVISIBLE, so that the background color can be displayed normally without the influence of MapView, there will be no perspective phenomenon. Remember to reset to VISIBLE when restoring.

Another way to get around perspective completely is to make the activity’s theme opaque, but make sure it doesn’t affect the rest of the activity’s display.

The deep reason

The SurfaceView has no transparent content, so the container background is invalid.

The Z-axis position of the Layer used to describe the SurfaceView or the LayerBuffer is smaller than that of the Layer used to host the Activity window, but the former makes a “hole” in the latter so that its UI is visible to the user. In fact, the “hole” the SurfaceView makes in its host Activity window is nothing more than a transparent area in its host Activity window.

So when the SurfaceView content disappears completely, the “hole” reveals the content of the following page, which causes problems.