Android2014. 1. 8. 10:11

Smallest-width Qualifier(최소너비 식별자) 사용하기


개발자가 안드로이드 3.2 이전 OS에서 가장 힘들었던 것 중의 하나가 "large" 화면사이즈 폴더였다. 여기에는 Dell Streak(5인치), 최초 갤럭시탭, 7인치 태블릿 등이 해당된다.

많은 앱들은 디바이스에 따라 서로 다른 레이아웃을 구성하기를 원했지만, 5인치든 7인치든 모두 "large" 스크린으로 처리됐다. 이런 문제를 해결하기 위해 안드로이드 3.2부터 "Smallest-width" qualifier(최소너비 식별자)를 추가했다.


최소너비 식별자를 이용하면 특정 가로사이즈(dp)의 화면을 구분지을 수 있다. 예를들어 대부분의 7인치 태블릿은 600dp의 너비값을 갖고 있는데, 한 페이지에 두개의 판(pane)을 동시에 보여주고 싶다면(반면 화면이 작은 디바이스에서는 한개의 판만...), 아래와 같이 두 개의 레이아웃을 구성하면 된다.


  • res/layout/main.xml, single-pane (default) layout:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <fragment android:id="@+id/headlines"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.HeadlinesFragment"
                  android:layout_width="match_parent" />
    </LinearLayout>
  • res/layout-large/main.xml, two-pane layout:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="horizontal">
        <fragment android:id="@+id/headlines"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.HeadlinesFragment"
                  android:layout_width="400dp"
                  android:layout_marginRight="10dp"/>
        <fragment android:id="@+id/article"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.ArticleFragment"
                  android:layout_width="fill_parent" />
    </LinearLayout>


그러나, 두 판을 보여주는 레이아웃의 최소너비가 600dp임을  지정하고 싶다면 "large"식별자를 사용하는 대신, "sw600dp" 식별자를 사용하면된다.


  • res/layout/main.xml, single-pane (default) layout:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <fragment android:id="@+id/headlines"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.HeadlinesFragment"
                  android:layout_width="match_parent" />
    </LinearLayout>
  • res/layout-sw600dp/main.xml, two-pane layout:
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="horizontal">
        <fragment android:id="@+id/headlines"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.HeadlinesFragment"
                  android:layout_width="400dp"
                  android:layout_marginRight="10dp"/>
        <fragment android:id="@+id/article"
                  android:layout_height="fill_parent"
                  android:name="com.example.android.newsreader.ArticleFragment"
                  android:layout_width="fill_parent" />
    </LinearLayout>


이것은 최소너비가 600dp이거나 그보다 큰 디바이스에서는 layout-sw600dp/main.xml (two-pane) 레이아웃을 사용하고, 600dp보다 작은 디바이스에서는 layout/main.xml (single-pane) 레이아웃을 사용한다는 것을 의미한다.


여기서 주의할 점이 있다. 안드로이드 3.2 미만의 디바이스에서는 "sw600dp"라는 식별자를 인식할 수 없기 때문에 원하는 대로 동작하지 않는다. 이런 이유로 이전에 사용하던 "large"식별자를 계속 유지시켜줘야 한다. 즉, res/layout-sw600dp/main.xml 과 똑같이 res/layout-large/main.xml 을 구성해주면 이 문제를 해결할 수 있다.


다음 섹션에서는 이 방법을 사용했을 때, 레이아웃파일이 중복되지 않게하는 방법에 대해 배울 수 있다.




[원문보기]

(발번역, 오역에 대한 내용이 있다면 댓글로 항의해주세요 ^^)
























Posted by 데브로망스
Android2012. 1. 12. 14:08

아래 코드는 안드로이드 프레임워크에서 Activity의 상태에 따라 호출하는 대부분의 함수를 나열했다.
각각 로그를 찍어서 각 함수의 실행 순서를 테스트하여 정리한다.


public class TestAppActivity extends Activity implements OnClickListener {
		
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		Log.d("TestAppActivity", "onCreate");
		setContentView(R.layout.main);

		Button btnAuth = (Button) findViewById(R.id.btn_finish);
		btnAuth.setOnClickListener(this);
		Button btnPopup = (Button) findViewById(R.id.btn_popup);
		btnPopup.setOnClickListener(this);
		Button btnNewAct = (Button) findViewById(R.id.btn_new_activity);
		btnNewAct.setOnClickListener(this);
	}
	
	
	@Override
	protected void onPostCreate(Bundle savedInstanceState) {
		super.onPostCreate(savedInstanceState);
		Log.d("TestAppActivity", "onPostCreate");
	}

	@Override
	protected void onStart() {
		super.onStart();
		Log.d("TestAppActivity", "onStart");
	}

	@Override
	protected void onRestart() {
		super.onRestart();
		Log.d("TestAppActivity", "onRestart");
	}
	
	@Override
	protected void onStop() {
		super.onStop();
		Log.d("TestAppActivity", "onStop");
	}
	
	@Override
	protected void onPostResume() {
		super.onPostResume();	
		Log.d("TestAppActivity", "onPostResume");
	}
	
	@Override
	protected void onPause() {
		super.onPause();
		Log.d("TestAppActivity", "onPause");
	}
	
	@Override
	protected void onResume() {
		super.onResume();
		Log.d("TestAppActivity", "onResume");
	}
	
	@Override
	protected void onDestroy() {
		super.onDestroy();
		Log.d("onPostCreate", "onDestroy");
	}

	@Override
	public void onClick(View v) {
		int id = v.getId();
		switch (id) {
		case R.id.btn_finish:
			finish();
			break;
		case R.id.btn_popup:
			AlertDialog.Builder ab=new AlertDialog.Builder(TestAppActivity.this);
		 	ab.setMessage(Html.fromHtml("<strong><font color=\"#000055\">Title</font></strong><br> Message!!"));
		 	ab.setPositiveButton("OK", null);
		 	ab.show();
			break;
		case R.id.btn_new_activity:
			Intent intent = new Intent(getBaseContext(), NewActivity.class);	
			startActivity(intent);
		default:
			break;
		}
	}
	
	@Override
	protected void onRestoreInstanceState(Bundle savedInstanceState) {
		Log.d("TestAppActivity", "onRestoreInstanceState");
		super.onRestoreInstanceState(savedInstanceState);
	}
	@Override
	protected void onSaveInstanceState(Bundle outState) {
		Log.d("TestAppActivity", "onSaveInstanceState");
		super.onSaveInstanceState(outState);
	}

	@Override
	public void onLowMemory() {
		Log.d("TestAppActivity", "onLowMemory");
		super.onLowMemory();
	}
}




onCreate()
무조건 Activity가 처음 실행될 때 호출된다. [Case1, Case3]
시스템에 의해 호출되면 인자로 받은 Bundle은 null이다. [Case1]
Activity가 실행된 적이 있는데, 어떤 이유로 종료된 후 재시작되면, 종료될 때 호출된 onSaveInstanceState()에서 저장한 내용과 동일한 Bundle을 넘겨준다.
디바이스가 회전되어 가로/세로 전환 등 리소스를 새롭게 갱신되어야 할 때 호출된다. [Case5]

onDestroy()
Actiivity가 종료되기 전 호출된다.
Activity내부에서 finish()를 실행하면 호출된다. [Case7]
시스템 메모리가 부족하면, 안드로이드가 강제로 TestApp을 죽일 때도 호출이 되는데, 메모리 확보가 매우 시급할 때는 호출조차 되지 않는 경우도 있다.
onCreate()와 짝을 이뤄 사용했던 리소스는 이 곳에서 싹~ 치워준다.

onStart()
Activity가 초기 실행 후, 화면의 전면으로 나타날 때 onCreate(), onRestart() 이후에 호출된다. [Case1, Case4]
전화수신, SMS수신 등으로 Background로 갔다가 다시 전면으로 나올때도 호출된다. [Case2, Case3, Case4]

onRestart()
Activity가 정지되었다가 다시 실행될 때 호출되는데, onDestroy()가 호출된 이후가 아닌, onStop()으로 정지된 상태에만 해당된다. 즉, onStop()과 onRestart()는 한 쌍으로 생각하면 된다. [Case2, Case4]

onStop()
하드웨어 HOME버튼을 눌렀을 때와 SMS수신, 전화수신, 다른 App실행할 때 호출된다. [Case2, Case4]

onResume()
Activity가 전면에 나타날 때 대부분의 상황에서 호출된다. 처음 실행했을 때, onCreate() 이후에도 호출된다.
(책에서는 팝업 대화상자가 떳다가 닫히는 경우에도 호출된다고 하지만, AlertDialog로 테스트 했을 때에는 호출되지 않았다.)

onPause()
거의 모든 경우에 onStop(), onDestroy()가 호출되기 이전에 호출된다.
Activity가 사용자의 시선에서 가려지는 경우에 호출된다고 생각하면 된다.
대부분의 상황에서 onStop() 발생하기 이전에 불린다.
*일반적으로 onResume()과 쌍으로 보고, onResume()에서 했던 작업을 onPause()에서 정리, 멈추는 것이 좋다.
예를 들면, onResume()에서 쓰레드를 실행시켰으면, onPause()가 호출될 때, 아직 쓰레드가 실행중이면 정리를 해주면 된다.
*onPause()가 호출되서 App(또는 Activity)이 일시정지된 상태라면 안드로이드 시스템에서 필요에 따라 완전이 죽일 수 있기 때문에 그 이후의 작업을 못할 수도 있다는 점을 유의해야한다.

onSaveInstanceState()
Activity가 전면에서 Background로 숨는 경우 호출된다. [Case2, Case4, Case5, Case8]
현재의 Activity 상태를 저장하려면 이 함수를 구현한다.
호출될 때, Bundle 인스턴스를 넘겨주는데 이를 이용해서 저장하면 되는데, 예를 들어 에디트박스에 입력된 문자열 등을 저장해 두면 된다.

onRestoreInstanceState()
onSaveInstanceState() 함수에서 저장했던 내용은 onCreate()에서 Bundle 인스턴스로 넘겨받는데, onRestoreInstanceState()에서도 같은 내용을 받을 수 있다.
주의할 점은 onRestoreInstanceState()는 정상적인 상황에서는 호출되지 않는다.
테스트 결과 일반적인 상황이 아닌, 디바이스의 화면회전이 발생할 때[Case5] , 강제종료 후 제시작 할 때 [Case6] 발생했다.

onPostCreate() 와 onPostResume()
이 두 함수는 시스템 상에서 마지막 초기화 작업을 목적으로 만들어진 것으로 일반적으로 어플리케이션 작성시에는 구현할 필요가 없다고 한다.
 
onLowMemory()
시스템 메모리가 부족할 때 호출된다고 하나, 테스트로 발생시키기 어려워 생략했다.
안드로이드 Dev 사이트를 참고하여 설명하면, 이 함수가 정확히 호출되는 시점은 명확하지 않고, 다만, Background에서 실행하는 프로세스가 죽임을 했을 때, 호출된다고 한다. 시스템은 현재 Foreground에 있는 App에게 메모리를 좀 확보해 주십사~하고 호출해 주는 것이다. 즉, 이 함수에서는 필요없는 리소스를 최대한 확보하는 코드를 넣어주면 시스템에서 매우 고마워한다는 것이다.
이 함수가 return되는 순간 시스템은 GC를 수행한다고 한다. 
 
[Case 1]
App 초기실행



[Case 2] 
HOME 버튼(하드웨어키)  눌러서 안드로이드 홈으로 이동시


TestApp 아이콘을 선택하여 재시작



[Case 3]
이전버튼(하드웨어키)을 눌러서 안드로이드 홈으로 이동시

 
 다시, TestApp 아이콘을 선택하여 재시작



[Case 4]
TestApp 실행상태에서 SMS수신, 전화수신 등이 되었을 때,
또는, HOME버튼(하드웨어키)를 길게 눌러 최근 사용한 다른 App을 띄워 
TestApp이 Background이동시 


SMS수신, 전화수신, 다른 앱에서 이전버튼(하드웨어키)를 눌러 뒤에 숨어있던 TestApp이 Foreground로 올라올 때



[Case 5] 
디바이스를 회전시켜, 세로에서 가로화면으로 전환 시킬때, 또는 그 반대의 경우



[Case 6]
프로세스 킬러App(마켓에 널려있는..)으로 TestApp을 강제로 죽인 후, 재 실행 시켰을 때


 
[Case 7]
TestApp에 있는 버튼을 눌러 finish()를 강제로 호출할 때



[Case 8]
새로운 Activity를 띄웠을 때


새로 띄웠던 Activity를 닫았을 때



[참고자료]
    안드로이드 프로그래밍2 / 마크 머피 / 에이콘출판사
    http://developer.android.com/reference/android/app/Activity.html 
    http://androidhuman.tistory.com/246  

[전체소스]
    다운로드 
Posted by 데브로망스
Android2011. 9. 22. 23:28

Activity를 이것 저것 띄우고 닫고 하다보면, 과연 내가 잘 하고 있나? 불안할 때가 있다.
더구나, 인텐트에 액티비티 관련 플래그를 사용하기 시작하면 그 불안감은 고조된다.

실제로 단 3개의 Activity가  destroy()되지 않은 채, 19개가 되는 현상을 겪었다. -0-a
물론, Activity의 생명주기를 로그로 찍어서 확인하는 것도 좋겠지만,
그래도 딱! Activity Stack의 모습을 볼 수 있으면 좋겠다는 생각에 방법을 찾아봤다.


* 방법 : 커멘드 창에서 "adb shell dumpsys activity" 실행

매우 긴 영문들이 뒤섞여 보인다.
처음에는 매우 난감했지만, Activity를 이래저래 띄우면서 테스트 한 결과에 의하면,
"Activity Stack"과 "Running Activities" 부분을 보면 된다.

"Activity Stack"에는 스택 순서대로 Hist #2, Hist #1, Hist #0 순으로 표기되어 있다.
즉, 단말기의 [이전]버튼을 누르면 ActivityA가 닫히고, 한 번 더 누르면 MainActivity가 닫힌다.

작업하는 중간중간에 다시 실행시켜보면, 정확히 화면에 보이는 Activity가 "Activity Stack" 로그의 최상위에 보여지게 된다.

Hist #0는 보통 안드로이드 런처(홈) Activity이다.



C:\Users\Juno>adb shell dumpsys activity


   ... 중략 ...

  

  Activity stack:

  * TaskRecord{461e6d10 #42 A com.juno.activitytest}

    clearOnBackground=false numActivities=2 rootWasReset=false

    affinity=com.juno.activitytest

    intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.juno.activitytest/.MainActivity}

    realActivity=com.juno.activitytest/.MainActivity

    lastActiveTime=6762895 (inactive for 6s)

    * Hist #2: HistoryRecord{46096400 com.juno.activitytest/.ActivityA}

        packageName=com.juno.activitytest processName=com.juno.activitytest

        launchedFromUid=10026 app=ProcessRecord{461537b0 1050:com.juno.adpd.activitytest/10026}

        Intent { flg=0x20000000 cmp=com.juno.activitytest/.ActivityA (has extras) }

        frontOfTask=false task=TaskRecord{461e6d10 #42 A com.juno.livepoint}

        taskAffinity=com.juno.activitytest

        realActivity=com.juno.activitytest/.ActivityA

        base=/data/app/com.juno.activitytest-1.apk/data/app/com.juno.activitytest-1.apk data=/data/data/com.juno.activitytest

        labelRes=0x7f080000 icon=0x7f02000e theme=0x1030006

        stateNotNeeded=false componentSpecified=true isHomeActivity=false

        configuration={ scale=1.0 imsi=0/0 loc=ko_KR touch=3 keys=1/1/2 nav=2/1orien=2 layout=34 uiMode=17 seq=2}

        launchFailed=false haveState=false icicle=null

        state=RESUMED stopped=false delayedResume=false finishing=false

        keysPaused=false inHistory=true persistent=false launchMode=0

        fullscreen=true visible=true frozenBeforeDestroy=false thumbnailNeeded=false idle=true

    * Hist #1: HistoryRecord{46130618 com.juno.activitytest/.MainActivity}

        packageName=com.juno.activitytest processName=com.juno.activitytest

        launchedFromUid=2000 app=ProcessRecord{461537b0 1050:com.juno.activitytest/10026}

        Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.juno.activitytest/.MainActivity }

        frontOfTask=true task=TaskRecord{461e6d10 #42 A com.juno.activitytest}

        taskAffinity=com.juno.activitytest

        realActivity=com.juno.activitytest/.MainActivity

        base=/data/app/com.juno.activitytest-1.apk/data/app/com.juno.activitytest-1.apk data=/data/data/com.juno.activitytest

        labelRes=0x7f080000 icon=0x7f02000e theme=0x1030006

        stateNotNeeded=false componentSpecified=true isHomeActivity=false

        configuration={ scale=1.0 imsi=0/0 loc=ko_KR touch=3 keys=1/1/2 nav=2/1orien=2 layout=34 uiMode=17 seq=2}

        launchFailed=false haveState=true icicle=Bundle[mParcelledData.dataSize=812]

        state=STOPPED stopped=true delayedResume=false finishing=false

        keysPaused=false inHistory=true persistent=false launchMode=0

        fullscreen=true visible=false frozenBeforeDestroy=false thumbnailNeeded=false idle=true

  * TaskRecord{461626c0 #2 A com.android.launcher}

    clearOnBackground=true numActivities=1 rootWasReset=true

    affinity=com.android.launcher

    intent={act=android.intent.action.MAIN cat=[android.intent.category.HOME] flg=0x10600000 cmp=com.android.launcher/com.android.launcher2.Launcher}

    realActivity=com.android.launcher/com.android.launcher2.Launcher

    lastActiveTime=6749602 (inactive for 19s)

    * Hist #0: HistoryRecord{4615d870 com.android.launcher/com.android.launcher2.Launcher}

        packageName=com.android.launcher processName=com.android.launcher

        launchedFromUid=0 app=ProcessRecord{46162bb0 2292:com.android.launcher/10012}

        Intent { act=android.intent.action.MAIN cat=[android.intent.category.HOME] flg=0x10000000 cmp=com.android.launcher/com.android.launcher2.Launcher }

        frontOfTask=true task=TaskRecord{461626c0 #2 A com.android.launcher}

        taskAffinity=com.android.launcher

        realActivity=com.android.launcher/com.android.launcher2.Launcher

        base=/system/app/Launcher2.apk/system/app/Launcher2.apk data=/data/data/com.android.launcher

        labelRes=0x7f0c0002 icon=0x7f02005a theme=0x7f0d0000

        stateNotNeeded=true componentSpecified=false isHomeActivity=true

        configuration={ scale=1.0 imsi=0/0 loc=ko_KR touch=3 keys=1/1/2 nav=2/1orien=2 layout=34 uiMode=17 seq=2}

        launchFailed=false haveState=true icicle=Bundle[mParcelledData.dataSize=4480]

        state=STOPPED stopped=true delayedResume=false finishing=false

        keysPaused=false inHistory=true persistent=false launchMode=2

        fullscreen=true visible=false frozenBeforeDestroy=false thumbnailNeeded=false idle=true



  Running activities (most recent first):

    TaskRecord{461e6d10 #42 A com.juno.activitytest}

      Run #2: HistoryRecord{46096400 com.juno.activitytest/.ActivityA}

      Run #1: HistoryRecord{46130618 com.juno.activitytest/.MainActivity}

    TaskRecord{461626c0 #2 A com.android.launcher}

      Run #0: HistoryRecord{4615d870 com.android.launcher/com.android.launcher2.Launcher}


  mPausingActivity: null

  mResumedActivity: HistoryRecord{46096400 com.juno.activitytest/.ActivityA}

  mFocusedActivity: HistoryRecord{46096400 com.juno.activitytest/.ActivityA}

  mLastPausedActivity: HistoryRecord{4614cb90 com.juno.activitytest/.DetailActivity}


  mCurTask: 42


   ... 중략 ...


C:\Users\Juno>



그림으로 그리면, 요런 모습이겠다.



[여담]
이런 로그 메시지를 분석하는 부분은 꽤 자신없는 부분이네요.
혹시나, 잘못된 내용은 가감없이 지적바랍니다.


Update #1

누군가 고맙게도 stack만 추려서 볼 수 있게, 만들어줬네요.

sed 명령어를 사용하려면 GnuWin32를 설치해야합니다.

http://sourceforge.net/projects/getgnuwin32/files/getgnuwin32/


  adb shell dumpsys activity activities | sed -En -e "/Stack #/p " -e "/Running activities/,/Run #0/p"

출처 : http://stackoverflow.com/a/31107447/536078



Posted by 데브로망스