/* * Copyright (C) 2019 ByteDance Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.bytedance.scene.group; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IdRes; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.Pair; import android.util.Log; import android.view.View; import android.view.ViewGroup; import com.bytedance.scene.Scene; import com.bytedance.scene.SceneTrace; import com.bytedance.scene.State; import com.bytedance.scene.animation.AnimationOrAnimator; import com.bytedance.scene.animation.AnimationOrAnimatorFactory; import com.bytedance.scene.navigation.NavigationScene; import com.bytedance.scene.parcel.ParcelConstants; import com.bytedance.scene.utlity.CancellationSignal; import com.bytedance.scene.utlity.SceneInstanceUtility; import com.bytedance.scene.utlity.SceneInternalException; import com.bytedance.scene.utlity.Utility; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.*; /** * Created by JiangQi on 7/30/18. * * Require GroupScene to be inResume state, if not, cache it. * * All operations are performed immediately, * batch operations are performed after commit, * and lifecycle callbacks are performed on the spot. */ class GroupRecord implements Parcelable { @IdRes int viewId = View.NO_ID; Scene scene; String tag; boolean isHidden = false; boolean isCurrentFocus = false; String sceneClassName; @Nullable Bundle bundle; protected GroupRecord(@NonNull Parcel in) { viewId = in.readInt(); tag = Utility.requireNonNull(in.readString(), "tag not found in Parcel"); isHidden = in.readByte() != 0; isCurrentFocus = in.readByte() != 0; sceneClassName = Utility.requireNonNull(in.readString(), "class name not found in Parcel"); } public static final Creator<GroupRecord> CREATOR = new Creator<GroupRecord>() { @Override public GroupRecord createFromParcel(Parcel in) { return new GroupRecord(in); } @Override public GroupRecord[] newArray(int size) { return new GroupRecord[size]; } }; public GroupRecord() { } static GroupRecord newInstance(@IdRes int viewId, @NonNull Scene scene, @NonNull String tag) { GroupRecord record = new GroupRecord(); record.viewId = viewId; record.scene = Utility.requireNonNull(scene, "scene can't be null"); record.tag = Utility.requireNonNull(tag, "tag can't be null"); record.sceneClassName = Utility.requireNonNull(scene.getClass().getName(), "Scene class name is null"); return record; } public void setHidden(boolean hidden) { this.isHidden = hidden; } public boolean isHidden() { return this.isHidden; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(viewId); dest.writeString(tag); dest.writeByte((byte) (isHidden ? 1 : 0)); dest.writeByte((byte) (isCurrentFocus ? 1 : 0)); dest.writeString(sceneClassName); } } class GroupRecordList { private static final String KEY_TAG = ParcelConstants.KEY_GROUP_RECORD_LIST; private List<GroupRecord> mSceneList = new ArrayList<>(); private final Map<Scene, GroupRecord> mSceneMap = new HashMap<>(); private final Map<String, GroupRecord> mTagMap = new HashMap<>(); public void add(GroupRecord record) { this.mSceneList.add(record); this.mSceneMap.put(record.scene, record); this.mTagMap.put(record.tag, record); } public void remove(GroupRecord record) { this.mSceneList.remove(record); this.mSceneMap.remove(record.scene); this.mTagMap.remove(record.tag); } public GroupRecord findByScene(Scene scene) { return this.mSceneMap.get(scene); } public GroupRecord findByTag(String tag) { return this.mTagMap.get(tag); } public GroupRecord findByView(View view) { GroupRecord groupRecord = null; for (GroupRecord record : mSceneList) { if (view.equals(record.scene.getView())) { groupRecord = record; break; } } return groupRecord; } public List<Scene> getChildSceneList() { List<Scene> sceneList = new ArrayList<>(); for (GroupRecord record : mSceneList) { sceneList.add(record.scene); } return Collections.unmodifiableList(sceneList); } public List<GroupRecord> getChildSceneRecordList() { return Collections.unmodifiableList(mSceneList); } public void saveToBundle(@NonNull Bundle bundle) { bundle.putParcelableArrayList(KEY_TAG, new ArrayList<Parcelable>(mSceneList)); } public void restoreFromBundle(@NonNull Context context, @NonNull Bundle bundle) { if (this.mSceneList != null && this.mSceneList.size() > 0) { throw new IllegalStateException("mSceneList size is not zero, Scene is added before restore"); } this.mSceneList = new ArrayList<>(bundle.<GroupRecord>getParcelableArrayList(KEY_TAG)); for (GroupRecord record : this.mSceneList) { record.scene = SceneInstanceUtility.getInstanceFromClassName(context, record.sceneClassName, null); this.mSceneMap.put(record.scene, record); this.mTagMap.put(record.tag, record); } } public void clear() { this.mSceneList.clear(); this.mSceneMap.clear(); this.mTagMap.clear(); } } class GroupSceneManager { private static final String TRACE_EXECUTE_OPERATION_TAG = "GroupSceneManager#executeOperation"; @NonNull private final GroupScene mGroupScene; @Nullable private ViewGroup mView; @NonNull private final GroupRecordList mSceneList = new GroupRecordList(); @NonNull private final Handler mHandler = new Handler(Looper.getMainLooper()); @NonNull private static final HashMap<Scene, CancellationSignal> SCENE_RUNNING_ANIMATION_CANCELLATION_SIGNAL_MAP = new HashMap<>(); @NonNull private final Set<Pair<Scene, String>> mCurrentTrackMoveStateSceneSet = new HashSet<>(); GroupSceneManager(@NonNull GroupScene groupScene) { this.mGroupScene = groupScene; } public void setView(ViewGroup view) { this.mView = view; } private static final Runnable EMPTY_RUNNABLE = new Runnable() { @Override public void run() { } }; private void executeOperation(final Operation operation) { SceneTrace.beginSection(TRACE_EXECUTE_OPERATION_TAG); operation.execute(EMPTY_RUNNABLE); SceneTrace.endSection(); } private boolean mIsInTransaction = false; private List<Operation> mOperationTransactionList = new ArrayList<>(); public void beginTransaction() { if (mIsInTransaction) { throw new IllegalStateException("you must call commitTransaction before another beginTransaction"); } mIsInTransaction = true; } /** * TODO: What if there are more than one Scene Tag repeated? */ void commitTransaction() { if (!mIsInTransaction) { throw new IllegalStateException("you must call beginTransaction before commitTransaction"); } if (mOperationTransactionList.size() > 0) { Iterator<Operation> iterator = mOperationTransactionList.iterator(); LinkedHashMap<Scene, List<Operation>> map = new LinkedHashMap<>(); while (iterator.hasNext()) { Operation operation = iterator.next(); List<Operation> list = map.get(operation.scene); if (list == null) { list = new ArrayList<>(); map.put(operation.scene, list); } list.add(operation); } Set<Scene> set = map.keySet(); for (Scene scene : set) { List<Operation> list = map.get(scene); State initState = scene.getState(); State dstState = list.get(list.size() - 1).state; boolean forceShow = list.get(list.size() - 1).forceShow; boolean forceHide = list.get(list.size() - 1).forceHide; boolean forceRemove = list.get(list.size() - 1).forceRemove; if (initState == dstState && !forceShow && !forceHide && !forceRemove) { //nothing changed continue; } if (initState == State.NONE) { AddOperation addOperation = getAddOperation(list); if (addOperation == null) { throw new IllegalStateException("you must add Scene first"); } if (findByTag(addOperation.tag) != null) { throw new IllegalStateException("already have a Scene with tag " + addOperation.tag); } executeOperation(new TransactionOperation(scene, addOperation.viewId, addOperation.tag, dstState, forceShow, forceHide, forceRemove)); } else { executeOperation(new TransactionOperation(scene, View.NO_ID, null, dstState, forceShow, forceHide, forceRemove)); } } mOperationTransactionList.clear(); } mIsInTransaction = false; } private static AddOperation getAddOperation(List<Operation> list) { for (int i = list.size() - 1; i >= 0; i--) { Operation operation = list.get(i); if (operation instanceof AddOperation) { return (AddOperation) operation; } } return null; } /** * GroupScene don't allow child Scene modify its state in its lifecycle method,for example * 1 child Scene invoke parent's remove method to remove itself in its onActivityCreated lifecycle method * 2 child Scene invoke parent's remove method to hide itself in its onResume lifecycle method * but child Scene can invoke parent's add/remove/show/hide to operate other child Scene */ private void checkStateChange(@NonNull Scene scene) { for (Pair<Scene, String> pair : this.mCurrentTrackMoveStateSceneSet) { if (pair.first == scene) { throw new IllegalStateException("Cant add/remove/show/hide " + scene.getClass().getCanonicalName() + " before it finish previous add/remove/show/hide operation or in its lifecycle method"); } } } private void beginTrackSceneStateChange(@NonNull Scene scene) { for (Pair<Scene, String> pair : this.mCurrentTrackMoveStateSceneSet) { if (pair.first == scene) { throw new SceneInternalException("Target scene " + scene.getClass().getCanonicalName() + " is already tracked"); } } //forbid NavigationScene execute navigation stack operation immediately, otherwise GroupScene may sync lifecycle to child, //then throw SceneInternalException("Target scene is already tracked") NavigationScene navigationScene = mGroupScene.getNavigationScene(); String suppressTag = null; if (navigationScene != null) { suppressTag = navigationScene.beginSuppressStackOperation(scene.toString()); } else { //execute GroupScene operations before GroupScene attached or after detached suppressTag = null; } this.mCurrentTrackMoveStateSceneSet.add(Pair.create(scene, suppressTag)); } private void endTrackSceneStateChange(@NonNull Scene scene) { Pair<Scene, String> target = null; for (Pair<Scene, String> pair : this.mCurrentTrackMoveStateSceneSet) { if (pair.first == scene) { target = pair; break; } } if (target == null) { throw new SceneInternalException("Target scene " + scene.getClass().getCanonicalName() + " is not tracked"); } String suppressTag = target.second; if (suppressTag != null) { mGroupScene.getNavigationScene().endSuppressStackOperation(target.second); } this.mCurrentTrackMoveStateSceneSet.remove(target); } public void add(int viewId, Scene scene, String tag, AnimationOrAnimatorFactory animationOrAnimatorFactory) { checkStateChange(scene); final Operation operation = new AddOperation(viewId, scene, tag, animationOrAnimatorFactory); if (mIsInTransaction) { mOperationTransactionList.add(operation); } else { executeOperation(operation); } } public void remove(Scene scene, AnimationOrAnimatorFactory animationOrAnimatorFactory) { checkStateChange(scene); if (!mIsInTransaction && mSceneList.findByScene(scene) == null) { throw new IllegalStateException("Target scene is not find"); } final Operation operation = new RemoveOperation(scene, animationOrAnimatorFactory); if (mIsInTransaction) { mOperationTransactionList.add(operation); } else { executeOperation(operation); } } public void clear() { mSceneList.clear(); } public void hide(Scene scene, AnimationOrAnimatorFactory animationOrAnimatorFactory) { checkStateChange(scene); if (!mIsInTransaction && mSceneList.findByScene(scene) == null) { throw new IllegalStateException("Target scene is not find"); } final Operation operation = new HideOperation(scene, animationOrAnimatorFactory); if (mIsInTransaction) { mOperationTransactionList.add(operation); } else { executeOperation(operation); } } public void show(Scene scene, AnimationOrAnimatorFactory animationOrAnimatorFactory) { checkStateChange(scene); if (!mIsInTransaction && mSceneList.findByScene(scene) == null) { throw new IllegalStateException("Target scene is not find"); } final Operation operation = new ShowOperation(scene, animationOrAnimatorFactory); if (mIsInTransaction) { mOperationTransactionList.add(operation); } else { executeOperation(operation); } } void dispatchChildrenState(State state) { List<Scene> childSceneList = this.getChildSceneList(); for (int i = 0; i <= childSceneList.size() - 1; i++) { final Scene scene = childSceneList.get(i); //may be removed by other child Scene if (containsScene(scene)) { beginTrackSceneStateChange(scene); GroupSceneManager.moveState(mGroupScene, scene, state, false, new Runnable() { @Override public void run() { endTrackSceneStateChange(scene); } }); } } } void dispatchVisibleChildrenState(State state) { List<GroupRecord> list = this.getChildSceneRecordList(); for (int i = 0; i <= list.size() - 1; i++) { GroupRecord record = list.get(i); if (!record.isHidden) { final Scene scene = record.scene; //may be removed by other child Scene if (containsScene(scene)) { beginTrackSceneStateChange(scene); GroupSceneManager.moveState(mGroupScene, record.scene, state, false, new Runnable() { @Override public void run() { endTrackSceneStateChange(scene); } }); } } } } @Nullable GroupRecord findByScene(@NonNull Scene scene) { return mSceneList.findByScene(scene); } @Nullable GroupRecord findByTag(@NonNull String tag) { return mSceneList.findByTag(tag); } @Nullable GroupRecord findByView(@NonNull View view) { return mSceneList.findByView(view); } int findSceneViewId(@NonNull Scene scene) { return mSceneList.findByScene(scene).viewId; } @NonNull String findSceneTag(@NonNull Scene scene) { return mSceneList.findByScene(scene).tag; } @NonNull List<Scene> getChildSceneList() { return mSceneList.getChildSceneList(); } private List<GroupRecord> getChildSceneRecordList() { return mSceneList.getChildSceneRecordList(); } private static final String KEY_TAG = ParcelConstants.KEY_GROUP_SCENE_MANAGER_TAG; void saveToBundle(@NonNull Bundle bundle) { this.mSceneList.saveToBundle(bundle); ArrayList<Bundle> bundleList = new ArrayList<>(); List<Scene> childSceneList = this.getChildSceneList(); for (int i = 0; i <= childSceneList.size() - 1; i++) { Scene scene = childSceneList.get(i); Bundle sceneBundle = new Bundle(); scene.dispatchSaveInstanceState(sceneBundle); bundleList.add(sceneBundle); } bundle.putParcelableArrayList(KEY_TAG, bundleList); } void restoreFromBundle(@NonNull Context context, @NonNull Bundle bundle) { this.mSceneList.restoreFromBundle(context, bundle); List<GroupRecord> childSceneList = this.mSceneList.getChildSceneRecordList(); if (childSceneList.size() == 0) { return; } ArrayList<Bundle> bundleList = bundle.getParcelableArrayList(KEY_TAG); for (int i = 0; i <= childSceneList.size() - 1; i++) { GroupRecord record = childSceneList.get(i); final Scene scene = record.scene; record.bundle = bundleList.get(i); //may be removed by other child Scene, but because restoreFromBundle is invoked at GroupScene onCreate, //so this should not happen if (!containsScene(scene)) { throw new SceneInternalException("Scene is not found"); } beginTrackSceneStateChange(scene); moveState(this.mGroupScene, scene, mGroupScene.getState(), false, new Runnable() { @Override public void run() { endTrackSceneStateChange(scene); } }); } } private abstract class Operation { @NonNull final Scene scene; @NonNull final State state; final boolean forceShow; /** * Forced display and hiding must be distinguished from normal related to the life cycle. * Can't just rely on DstState, otherwise it's very easy to make mistakes, mix together mess */ final boolean forceHide; final boolean forceRemove; Operation(@NonNull Scene scene, @NonNull State state, boolean forceShow, boolean forceHide, boolean forceRemove) { this.scene = scene; this.state = state; this.forceShow = forceShow; this.forceHide = forceHide; this.forceRemove = forceRemove; } abstract void execute(@NonNull Runnable operationEndAction); } private boolean containsScene(@NonNull Scene scene) { List<GroupRecord> recordList = getChildSceneRecordList(); for (int i = 0; i < recordList.size(); i++) { if (recordList.get(i).scene == scene) { return true; } } return false; } private abstract class MoveStateOperation extends Operation { @IdRes final int viewId; @Nullable final String tag; @NonNull final State dstState; MoveStateOperation(@NonNull Scene scene, @IdRes int viewId, @Nullable String tag, @NonNull State dstState, boolean forceShow, boolean forceHide, boolean forceRemove) { super(scene, dstState, forceShow, forceHide, forceRemove); if (forceShow && forceHide) { throw new IllegalArgumentException("cant forceShow with forceHide"); } this.viewId = viewId; this.tag = tag; this.dstState = dstState; } @Override final void execute(@NonNull Runnable operationEndAction) { CancellationSignal cancellationSignal = SCENE_RUNNING_ANIMATION_CANCELLATION_SIGNAL_MAP.get(scene); if (cancellationSignal != null) { cancellationSignal.cancel(); if (SCENE_RUNNING_ANIMATION_CANCELLATION_SIGNAL_MAP.get(scene) != null) { throw new SceneInternalException("CancellationSignal cancel callback should remove target Scene from CancellationSignal map"); } } if (!containsScene(scene)) { if (scene.getState() == State.NONE) { Utility.requireNonNull(tag, "tag can't be null"); mSceneList.add(GroupRecord.newInstance(viewId, scene, tag)); } else { throw new SceneInternalException("Scene state is " + scene.getState().name + " but it is not added to record list"); } } if (forceShow) { mSceneList.findByScene(scene).isHidden = false; } if (forceHide) { mSceneList.findByScene(scene).isHidden = true; } boolean executeStateChange = scene.getState() != dstState; executeOnStart(executeStateChange); beginTrackSceneStateChange(scene); moveState(mGroupScene, scene, dstState, forceRemove, new Runnable() { @Override public void run() { endTrackSceneStateChange(scene); } }); if (forceRemove) { mSceneList.remove(mSceneList.findByScene(scene)); } executeOnFinish(executeStateChange); operationEndAction.run(); } protected void executeOnStart(boolean stateChanged) { } protected void executeOnFinish(boolean stateChanged) { } } private static State getMinState(State groupState, State state) { if (groupState.value < state.value) { return groupState; } else { return state; } } private final class TransactionOperation extends MoveStateOperation { TransactionOperation(@NonNull Scene scene, int viewId, @Nullable String tag, @NonNull State dstState, boolean forceShow, boolean forceHide, boolean forceRemove) { super(scene, viewId, tag, dstState, forceShow, forceHide, forceRemove); } @Override protected void executeOnStart(boolean stateChanged) { super.executeOnStart(stateChanged); View view = this.scene.getView(); if (view != null && forceShow) { setSceneViewVisibility(scene, View.VISIBLE); } } @Override protected void executeOnFinish(boolean stateChanged) { super.executeOnFinish(stateChanged); View view = this.scene.getView(); if (view != null && forceHide) { setSceneViewVisibility(scene, View.GONE); } } } private final class AddOperation extends MoveStateOperation { final int viewId; final String tag; final AnimationOrAnimatorFactory animationOrAnimatorFactory; private AddOperation(int viewId, Scene scene, String tag, AnimationOrAnimatorFactory animationOrAnimatorFactory) { super(scene, viewId, tag, getMinState(State.RESUMED, mGroupScene.getState()), true, false, false); this.viewId = viewId; this.tag = tag; this.animationOrAnimatorFactory = animationOrAnimatorFactory; } @Override protected void executeOnFinish(boolean stateChanged) { super.executeOnFinish(stateChanged); if (!stateChanged) { return; } final AnimationOrAnimator animationOrAnimator = animationOrAnimatorFactory.getAnimationOrAnimator(); if (animationOrAnimator == null) { return; } View view = this.scene.getView(); if (view == null) { return; } animationOrAnimator.addEndAction(new Runnable() { @Override public void run() { SCENE_RUNNING_ANIMATION_CANCELLATION_SIGNAL_MAP.remove(scene); } }); SCENE_RUNNING_ANIMATION_CANCELLATION_SIGNAL_MAP.put(this.scene, new CancellationSignal() { @Override public void cancel() { super.cancel(); animationOrAnimator.end(); } }); animationOrAnimator.start(view); } } private final class RemoveOperation extends MoveStateOperation { private final AnimationOrAnimatorFactory animationOrAnimatorFactory; private final boolean canAnimation; private final View sceneView; private final ViewGroup parentViewGroup; private boolean isAnimating = false; private int dstVisibility = View.VISIBLE; private RemoveOperation(Scene scene, AnimationOrAnimatorFactory animationOrAnimatorFactory) { super(scene, View.NO_ID, null, State.NONE, false, false, true); this.animationOrAnimatorFactory = animationOrAnimatorFactory; this.canAnimation = scene.getView() != null && scene.getView().getParent() != null; if (this.canAnimation) { this.sceneView = scene.getView(); this.parentViewGroup = (ViewGroup) this.sceneView.getParent(); } else { this.sceneView = null; this.parentViewGroup = null; } } @Override protected void executeOnStart(boolean stateChanged) { super.executeOnStart(stateChanged); if (!stateChanged) { return; } if (!canAnimation) { return; } final AnimationOrAnimator animationOrAnimator = this.animationOrAnimatorFactory.getAnimationOrAnimator(); if (animationOrAnimator == null) { return; } /* * View try execute remove() without call the measure() or layout() first, * will result in a 0 of height and width, which can not be animated. */ if (this.parentViewGroup != null && (this.sceneView.getWidth() == 0 || this.sceneView.getHeight() == 0)) { Log.w("GroupScene", "Scene view width or height is zero, skip animation"); return; } animationOrAnimator.addEndAction(new Runnable() { @Override public void run() { SCENE_RUNNING_ANIMATION_CANCELLATION_SIGNAL_MAP.remove(scene); parentViewGroup.endViewTransition(sceneView); sceneView.setVisibility(dstVisibility); } }); SCENE_RUNNING_ANIMATION_CANCELLATION_SIGNAL_MAP.put(scene, new CancellationSignal() { @Override public void cancel() { super.cancel(); animationOrAnimator.end(); } }); this.parentViewGroup.startViewTransition(this.sceneView); animationOrAnimator.start(this.sceneView); this.isAnimating = true; } @Override protected void executeOnFinish(boolean stateChanged) { super.executeOnFinish(stateChanged); if (!stateChanged) { return; } if (!this.isAnimating) { return; } this.dstVisibility = this.sceneView.getVisibility(); this.sceneView.setVisibility(View.VISIBLE); } } private final class HideOperation extends MoveStateOperation { private final AnimationOrAnimatorFactory animationOrAnimatorFactory; private HideOperation(Scene scene, AnimationOrAnimatorFactory animationOrAnimatorFactory) { super(scene, View.NO_ID, null, getMinState(State.ACTIVITY_CREATED, mGroupScene.getState()), false, true, false); this.animationOrAnimatorFactory = animationOrAnimatorFactory; } @Override protected void executeOnFinish(boolean stateChanged) { super.executeOnFinish(stateChanged); final View sceneView = this.scene.getView(); if (sceneView == null) { return; } setSceneViewVisibility(scene, View.GONE); if (!stateChanged) { return; } final AnimationOrAnimator animationOrAnimator = this.animationOrAnimatorFactory.getAnimationOrAnimator(); if (animationOrAnimator == null) { return; } final int dstVisibility = sceneView.getVisibility(); sceneView.setVisibility(View.VISIBLE); animationOrAnimator.addEndAction(new Runnable() { @Override public void run() { SCENE_RUNNING_ANIMATION_CANCELLATION_SIGNAL_MAP.remove(scene); sceneView.setVisibility(dstVisibility); } }); SCENE_RUNNING_ANIMATION_CANCELLATION_SIGNAL_MAP.put(scene, new CancellationSignal() { @Override public void cancel() { super.cancel(); animationOrAnimator.end(); } }); animationOrAnimator.start(this.scene.getView()); } } private final class ShowOperation extends MoveStateOperation { private final AnimationOrAnimatorFactory animationOrAnimatorFactory; private ShowOperation(Scene scene, AnimationOrAnimatorFactory animationOrAnimatorFactory) { super(scene, View.NO_ID, null, getMinState(State.RESUMED, mGroupScene.getState()), true, false, false); this.animationOrAnimatorFactory = animationOrAnimatorFactory; } @Override protected void executeOnStart(boolean stateChanged) { super.executeOnStart(stateChanged); final View sceneView = this.scene.getView(); if (sceneView == null) { return; } setSceneViewVisibility(scene, View.VISIBLE); } @Override protected void executeOnFinish(boolean stateChanged) { super.executeOnFinish(stateChanged); if (!stateChanged) { return; } final View sceneView = this.scene.getView(); if (sceneView == null) { return; } final AnimationOrAnimator animationOrAnimator = this.animationOrAnimatorFactory.getAnimationOrAnimator(); if (animationOrAnimator == null) { return; } animationOrAnimator.addEndAction(new Runnable() { @Override public void run() { SCENE_RUNNING_ANIMATION_CANCELLATION_SIGNAL_MAP.remove(scene); } }); SCENE_RUNNING_ANIMATION_CANCELLATION_SIGNAL_MAP.put(scene, new CancellationSignal() { @Override public void cancel() { super.cancel(); animationOrAnimator.end(); } }); animationOrAnimator.start(sceneView); } } /** * @hide */ @IntDef({View.VISIBLE, View.INVISIBLE, View.GONE}) @Retention(RetentionPolicy.SOURCE) @interface Visibility { } private static void setSceneViewVisibility(@NonNull Scene scene, @Visibility int visibility) { View view = scene.getView(); if (view.getVisibility() != visibility) { view.setVisibility(visibility); } } /** * forceRemove value is true when be invoked by GroupScene.remove() */ private static void moveState(@NonNull GroupScene groupScene, @NonNull Scene scene, @NonNull State to, boolean forceRemove, @Nullable Runnable endAction) { State currentState = scene.getState(); if (currentState == to) { if (endAction != null) { endAction.run(); } return; } GroupRecord record = null; Bundle sceneBundle = null; if (currentState.value < to.value) { switch (currentState) { case NONE: scene.dispatchAttachActivity(groupScene.requireActivity()); scene.dispatchAttachScene(groupScene); record = groupScene.getGroupSceneManager().findByScene(scene); sceneBundle = record.bundle; scene.dispatchCreate(sceneBundle); ViewGroup containerView = groupScene.findContainerById(groupScene.getGroupSceneManager().findSceneViewId(scene)); scene.dispatchCreateView(sceneBundle, containerView); containerView.addView(scene.getView()); if (record.isHidden()) { setSceneViewVisibility(scene, View.GONE); } moveState(groupScene, scene, to, forceRemove, endAction); break; case VIEW_CREATED: record = groupScene.getGroupSceneManager().findByScene(scene); sceneBundle = record.bundle; scene.dispatchActivityCreated(sceneBundle); record.bundle = null; moveState(groupScene, scene, to, forceRemove, endAction); break; case ACTIVITY_CREATED: scene.dispatchStart(); moveState(groupScene, scene, to, forceRemove, endAction); break; case STARTED: scene.dispatchResume(); moveState(groupScene, scene, to, forceRemove, endAction); break; default: throw new SceneInternalException("unreachable state case " + currentState.getName()); } } else { switch (currentState) { case RESUMED: scene.dispatchPause(); moveState(groupScene, scene, to, forceRemove, endAction); break; case STARTED: scene.dispatchStop(); moveState(groupScene, scene, to, forceRemove, endAction); break; case ACTIVITY_CREATED: if (to == State.VIEW_CREATED) { throw new IllegalArgumentException("cant switch state ACTIVITY_CREATED to VIEW_CREATED"); } //continue case VIEW_CREATED: View view = scene.getView(); scene.dispatchDestroyView(); if (forceRemove) { /* * case 1: Scene is removed from parent GroupScene, we should remove its view from parent's view hierarchy * case 2: Parent GroupScene is pop from grandparent NavigationScene, this child Scene's state will sync to * destroy state, but its view should not be removed, otherwise, pop animation will be hard to see. */ Utility.removeFromParentView(view); } scene.dispatchDestroy(); scene.dispatchDetachScene(); scene.dispatchDetachActivity(); moveState(groupScene, scene, to, forceRemove, endAction); break; default: throw new SceneInternalException("unreachable state case " + currentState.getName()); } } } }