반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

코딩하기 좋은날

Android Transition 버그(Exit Transition이 동작하지 않는 문제) 본문

Android

Android Transition 버그(Exit Transition이 동작하지 않는 문제)

huiung 2021. 7. 5. 21:16
반응형

 

안드로이드에서 화면 이동시에 Transition을 사용하는 경우가 있는데요. API 29, 30레벨에서 Exit Transition이 동작하지 않는 버그가 있습니다.

 

예를 들어 A,B,C 3개의 액티비티가 있는 경우

 

A -> B -> C 로 진입시 EnterTransition 모두 정상 동작

C -> B Exit Transition 정상 동작

B -> A Exit Transition 동작 X (???)

 

이런 현상이 있습니다. 또한 이현상은 API 29,30 레벨에서만 발생하고 있는데요. 원인 파악을 조금 해보았습니다.

 

우선 해당 현상과 관련 있는 클래스들 입니다.

 

Activity.java

EnterTransitionCoordinator.java

ExitTransitionCoordinator.java

ActivityTransitionState.java

ActivityTransitionCoordinator.java

 

일반적으로 한 액티비티에서 백키를 눌렀을시 다음과 같은 호출 순서를 가집니다.

Activity.finishAfterTransition() → ActivityTransitionState.startExitBackTransition() → EnterTransitionCoordinator.getPendingExitSharedElementNames()

 

A -> B -> C -> B -> A, B로 다시 되돌아온 순간

 

getPendingExitNames()함수 내부의 mEnterTransitionCoordinator.getPendingExitSharedElementNames() 에서 null이 반환된다. 따라서 아래의 startExitBackTransition에서 바로 false가 리턴되어 버린다.

 

ActivityTransitionState.java

/**
     * Returns the element names to be used for exit animation. It caches the list internally so
     * that it is preserved through activty destroy and restore.
     */
    private ArrayList<String> getPendingExitNames() {
        if (mPendingExitNames == null && mEnterTransitionCoordinator != null) {
            mPendingExitNames = mEnterTransitionCoordinator.getPendingExitSharedElementNames();
        }
        return mPendingExitNames;
    }
    
    
    public boolean startExitBackTransition(final Activity activity) {
        ArrayList<String> pendingExitNames = getPendingExitNames();
        if (pendingExitNames == null || mCalledExitCoordinator != null) {
            return false;
        } else {
            if (!mHasExited) {
                mHasExited = true;
                Transition enterViewsTransition = null;
                ViewGroup decor = null;
                boolean delayExitBack = false;
                if (mEnterTransitionCoordinator != null) {
                    enterViewsTransition = mEnterTransitionCoordinator.getEnterViewsTransition();
                    decor = mEnterTransitionCoordinator.getDecor();
                    delayExitBack = mEnterTransitionCoordinator.cancelEnter();
                    mEnterTransitionCoordinator = null;
                    if (enterViewsTransition != null && decor != null) {
                        enterViewsTransition.pause(decor);
                    }
                }

                mReturnExitCoordinator = new ExitTransitionCoordinator(activity,
                        activity.getWindow(), activity.mEnterTransitionListener, pendingExitNames,
                        null, null, true);
                if (enterViewsTransition != null && decor != null) {
                    enterViewsTransition.resume(decor);
                }
                if (delayExitBack && decor != null) {
                    final ViewGroup finalDecor = decor;
                    OneShotPreDrawListener.add(decor, () -> {
                        if (mReturnExitCoordinator != null) {
                            mReturnExitCoordinator.startExit(activity.mResultCode,
                                    activity.mResultData);
                        }
                    });
                } else {
                    mReturnExitCoordinator.startExit(activity.mResultCode, activity.mResultData);
                }
            }
            return true;
        }
    }

 

그렇다면 왜? 2개이상의 액티비티를 이동 한 후 한개의 액티비티가 종료된 이후의 액티비티에서 해당 값이 null일까?

 

우선 API29부터 변경된 부분은 다음과 같다.

startExitBackTransition()함수의 첫번째 라인인 해당 부분이 추가 되었다. 

ArrayList<String> pendingExitNames = getPendingExitNames();

 

조금 복잡할 수 있는데 다시 mEnterTransitionCoordinator.getPendingExitSharedElementNames() 해당 호출의 내부를 살펴보자. mPendingExitNames를 리턴하고 있다. 그렇다면 해당 변수는 언제 초기화 될까?

 

EnterTransitionCoordinator.java

public ArrayList<String> getPendingExitSharedElementNames() {
        return mPendingExitNames;
    }

@Override
    protected void onReceiveResult(int resultCode, Bundle resultData) {
        switch (resultCode) {
            case MSG_TAKE_SHARED_ELEMENTS:
                if (!mIsCanceled) {
                    mSharedElementsBundle = resultData;
                    onTakeSharedElements();
                }
                break;
            case MSG_EXIT_TRANSITION_COMPLETE:
                if (!mIsCanceled) {
                    mIsExitTransitionComplete = true;
                    if (mSharedElementTransitionStarted) {
                        onRemoteExitTransitionComplete();
                    }
                }
                break;
            case MSG_CANCEL:
                cancel();
                break;
            case MSG_ALLOW_RETURN_TRANSITION:
                if (!mIsCanceled) {
                    mPendingExitNames = mAllSharedElementNames;
                }
                break;
        }
    }

 

해당 변수는 MSG_ALLOW_RETURN_TRANSITION 을 resultReceiver가 수신한 경우에만 초기화를 하고 있다.

즉 버그가 일어나는 상황에서 EnterTransitionCoordinator에서 MSG_ALLOW_RETURN_TRANSITION 메시지를 받지 못한 것으로 생각이 된다. 처음에 액티비티가 생성 되었을때는 해당 메시지를 받았고 mPendingExitNames 변수는 초기화가 되었다. 그렇다는건 화면 전환시에 재생성이 되었나? 라고 짐작 할 수 있다. 재생성이 된 경우에 위의 메시지를 받지 못했고 startExitBackTransition()함수에서 호출을 하면서 문제가 생기는 것이다. API29이전에서는 초기의 값을 그대로 사용 했기 때문에 문제가 없었지만 이것이 추가됨으로써 해당 버그가 발생하고 있다. 

 

아래 코드를 보면 실제 EnterTransitionCoordinator는 onstop()의 경우 null이 되고 onStart() -  perfromStart에서 호출되는 enterReady()함수에 의해 재생성 되고 있다.

 

public void onStop() {
        restoreExitedViews();
        if (mEnterTransitionCoordinator != null) {
            mEnterTransitionCoordinator.stop();
            mEnterTransitionCoordinator = null;
        }
        if (mReturnExitCoordinator != null) {
            mReturnExitCoordinator.stop();
            mReturnExitCoordinator = null;
        }
    }
    
    public void enterReady(Activity activity) {
        if (mEnterActivityOptions == null || mIsEnterTriggered) {
            return;
        }
        mIsEnterTriggered = true;
        mHasExited = false;
        ArrayList<String> sharedElementNames = mEnterActivityOptions.getSharedElementNames();
        ResultReceiver resultReceiver = mEnterActivityOptions.getResultReceiver();
        final boolean isReturning = mEnterActivityOptions.isReturning();
        if (isReturning) {
            restoreExitedViews();
            activity.getWindow().getDecorView().setVisibility(View.VISIBLE);
        }
        mEnterTransitionCoordinator = new EnterTransitionCoordinator(activity,
                resultReceiver, sharedElementNames, mEnterActivityOptions.isReturning(),
                mEnterActivityOptions.isCrossTask());
        if (mEnterActivityOptions.isCrossTask()) {
            mExitingFrom = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
            mExitingTo = new ArrayList<>(mEnterActivityOptions.getSharedElementNames());
        }

        if (!mIsEnterPostponed) {
            startEnter();
        }
    }

 

자 그렇다면 이 메시지는 누가 보낼까? 추적을 해보면 다음과 같은 코드를 발견 할 수 있다.

 

ExitTransitionCoordinator.java

 protected void notifyComplete() {
        if (isReadyToNotify()) {
            if (!mSharedElementNotified) {
                mSharedElementNotified = true;
                delayCancel();

                if (!mActivity.isTopOfTask()) {
                    mResultReceiver.send(MSG_ALLOW_RETURN_TRANSITION, null);
                }

                if (mListener == null) {
                    mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle);
                    notifyExitComplete();
                } else {
                    final ResultReceiver resultReceiver = mResultReceiver;
                    final Bundle sharedElementBundle = mSharedElementBundle;
                    mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements,
                            new OnSharedElementsReadyListener() {
                                @Override
                                public void onSharedElementsReady() {
                                    resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS,
                                            sharedElementBundle);
                                    notifyExitComplete();
                                }
                            });
                }
            } else {
                notifyExitComplete();
            }
        }
    }

 

거의 다 왔다. Activity.isTopOfTask() 의 리턴값에 따라 메시지를 보내고 있다.

해당 함수는 다음과 같이 mToken, mWindow 값을 확인하고 있는데 저값이 null이여서 false가 리턴되야 메시지를 보낼 수 있다. 그러나 해당 값들은 attach()함수 에서 초기화가 되고있다. 즉 startActivity를 통해 실행되면서 생성되는 transitionCoordinator는 메시지를 보낼 수 있는 상태지만 다른 액티비티가 위에 뜨면서 onStop()이 호출되고 다시 돌아오면서 onStart()가 호출되는 시점에는 mToken, mWindow 값이 이미 초기화가 되어 있으므로 메시지를 받지 못했고 트랜지션 동작이 하지 않게 되었다.

 

final boolean isTopOfTask() {
        if (mToken == null || mWindow == null) {
            return false;
        }
        try {
            return ActivityTaskManager.getService().isTopOfTask(getActivityToken());
        } catch (RemoteException e) {
            return false;
        }
    }

 

 

이는 안드로이드의 버그로 보여 issueTracker에 제보차 들려보았는데 꽤나 많은 사람들이 이미 issue를 올려놓았다. 그 중 

https://issuetracker.google.com/issues/137487202 해당 글에서 2021 4/29 기준 수정되었다는 글을 보게 되어 혹시나 하는 마음에

Android S(12) preview를 설치하여 동작 시켜보니 고쳐졌다. 아직 코드 확인은 안되서 어떻게 수정되었는지는 모르겠지만 해당 버그는 수정된것 같다~

 

반응형