오늘의 강좌는 미리 공지한대로 TabActivity 내에서 ActivityGroup을 사용할 경우의 Navigation 처리에 대한 내용입니다.
먼저 TabActivity가 동작하는 간단한 원리를 정리해 보면 다음과 같습니다.
1) TabActivity에 생성되어 있는 TabHost를 얻는다.
getTabHost();
2) TabHost에 통해 TabSpec을 생성하면서 Indicator와 Intent를 지정한다.
Intent intent = new Intent().setClass(this,
com.mk.counsel.group.ViewCounselGroup.class);
spec = tabHost.newTabSpec("tab01").setIndicator(
new TabView(this, R.drawable.tab1_selector, "tab01")
).setContent(intent);
3) TabHost에 TabSpec을 등록한다.
tabHost.addTab(spec);
위의 과정에서 TabSpec에 적용되는 Intent가 하나의 Activity 만을 처리하는 형태와 여러 Activity가 하나의 Tab 내에서 관리되는 경우(그림 참조)로 구성될 수 있는데 후자의 경우에 문제가 발생할 수 있습니다.
단순히 Activity를 변경하기 위해서 startActivity() 함수를 사용할 경우 TabActivity가 새로운 Activity로 변경되어 버려서 기존의 Tab 화면을 유지할 수 없다는 문제가 생기는데 이런 문제를 해결해 주기 위해서 등장한 것이 ActivityGroup 입니다.
2. ActivityGroup
ActivityGroup은 철저하게 Activity수행에 대한 관리와 화면처리(View)를 분리하여 관리하기 위한 용도로 생각하시면 됩니다. Activity 수행에 대한 관리는 전적으로 LocalActivityManager에 위임하고 각종 Event 처리에 대해서는 Activity 자체에 맡기고 생성된 최종 화면(View)만 본인이 소유하고 있도록 구성되어 있습니다.(그림참조)
문제는 전형적인 ActivityGroup은 모든 Activity의 관리 권한을 LocalActivityManager에 위임한 상태이고 View만을 제공 받으므로 자체적으로 Navigation 처리를 할 수 없다는 것입니다.바로 여기서 Navigation을 관리할 대상이 정해집니다. 바로 ActivityGroup이 유일하게 소유할 수 있는 View가 바로 그것입니다. 이 View들을 ActivityGroup이 관리함으로 Navigation이 가능해 진다는 것입니다.
3. 실제 적용
다음의 코드를 보시기 바랍니다.
public class NavigationGroupActivity extends ActivityGroup { ArrayList<View> history; // View들을 관리하기 위한 List NavigationGroupActivity group; // Activity들이 접근하기 위한 Group @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); history = new ArrayList<View>(); group = this; } public void changeView(View v) { // 동일한 Level의 Activity를 다른 Activity로 변경하는 경우 history.remove(history.size()-1); history.add(v); setContentView(v); } public void replaceView(View v) { // 새로운 Level의 Activity를 추가하는 경우 Log.d("MK","REPLACE VIEW..."); history.add(v); setContentView(v); } public void back() { // Back Key가 눌려졌을 경우에 대한 처리 if(history.size() > 1) { history.remove(history.size()-1); setContentView(history.get(history.size()-1)); } else { finish(); // 최상위 Level의 경우 TabActvity를 종료해야 한다. } } @Override public void onBackPressed() { // Back Key에 대한 Event Handler group.back(); return; } }
그러면 Back Key 처리는 알겠는데 새로운 Activity를 추가할 경우는 어디서 누가 호출을 해 주는지가 궁금할 것입니다. 다음의 코드를 보시기 바랍니다.
public class NavigationActivity extends Activity { public void goNextHistory(String id,Intent intent) { //앞으로 가기 처리 NavigationGroupActivity parent = ((NavigationGroupActivity)getParent()); View view = parent.group.getLocalActivityManager() .startActivity(id,intent) .getDecorView(); parent.group.replaceView(view); } @Override public void onBackPressed() { //뒤로가기 처리 NavigationGroupActivity parent = ((NavigationGroupActivity)getParent()); parent.back(); } }
위의 코드를 보시면 해당 Activity를 소유하고 있는 ActivityGroup을 getParent() 함수로 구한 다음에 실제 Activity의 실행을 LocalActivityManager에 맡기고 있습니다.
LocalActivityManager에서 실행된(startActivity() 함수 호출) Activity의 View를 getDecoderView() 함수를 통해 얻은 다음 이 View를 ActivityGroup의 view에 적용하는 모습을 볼 수 있습니다. 반대로 Back Key가 발생하면 Parent의 back 함수를 호출하여ActivityGroup내의 View를 조정하여 Navigation처리를 하도록 요청합니다.
자 그럼 이제 두 클래스를 상속받은 실제 ActivityGroup 과 Activity가 어떻게 적용되는지를 살펴 보겠습니다.
아래의 코드를 보시기 바랍니다.
public class ViewCounselGroup extends NavigationGroupActivity { @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); Intent intent = new Intent(this,ViewCounselMainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |Intent.FLAG_ACTIVITY_SINGLE_TOP); View view = getLocalActivityManager().startActivity("CounselMainActivity",intent) .getDecorView(); replaceView(view); } @Override public void onBackPressed() { // Back Key에 대한 처리 요청 super.onBackPressed(); } }
ViewCounselGroup이라는 ActivityGroup에서는 LocalActivityManager를 통해 초기에 실행할 Activity인 ViewCounselMainActivity라는 클래스를 Intent로 실행합니다. 이때 Intent에 적절한 Flag를 설정해 주지 않으면 LocalActivityManager가 관리하는 Stack에 Activity가 View의 저장 구조와 다른 형태로 쌓이게 되므로 Navigation이 동기화 되지 못합니다.따라서 반드시 위의 Intent Flag를 설정해 주셔야 Activity와 View의 순서가 동기화 됩니다. 또한 Back Key에 대한 처리는 모든 Activity에서 처리가 되는 것이 아니라 ActivityGroup에서 실행한 맨처음 Activity에서만 Event를 받을 수 있다는 점을 꼭~~~ 기억 하셔야 합니다.
참고로 Intent에 대한 Flag 설명은 다음 주소를 참고하시기 바랍니다.
http://chihun80.springnote.com/pages/6423199
public class ViewCounselMainActivity extends NavigationActivity implements OnItemClickListener{ ……중략…… @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.view_counsel_list); adapter = new CounselAdapter(this,items); ListView view = (ListView)findViewById(R.id.counsel_list); view.setAdapter(adapter); view.setOnItemClickListener(this); } @Override public void onItemClick(AdapterView<?> adapter, View view, int position, long id) { Intent intent = new Intent(ViewCounselMainActivity.this, com.mk.counsel.ViewCounselDetailActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.putExtra("id", items.get(position).id); goNextHistory("ViewCounselMainActivity",intent); } }
동작에 대한 Event 처리 등은 실행된 Activity 내에서 관리가 된다고 말씀 드렸듯이 GroupActivity에서 처음 실행된 ViewCounselMainActivity 내에서 특정 아이템이 선택된 경우에 ItemClick 핸들러를 처리하고 다음 Level의 Activity로 이동을 하려고 한다면 goNextHistory()를 통해 이동하고자 하는 Intent를 넘겨주면 GroupActivity 내부에서 LocalActivityManager를 통해 Intent를 수행하고 View를 GroupACtivity의 ViewList에 추가하고 화면을 전환하는 작업을 해 줍니다.
4. 마무리
TabSpec에 Intent를 설정할 때 GroupActivity를 먼저 등록하고 GroupActivity 내에서 처음 호출할 Activity를 LocalActivityManager를 통해 실행하고 View를 얻는 과정이 핵심입니다.
완벽하게 돌아가는 예제를 만들어 전체 소스를 올리고 싶지만 예제를 따로 만들만한 개인적인 시간이 조금 부족한 관계로 핵심 코드만을 보여 드렸습니다. 다음 번 강좌를 기대해 주세요. ^^
1. 그냥가자 님의 의견대로도 구현을 해 보았지만 몇가지 문제가 발생하여 위의 코드로 대체한 것입니다.
1) 현재 Activity를 LocalActivityManager를 통해서 종료 시키고 이전 Activity를 활성화 할 경우 화면상으로는
이전 Activity가 나타나지만 기존에 처리되던 상태나 화면을 그대로 다시 복원하는데 문제가 발생합니다.
Intent Flag를 통해서 기존 Activity가 재사용 되도록 처리를 하더라도 화면에 대한 복원을 해 주지는 않기 때문에
인스턴스 상태를 저장하거나 복원하기 위한 처리를 따로 해 주어야 하는데 그 방법이 복잡하기 때문에
View 관리를 통해서 이동하는 것이 훨씬 간단하다고 생각되었습니다.
그리고 DecodeView를 하게 되면 근본적으로 LocalActivityManager가 Activity를 재시작하고 View를 새로 얻는 것이므로
상태 유지 자체가 안됩니다.
2. 이 질문은 좀 이해가 안가는 부분이 많아서 정확한 설명을 부탁 드립니다.
어떤 곳에서도 형 변환을 해서 사용한 경우는 없는데요.. Intent에 ActivityClass를 그대로 사용하고 있는데요..ㅠㅠ
Broadcast를 사용해 보았지만 의존성이라는 측면은 공감을 하지만 아무래도 Message전송을 통한 Receiver를 통해
Activity의 View를 변경하는 방식보다 직접 View를 관리하는 것이 성능면에서 조금 나은 면을 보였습니다.
그리고 LocalActivityManager에서도 Stack에 Activity를 들고 다니면서 관리하고 있기 때문에 Activity View를 추가로 들고
다니는 것이 같은 원리에서 보면 큰 문제는 아니지 않을까 생각됩니다.
마지막으로 ActivityGroup을 상속받는대신 NavigationGroupActivity를 상속받고 Activity 대신 NavigationActivity 클래스를 상속 받는거
뿐이기 때문에 재사용성 측면에서도 문제가 될 부분이 없어 보이는데요...ㅠㅠ
그냥가자님
제가 아까 onBackPressed 메서드 안에서 LocalActivityManager 의 dispatchDestroy, dispatchStop 메서드 다 실행해봤는데요,
먹통이던데 잘되시나요?
잘되신다면..샘플코드좀 보여주셨으면 해요..
종일 삽질중이네요..
일단 테스트 해본 결과 제가 제시한 방법은 안됩니다.
소스를 봐도 LocalActivityManager는 엑티비티의 상태만 관리할뿐 스텍의 관리는 하지 않는군요.
이글을 남기고 앞글은 삭제하겠습니다. 낚일분들이 있을까봐...
근데 제가 뷰를 가지고 스택관리하는 것에 일단 태클 건 이유는
메모리 문제가 발생하지 않을까 하는 점에서였습니다.
뷰의 관리는 상당히 정확히 해줘야 할 필요가 있는것으로 알고 있고...
화면 전환등의 이슈에서 뷰를 직접 보관하지 않고 스테이트만 보관하는 것은
메모리와 관련이 있는것으로 판단하고 있거든요..(제 추측입니다.)
메모리 릭땜시 생고생을 해본입장에서 저 코드를 돌려보고 싶은데 릭을 테스트할 환경이 안되네요..
서브 엑티비티에 각각 다른 비트맵 하나씩 띄워서 (되도록이면 화면을 많이 차지하는걸로...)
스텍에 죽 쌓아도 문제 없는지 궁금합니다.
본문에 올려주신 코드가 전부인가요?
적용해도 전혀 안되는데요;;
에러는 안나는데 정작 실행되어야할 다음 인텐트가 담긴 파일을 찾질 못하네요.. (ViewCounselGroup.java)
엑티비티 생명주기가 도는게 아니고 뷰만 왔다갔다 한거니까요....
키패드 버그의 경우 강제로 일단 숨기는 방법밖에 없을겁니다.
back키나 onPause 혹은 Stop 때 키를 숨기는 방향으로 하시는것이 나을듯합니다.
대부분의 분들이 tab을 쓸때 Activity를 이용해서 많이들 하시고...
덕분에 뷰의 갱신들을 Activity 생명주기에 의존하고 계신듯합니다.
위 코드의 경우 일부 경우 Activity 생명주기를 타지 않고 뷰만 교체하는 경우가 생기므로...
잘 보시고 적당히 고쳐서 활용하셔야 할겁니다.
좋은 정보 감사 합니다 그런대 궁굼증이 있습니다. 제가 틀리게 한거 같기도 한데요..
텝 화면에서 텝메뉴가 안없어지구 이동 되는것 까지는 했습니다. 헌데 뒤로가기를 누르니 걍 그화면에서 종료가 되어버립니다,
제 생각엔
public void back() { // Back Key가 눌려졌을 경우에 대한 처리
Log.d("LogKMG",Integer.toString(history.size()-1));
if(history.size() > 1) {
history.remove(history.size()-1);
setContentView(history.get(history.size()-1));
}
else {
finish(); // 최상위 Level의 경우 TabActvity를 종료해야 한다.
}
}
여기서 에러라 생각 해서 로그를 찍어봤더니 history.size에서 -1 을 해서 0이 니옵니다.
어떻게 하는게 좋을가요;
이렇게 해 주시면 다음과 같은 생명 주기를 타게 됩니다.
// A Activity 실행
11-11 09:34:20.449: DEBUG/USilver(21719): (A):Lifecycle is OnStart
11-11 09:34:20.454: DEBUG/USilver(21719): (A):Lifecycle is OnRestart
// B Activity 실행
11-11 09:34:29.174: DEBUG/USilver(21719): (A):Lifecycle is OnPause
11-11 09:34:29.184: DEBUG/USilver(21719): (B):Lifecycle is OnStart
11-11 09:34:29.184: DEBUG/USilver(21719): (B):Lifecycle is OnResume
// B -> A Activity로 Back 처리
11-11 09:34:49.419: DEBUG/USilver(21719): (B):Lifecycle is OnPause
11-11 09:34:49.424: DEBUG/USilver(21719): (A):Lifecycle is OnNewIntent
11-11 09:34:49.424: DEBUG/USilver(21719): (A):Lifecycle is OnRestart
// A->B Activity 실행
11-11 09:40:57.024: DEBUG/USilver(21719): A:Lifecycle is OnPause
11-11 09:40:57.024: DEBUG/USilver(21719): B:Lifecycle is OnNewIntent
11-11 09:40:57.024: DEBUG/USilver(21719): B:Lifecycle is OnResume
또 오해 하실거 같아서 말씀 드리지만 해당 생명 주기에 따른 처리에 필요한 정보는
History 객체에 들어있는 Intent의 Extra 정보에 저장해 놓으셨다가 이용하시거나
해당 LifeCycle에서 별도로 처리해 주시거나 해 주셔야 합니다.
다음 강좌는 오해가 없도록 예제를 완전히 만들어서 올려야겠네요^^
강좌 보고 이해는 했지만, 실전 예제가 있으면, 조금 더 개발 시간을 단축 할 수 있을 것 같아서 올립니다.
http://blog.henriklarsentoft.com/2010/07/android-tabactivity-nested-activities/
http://richipal.com/post/2624844577
상세한 설명 감사합니다. 하나의 패턴이라고 볼수도 잇겟네요. VIew를 List로 관리하는것이 속도면에서 장점이 될수잇을테구요 새로 생성하는것보다 우선 해보진 않앗지만 이렇게 하면되겟다는 구상이 나오네요. 강의 잘보고 갑니다. 감사합니다.
봉이선달님..
올리신 강의를 보고 프로젝트에 반영 중입니다
그런데 이해를 못 하는 부부이 있어서 질문 좀 합니다
"일반액티비티에서호출하는게 아니라 TabACtivity를 상속받은 클래스에서 탭에 추가되는 인텐트에 이걸 등록해주셔야 합니다."
이렇게 말씀하셨는데 그럼
Intent intent = new Intent().setClass(TabFrameActivity.this, AAA.class);
AAA Activity에서 등록을 말씀을 하시는 게 맞는지?
그 부분에 추가를 했는데 어플 최초 로딩화면인 intro로 화면이 전개가 되어서 문제가 많네요
혹시 제가 잘 못 이해를 하는지 ..
부탁 드립니다
[back() 눌렀을 때 이전 액티비티를 갱신하는 방법]
GroupActivity의 back()함수에서 history-1을 바로 뷰로 뿌릴 것이 아니라 다시 한 번 뷰를 받아서 뿌리면 됩니다.
http://pastebin.com/tsC1E0NS을 보시면 대부분 18번째 줄의 back()함수로 구현하셨을 겁니다.
52번째 줄 back()함수를 참고하시면 되겠습니다.
사족을 붙이자면 코드의 mIdList에는 replaceView()함수를 사용할 때마다 id값을 집어 넣어주시면 되겠습니다.
의문점과 아쉬운점을 적어봅니다.
1. 왜 뷰를 따로 리스트 관리를 하나요?
LocalActivityManager에 보면 현재 Activity를 종료하는... (finish와 유사동작) 방법이 있는것으로 알고 있습니다.
현재 Activity를 종료하고 현재 Activity를 얻어서 윈도우의 DecorView를 얻어서 현재 ContentView를 갱신하는 방식으로 하면
뒤로가기가 따로 뷰를 관리 안해도 되지 않나요? (실테스트는 완벽하게 하지 않아서... 혹시 이게 안되서 그리하신건가요?)
그리고 Flag를 쓸때 문제점이 해결될수 있습니다. 현재는 자유롭게 쓸수 없죠...
2. 직접 ViewCounselMainActivity Class를 형변환 하여 사용하는 방식은 안좋다고 생각합니다.
결국 저런 구조의 소스들은 재사용 측면에서 통째로 들고 다녀야 하는 나쁜점이 있습니다.
클래스이름에 대한 리펙토링 문제도 있구요.. (물론 이클립스의 리펙토링이 좋긴해서 다 되긴합니다.)
Broadcast를 이용하는게 의존성이 낮아지기 때문에 구성이 더 깔끔해 보일거라 생각합니다.