back stack を保持したまま現在のfragmentを入れ替える

素直に入れ替えると「back stackに記録された最後のフラグメントとcontainerに現在保持されているフラグメントがマッチしない」という問題が発生します。

back stack の内容を書き換えるのは難易度が高いので、別の対応を考えてみましょう。

  • back stack をpopする時に、現在のフラグメントがback stackに記録されていないならremoveする

コードは後述。これだけでもとりあえず動きますね。ライフサイクルイベントがこんな順序で発生します。

#----------------------------------------
# アプリを起動してフラグメントを1つ追加
(ActMain onApplyThemeResource)ActMain onApplyThemeResource
(ActMain onCreate(1arg)
	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onContentChanged)
)ActMain onCreate(1arg)
(ActMain onStart
	(FragmentTest#1 onAttach)
	(ActMain onAttachFragment(support.Fragment))
	(FragmentTest#1 onCreate state=null,is_view_destroyed=false)
	(FragmentTest#1 onCreateView state=null,is_view_destroyed=false)
	(FragmentTest#1 onCreateAnimation)
	(FragmentTest#1 onViewCreated state=null,is_view_destroyed=false)
	(FragmentTest#1 onActivityCreated state=null)
	(FragmentTest#1 onViewStateRestored state=null)
	(FragmentTest#1 onStart)
)ActMain onStart
(ActMain onPostCreate(1arg)
	(ActMain onTitleChanged)
)ActMain onPostCreate(1arg)
(ActMain onResume)
(ActMain onPostResume
	(ActMain onResumeFragments
		(FragmentTest#1 onResume)
	)ActMain onResumeFragments
)ActMain onPostResume
(ActMain onAttachedToWindow)
(ActMain onWindowFocusChanged)
(ActMain onEnterAnimationComplete)

#----------------------------------------
# 状態の確認
(ActMain onUserInteraction)
backstack: count=0
getFragments: FragmentTest#1
findFragmentById: FragmentTest#1

#----------------------------------------
# 現在のフラグメントを差し替えて、バックスタックに追加する
(ActMain onUserInteraction)
(FragmentTest#1 onPause)
(FragmentTest#1 onStop)
(FragmentTest#1 onDestroyView)
(FragmentTest#1 onCreateAnimation)
(FragmentTest#2 onAttach)
(ActMain onAttachFragment(support.Fragment))
(FragmentTest#2 onCreate state=null,is_view_destroyed=false)
(FragmentTest#2 onCreateView state=null,is_view_destroyed=false)
(FragmentTest#2 onCreateAnimation)
(FragmentTest#2 onViewCreated state=null,is_view_destroyed=false)
(FragmentTest#2 onActivityCreated state=null)
(FragmentTest#2 onViewStateRestored state=null)
(FragmentTest#2 onStart)
(FragmentTest#2 onResume)

#----------------------------------------
# 状態の確認
(ActMain onUserInteraction)
backstack: count=1
backstack[0]: FragmentTest#2
getFragments: FragmentTest#1
getFragments: FragmentTest#2
findFragmentById: FragmentTest#2

#------------------------------------
# 現在のフラグメントを差し替えるが、バックスタックは変更しない
(ActMain onUserInteraction)
replace FragmentTest#2 => FragmentTest#3
(FragmentTest#2 onPause)
(FragmentTest#2 onStop)
(FragmentTest#2 onDestroyView)
(FragmentTest#2 onCreateAnimation)
(FragmentTest#3 onAttach)
(ActMain onAttachFragment(support.Fragment))
(FragmentTest#3 onCreate state=null,is_view_destroyed=false)
(FragmentTest#3 onCreateView state=null,is_view_destroyed=false)
(FragmentTest#3 onCreateAnimation)
(FragmentTest#3 onViewCreated state=null,is_view_destroyed=false)
(FragmentTest#3 onActivityCreated state=null)
(FragmentTest#3 onViewStateRestored state=null)
(FragmentTest#3 onStart)
(FragmentTest#3 onResume)

#----------------------------------------
# 状態の確認
(ActMain onUserInteraction)
backstack: count=1
backstack[0]: FragmentTest#2
getFragments: FragmentTest#1
getFragments: FragmentTest#2
getFragments: FragmentTest#3
findFragmentById: FragmentTest#3

#-------------------------------------------
# ホームキーを押した(1回目)
(ActMain onUserInteraction)
(ActMain onUserLeaveHint)
(ActMain onPause
	(FragmentTest#3 onPause)
)ActMain onPause
(ActMain onWindowFocusChanged)
(ActMain onCreateDescription)
(ActMain onSaveInstanceState
	(FragmentTest#1 onSaveInstanceState is_created=true,is_view_created=false,is_view_destroyed=true,is_resumed=false)
	(FragmentTest#2 onSaveInstanceState is_created=true,is_view_created=false,is_view_destroyed=true,is_resumed=false)
	(FragmentTest#3 onSaveInstanceState is_created=true,is_view_created=true,is_view_destroyed=false,is_resumed=false)
)ActMain onSaveInstanceState
(ActMain onStop
	(FragmentTest#3 onStop)
)ActMain onStop
(ActMain onTrimMemory)
(ActMain onDestroy
	(FragmentTest#1 onDestroy is_view_destroyed=true)
	(FragmentTest#1 onDetach)
	(FragmentTest#2 onDestroy is_view_destroyed=true)
	(FragmentTest#2 onDetach)
	(FragmentTest#3 onDestroyView)
	(FragmentTest#3 onDestroy is_view_destroyed=true)
	(FragmentTest#3 onDetach)
)ActMain onDestroy
(ActMain onDetachedFromWindow)

#----------------------------------------
# 履歴からアプリを復元
(ActMain onApplyThemeResource)
(ActMain onCreate(1arg)
	(FragmentTest#1 onAttach)
	(ActMain onAttachFragment(support.Fragment))
	(FragmentTest#1 onCreate state=(Bundle),is_view_destroyed=false)
	(FragmentTest#2 onAttach)
	(ActMain onAttachFragment(support.Fragment))
	(FragmentTest#2 onCreate state=(Bundle),is_view_destroyed=false)
	(FragmentTest#3 onAttach)
	(ActMain onAttachFragment(support.Fragment))
	(FragmentTest#3 onCreate state=(Bundle),is_view_destroyed=false)
	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onContentChanged)
)ActMain onCreate(1arg)
(ActMain onStart
	(FragmentTest#3 onCreateView state=(Bundle),is_view_destroyed=false)
	(FragmentTest#3 onCreateAnimation)
	(FragmentTest#3 onViewCreated state=(Bundle),is_view_destroyed=false)
	(FragmentTest#3 onActivityCreated state=(Bundle))
	(FragmentTest#3 onViewStateRestored state=(Bundle))
	(FragmentTest#3 onStart)
)ActMain onStart
(ActMain onRestoreInstanceState(1arg))
(ActMain onPostCreate(1arg)
	(ActMain onTitleChanged)
)ActMain onPostCreate(1arg)
(ActMain onResume)
(ActMain onPostResume
	(ActMain onResumeFragments
		(FragmentTest#3 onResume)
	)ActMain onResumeFragments
)ActMain onPostResume
(ActMain onAttachedToWindow)
(ActMain onWindowFocusChanged)
(ActMain onEnterAnimationComplete)

#----------------------------------------
# 状態の確認
(ActMain onUserInteraction)
backstack: count=1
backstack[0]: FragmentTest#2
getFragments: FragmentTest#1
getFragments: FragmentTest#2
getFragments: FragmentTest#3
findFragmentById: FragmentTest#3

#----------------------------------------
# ホームキーを押した(2回目)
(ActMain onUserInteraction)
(ActMain onUserLeaveHint)
(ActMain onPause
	(FragmentTest#3 onPause)
)ActMain onPause
(ActMain onWindowFocusChanged)
(ActMain onCreateDescription)
(ActMain onSaveInstanceState
	(FragmentTest#1 onSaveInstanceState is_created=true,is_view_created=false,is_view_destroyed=false,is_resumed=false)
	(FragmentTest#2 onSaveInstanceState is_created=true,is_view_created=false,is_view_destroyed=false,is_resumed=false)
	(FragmentTest#3 onSaveInstanceState is_created=true,is_view_created=true,is_view_destroyed=false,is_resumed=false)
)ActMain onSaveInstanceState
(ActMain onStop
	(FragmentTest#3 onStop)
)ActMain onStop
(ActMain onTrimMemory)
(ActMain onDestroy
	(FragmentTest#1 onDestroy is_view_destroyed=false)
	(FragmentTest#1 onDetach)

	(FragmentTest#2 onDestroy is_view_destroyed=false)
	(FragmentTest#2 onDetach)

	(FragmentTest#3 onDestroyView)
	(FragmentTest#3 onDestroy is_view_destroyed=true)
	(FragmentTest#3 onDetach)
)ActMain onDestroy
(ActMain onDetachedFromWindow)

#----------------------------------------
# 履歴からアプリを復元
(ActMain onApplyThemeResource)
(ActMain onCreate(1arg)

	(FragmentTest#1 onAttach)
	(ActMain onAttachFragment(support.Fragment))
	(FragmentTest#1 onCreate state=(Bundle),is_view_destroyed=false)

	(FragmentTest#2 onAttach)
	(ActMain onAttachFragment(support.Fragment))
	(FragmentTest#2 onCreate state=(Bundle),is_view_destroyed=false)

	(FragmentTest#3 onAttach)FragmentTest#3 onAttach
	(ActMain onAttachFragment(support.Fragment))
	(FragmentTest#3 onCreate state=(Bundle),is_view_destroyed=false)

	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onWindowAttributesChanged)
	(ActMain onContentChanged)
)ActMain onCreate(1arg)
(ActMain onStart
	(FragmentTest#3 onCreateView state=(Bundle),is_view_destroyed=false )
	(FragmentTest#3 onCreateAnimation)
	(FragmentTest#3 onViewCreated state=(Bundle),is_view_destroyed=false )
	(FragmentTest#3 onActivityCreated state=(Bundle) )
	(FragmentTest#3 onViewStateRestored state=(Bundle) )
	(FragmentTest#3 onStart)
)ActMain onStart
(ActMain onRestoreInstanceState(1arg))
(ActMain onPostCreate(1arg)
	(ActMain onTitleChanged)
)ActMain onPostCreate(1arg)
(ActMain onResume)
(ActMain onPostResume
	(ActMain onResumeFragments
		(FragmentTest#3 onResume)
	)ActMain onResumeFragments
)ActMain onPostResume
(ActMain onAttachedToWindow)ActMain onAttachedToWindow
(ActMain onWindowFocusChanged)ActMain onWindowFocusChanged
(ActMain onEnterAnimationComplete)ActMain onEnterAnimationComplete

#------------------------------------------
# 状態の確認
(ActMain onUserInteraction)
backstack: count=1
backstack[0]: FragmentTest#2
getFragments: FragmentTest#1
getFragments: FragmentTest#2
getFragments: FragmentTest#3
findFragmentById: FragmentTest#3

#------------------------------------------
# 戻るキーを押した

(ActMain onUserInteraction )
(ActMain onKeyDown )
(ActMain onUserInteraction)
(ActMain onKeyUp
	(ActMain onBackPressed
page_pop: back stack name and current fragment are not match. remove current fragment.
	)ActMain onBackPressed
)ActMain onKeyUp

(FragmentTest#3 onPause )
(FragmentTest#3 onStop)
(FragmentTest#3 onDestroyView )
(FragmentTest#3 onCreateAnimation )

(FragmentTest#2 onDestroy is_view_destroyed=false )
(FragmentTest#2 onDetach )

(FragmentTest#1 onCreateView state=(Bundle),is_view_destroyed=false )
(FragmentTest#1 onCreateAnimation )
(FragmentTest#1 onViewCreated state=(Bundle),is_view_destroyed=false )
(FragmentTest#1 onActivityCreated state=(Bundle) )
(FragmentTest#1 onViewStateRestored state=(Bundle) )
(FragmentTest#1 onStart)
(FragmentTest#1 onResume)

(FragmentTest#3 onDestroy is_view_destroyed=true)
(FragmentTest#3 onDetach)

#------------------------------------------
# 状態の確認
(ActMain onUserInteraction)
backstack: count=0
getFragments: FragmentTest#1
getFragments: null
getFragments: null
findFragmentById: FragmentTest#1

#------------------------------------------
# 戻るキーを押した

(ActMain onUserInteraction)
(ActMain onKeyDown)

(ActMain onUserInteraction)
(ActMain onKeyUp
	(ActMain onBackPressed)
)ActMain onKeyUp

(ActMain onWindowFocusChanged)

(ActMain onPause
	(FragmentTest#1 onPause )
)ActMain onPause

(ActMain onStop
	(FragmentTest#1 onStop )
)ActMain onStop

(ActMain onDestroy
	(FragmentTest#1 onDestroyView )
	(FragmentTest#1 onDestroy is_view_destroyed=true )
	(FragmentTest#1 onDetach )
)ActMain onDestroy

(ActMain onDetachedFromWindow )

このアプローチは入れ替えの時点でback stackを操作しようとするよりもラクですが、問題もあります。

問題1: 入れ替えによって表示されなくなったフラグメントが back stack に保持され続ける

onDestroyView などのタイミングでビューへの参照を外すなどしてFragmentの消費メモリを軽減すると良いです。ただし onDestroyView が呼ばれた時点ではビューはまだ画面に表示されています。Bitmapの後処理を行うタイミングはFragment#onCreateAnimation で作成するアニメーションにセットするリスナーの onAnimationEnd イベントなどが適しているのかもしれません。

問題2: ホーム画面が再生成される際に、FragmentTransaction#setCustomAnimation() に指定した情報の一部が失われる

他アプリに移動せずに操作していると正常に動くのですが、他アプリに移動してまたテストアプリに戻ってきてから操作すると、popBackStack() した際のアニメーションが一部行われない場合がありました。

onCreateAnimation に渡される引数を追いかけてみるとこんな感じです。
バックスタックの頭のF#6と、バックスタックを更新しない入れ替えを行った F#3,F#5でnextAnimの情報が失われています。

# 初期配置。アニメーションは指定していない
(FragmentTest#1 onCreateAnimation transit=0,enter=true,nextAnim=0

# f#1 がexit, f#2がenter (通常の入れ替え)
(FragmentTest#1 onCreateAnimation transit=0,enter=false,nextAnim=2131034118
(FragmentTest#2 onCreateAnimation transit=0,enter=true,nextAnim=2131034121

# f#2 がexit, f#3がenter (back stackを更新しない入れ替え)
(FragmentTest#2 onCreateAnimation transit=0,enter=false,nextAnim=2131034118
(FragmentTest#3 onCreateAnimation transit=0,enter=true,nextAnim=2131034121

# f#3 がexit, f#4がenter (通常の入れ替え)
(FragmentTest#3 onCreateAnimation transit=0,enter=false,nextAnim=2131034118
(FragmentTest#4 onCreateAnimation transit=0,enter=true,nextAnim=2131034121

# f#4 がexit, f#5がenter (back stackを更新しない入れ替え)
(FragmentTest#4 onCreateAnimation transit=0,enter=false,nextAnim=2131034118
(FragmentTest#5 onCreateAnimation transit=0,enter=true,nextAnim=2131034121

# f#5 がexit, f#6がenter (通常の入れ替え)
(FragmentTest#5 onCreateAnimation transit=0,enter=false,nextAnim=2131034118
(FragmentTest#6 onCreateAnimation transit=0,enter=true,nextAnim=2131034121

# 状態の確認
backstack: count=3
backstack[0]: FragmentTest#2
backstack[1]: FragmentTest#4
backstack[2]: FragmentTest#6
getFragments: FragmentTest#1
getFragments: FragmentTest#2
getFragments: FragmentTest#3
getFragments: FragmentTest#4
getFragments: FragmentTest#5
getFragments: FragmentTest#6
findFragmentById: FragmentTest#6

# 他アプリから戻ってきた
(FragmentTest#6 onCreateAnimation transit=0,enter=true,nextAnim=0

# 他アプリから戻ってきた
(FragmentTest#6 onCreateAnimation transit=0,enter=true,nextAnim=0

# f#6 がexit, f#5がenter
(FragmentTest#6 onCreateAnimation transit=0,enter=false,nextAnim=0
(FragmentTest#5 onCreateAnimation transit=0,enter=true,nextAnim=0

# f#5 がexit, f#3がenter
(FragmentTest#5 onCreateAnimation transit=0,enter=false,nextAnim=2131034119
(FragmentTest#3 onCreateAnimation transit=0,enter=true,nextAnim=0

# f#3 がexit, f#1がenter
(FragmentTest#3 onCreateAnimation transit=0,enter=false,nextAnim=2131034119
(FragmentTest#1 onCreateAnimation transit=0,enter=true,nextAnim=0

対策ですが、fragment を pop した直後だけ onCreateAnimation でnextAnimを適切に補完できるようにします。今回は以下のようなことを行いました。

  • Fragment#toString() をオーバライドして、フラグメントのインスタンスを特定できるような名前を返す
  • addToBackStack(name) に指定する名前を、除去したフラグメントと追加したフラグメントの名前を含めたパターンにする
  • popBackStackする直前にBackStackEntry#getName()でそのパターンを取り出し、補完するアニメーションのリソースIDをフラグメントに渡しておく
  • フラグメントのonCreateAnimation では、もしリソースIDが渡されていたらそれを使ってアニメーションを生成する。渡されたリソースIDはリセットして、別のタイミングで呼びだされた時に使わないようにする

この問題はこれで解決したようです。

コード例

// Fragment 側


static final String ARG_NAME = "name";
static final FragmentTest create(int idx){
	Bundle b = new Bundle();
	b.putString( ARG_NAME, String.format( "%s", idx ) );
	FragmentTest fragment = new FragmentTest();
	fragment.setArguments( b );
	return fragment;
}

@Override public String toString(){
	return this.getClass().getSimpleName() + "#" + getArguments().getString( ARG_NAME );
}

int nextAnim = 0;
public void setNextAnimation( int nextAnim ){
	this.nextAnim =nextAnim;
}

// Called when a fragment loads an animation.
public Animation onCreateAnimation( int transit, boolean enter, int nextAnim ){
	if( DEBUG_LIFECYCLE ) log.d( "(%s onCreateAnimation transit=%s,enter=%s,nextAnim=%s", toString() ,transit,enter,nextAnim);
	Animation rv;
	if( nextAnim == 0 && this.nextAnim != 0){
		log.d("supply next Anim: %s",this.nextAnim);
		rv = AnimationUtils.loadAnimation( getActivity(), this.nextAnim );
		this.nextAnim = 0;
	}else{
		rv = super.onCreateAnimation( transit, enter, nextAnim );
	}
	return rv;
}

// Activity側
int index_seed = 0;
static final String STATE_INDEX_SEED="index_seed";

@Override protected void onCreate( Bundle state ){
	is_created = true;
	super.onCreate( state );
	setContentView( R.layout.act_main );
	if( state == null ){
		getSupportFragmentManager().beginTransaction()
			.add( R.id.container, FragmentTest.create(++index_seed) )
			.commit();
	}else{
		index_seed = state.getInt(STATE_INDEX_SEED);
	}
}

@Override protected void onSaveInstanceState( Bundle state ){
	super.onSaveInstanceState( state );
	state.putInt(STATE_INDEX_SEED,index_seed);
}

@Override public void onBackPressed(){
	// XXX: may you want to dispatch this event to fragment ?

	if( getSupportFragmentManager().getBackStackEntryCount() > 0){
		page_pop();
	}else{
		super.onBackPressed();
	}
}

void page_push(){
	FragmentManager fm = getSupportFragmentManager();

	FragmentTest f_new = FragmentTest.create(++index_seed);
	Fragment f_old = fm.findFragmentById( R.id.container );
	String stack_name = (f_old==null?"null": f_old.toString() +"=>" + f_new.toString() );

	getSupportFragmentManager().beginTransaction()
		.setCustomAnimations( // enter,exit,popEnter,popExit 後ろ2つは popBackStack の際に使われる
			R.anim.right_to_center,
			R.anim.center_to_left,
			R.anim.left_to_center,
			R.anim.center_to_right
		)
		.replace( R.id.container, f_new )
		.addToBackStack( stack_name )
		.commit();
}

void page_replace(boolean back){
	FragmentManager fm = getSupportFragmentManager();

	Fragment f_old = fm.findFragmentById( R.id.container );
	Fragment f_new = FragmentTest.create( ++ index_seed );
	log.d( "replace %s => %s", f_old, f_new );

	FragmentTransaction ft = fm.beginTransaction();
	if( back ){
		ft.setCustomAnimations( // enter,exit,popEnter,popExit 後ろ2つは popBackStack の際に使われる
			R.anim.left_to_center,
			R.anim.center_to_right,
			R.anim.right_to_center,
			R.anim.center_to_left
		);
	}else{
		ft.setCustomAnimations( // enter,exit,popEnter,popExit 後ろ2つは popBackStack の際に使われる
			R.anim.right_to_center,
			R.anim.center_to_left,
			R.anim.left_to_center,
			R.anim.center_to_right
		);
	}
	ft.replace( R.id.container, f_new )
		.commit();
	// 結果として、最後のbackstackで追加されたfragmentと現在のフラグメントが食い違うことになる。
	// これをこの時点で解決することはできないので、pop_pageで処理する
}

public boolean page_pop(){
	FragmentManager fm = getSupportFragmentManager();
	//
	FragmentManager.BackStackEntry entry = fm.getBackStackEntryCount() == 0 ? null : fm.getBackStackEntryAt( fm.getBackStackEntryCount() - 1 );
	if( entry == null ){
		log.d( "page_pop: back stack is empty." );
		return false;
	}

	// BackStackに記録されているFragment
	Fragment f_backstack_old =null;
	Fragment f_backstack_current =null;
	Matcher m = Pattern.compile( "(.+?)=>(.+?)" ).matcher( entry.getName() );
	if( m.find() ){
		f_backstack_old = getFragmentByName( m.group( 1 ) );
		f_backstack_current = getFragmentByName( m.group( 2 ) );
	}

	//Activityの破棄と復元によりアニメーションが失われた場合に補う
	if( f_backstack_old != null ) ((FragmentTest)f_backstack_old).setNextAnimation( R.anim.left_to_center );
	if( f_backstack_current != null ) ((FragmentTest)f_backstack_current).setNextAnimation( R.anim.center_to_right );

	// 現在container に格納されているFragment
	Fragment f_current = fm.findFragmentById( R.id.container );
	// 現在container に格納されているFragmentがバックスタックに記録されている追加したFragmentと異なる場合は、アニメーションつきで除去する
	if( f_current != null && f_current != f_backstack_current ){
		log.d( "page_pop: current fragment mismatch between back stack and holded in container. remove fragment in container." );
		fm.beginTransaction()
			.setCustomAnimations( // enter,exit,popEnter,popExit 後ろ2つは popBackStack の際に使われる
				R.anim.left_to_center,
				R.anim.center_to_right,
				R.anim.right_to_center,
				R.anim.center_to_left
			)
			.remove( f_current )
			.commit();
	}

	// バックスタックをpopする
	fm.popBackStack();

	return true;
}


private FragmentTest getFragmentByName( String name ){
	for( Fragment f : getSupportFragmentManager().getFragments()){
		if(f ==null ) continue;
		if( f instanceof FragmentTest ){
			if( f.toString().equals( name ) ) return (FragmentTest) f;
		}
	}
	return null;
}

その他

今回の話題とは直接関係ありませんが、上のログを記録した時は開発者オプションで「アクティビティを保持しない」を指定していました。ホームボタンを押すとアクティビティが破棄されて、履歴から戻った際に作成と復元が行われます。再度ホームボタンを押すとまたアクティビティが破棄されるのですが、この間、back stack にだけ存在する fragment に対して呼び出されるライフサイクルイベントは onAttach, onCreate, onSaveInstanceState, onDestroy, onDetach だけです。つまりonCreateで全ての状態をsavedInstanceStateから取得してonSaveInstanceStateでそれを保存しないと、状態が失われます。なんと恐ろしい。