액션바 taps+swipe에서 잠시 언급했던 fragment에 대해 이번엔 정리해 보도록 하겠습니다.

안드로이드 플랫폼 3.0 버전인 허니컴Honeycomb부터 큰 화면의 태블릿을 위해 도입된 대표적인 컴포넌트가 프래그먼트(Fragment)와 액션바(ActionBar)입니다.그 중 액션바는 이전 게시판에서 이야기했으니 오늘은 프래그먼트에 대해서입니다.

 프래그먼트 개요

만약 스마트 폰 앱을 개발할 때 세로모드(Portrait)에서는 단순하게 화면을 구성하지만 가로모드(Landscape) 화면 구성을 좀더 다양한 화면으로 구성 하고 싶다면 어떻게 할지 고민할 수 있습니다. 또는 해상도나 디바이스의 스크린크기, 디바이스 종류에 따라서 보여지는 것들을 다르게 구성하고 싶어할 수도 있습니다.

안드로이드에서는 이러한 요구사항을 만족시켜줄 수 있는 것을 fragment라는 개념으로 추가하게 되었습니다.

안드로이드 3.0이전 버전에서는 한 화면에 보이는 모든 것을 관장하는 것이 Activity라는 개념이였는데 이러한 Activity는 하나의 화면에 여러개 사용할 수 없게 설계가 되어있습니다.

그러나 하나의 액티비티로 상호작용하는 것은 큰 화면의 기기에서 비효율적이며 자원 낭비가 심하고 또한 일부 화면을 위한 자원을 재사용할 수 없고, 다양한 내용을 표시하려면 구성이 매우 복잡했습니다.

그래서 하나의 화면에 Activity와 비슷한 개념을 가지면서도 여러가지 화면을 넣을 수 있는 방법으로 fragment가 등장하게 되었습니다. 

 프래그먼트 특징

1) 프래그먼트는 액티비티 조각 혹은 서브액티비티(subactivity)

2) 하나의 액티비티에서 다수의 프래그먼트를 결합하여 다중 패널multi-pane UI를 생성할 수 있음

3) 여러 액티비티에서 하나의 프래그먼트를 재활용 가능

4) 프래그먼트 자신만의 생명주기 가짐

5) 입력 이벤트를 받아들이며, 액티비티가 실행하는 동안 동적으로 추가 혹은 삭제 가능

6) activity 안에서만 존재할 수 있고 단독으로 존재할 수 없다.

7) activity 안에서 다른 view와 함께 존재 가능

8) back stack을 사용가능

9) 반드시 default 생성자 존재

 프래그먼트 디자인 철학

애플리케이션을 태블릿과 핸드셋에 모두 지원하도록 설계하고자 한다면 화면의 사이즈에 따라 최적의 UX을 맛볼 수 있도록 프래그먼트를 다양한 레이아웃 구성에 재사용할 수 있습니다.

작은 화면사이즈 기기에는 하나의 액티비티에 하나의 프래그먼트를 사용하는 1-패널 UI를 제공하고, 태블릿과 같은 큰 화면사이즈 기기에서는 2개의 프래그먼트를 하나의 액티비티에 조합하여 2-패널UI를 제공하면 됩니다. 

 프래그먼트 생명주기

● 활성resumed 상태 : 실행 중인 액티비티에서 프래그먼트가 보이는 상태

● 중지paused 상태 : 다른 액티비티가 포그라운드 상태이며 포커스를 가지고 있지만 프래그먼트가 거주하는 액티비티가 여전히 보이는 상태

● 정지stopped 상태 : 프래그먼트가 보이지 않는다. 호스트 액티비티가 정지되거나 프래그먼트가 액티비티에서 제거되고 백스택에 추가되어 있는 상태 정지된 프래그먼트는 여전히 살아 있어 모든 상태와 멤버 정보가 시스템에 보관되어 있으나 사용자에게 더 이상 보이지 않고 액티비티가 종료되면 프래그먼트도 같이 종료됨

아래 소스에서 사용한 메소드는 onActivityCreated,onResume,onPause, onCreateView정도여서 http://blog.saltfactory.net/190 의 설명을 인용하도록 하겠습니다.

● onAttach()

onAttach() 콜백 메소드는 fargment가 activity에 추가되고 나면 호출된다. 이때 Activity가 파라미터로 전달되게 된다. Fragment는 Activity가 아니다. 다시말해서 Activity는 Context를 상속받아서 Context가 가지고 있는 많이 있지만 Fragment는 android.app 의 Object 클래스를 상속받아서 만들어진 클래스이다. 그래서 Fragment에서는 Context의 기능을 바로 사용할 수 없다. 뿐만 아니라 Fragment는  Activity 안에서만 존재하기 때문에 Activity를 onAttach() 콜백으로 받아올 수 있다.

1
2
3
public void onAttach(Activity activity) {
     super.onAttach(activity);
}


● onCreate()

프래그먼트가 생성될 때 호출된다. 프래그먼트가 중지 혹은 정지된 후 재개될 때 보유하기 원하는 프래그먼트의 필수 컴포넌트을 초기화한다.

Fragment의 onCreate() 메소드는 Activity의 onCreate() 메소드와 비슷하지만 Bundle을 받아오기 때문에 bundle에 대한 속성을 사용할 수 있다.

1
2
3
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
}


● onCreateView()

Fragment의 onCreateView()는 Fragment에 실제 사용할 뷰를 만드는 작업을 하는 메소드이다. LayoutInflater를 인자로 받아서 layout으로 설정한 XML을 연결하거나 bundle에 의한 작업을 하는 메소드이다. 

1
2
3
4
5
6
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState)
{
        view = inflater.inflate(R.layout.table, container, false);
        return view;
}


● onActivityCreated()

onActivityCreate() 메소드는 Activity에서 Fragment를 모두 생성하고 난 다음에 (Activity의 onCreate()가 마치고 난 다음)에 생성되는 메소드이다. 이 메소드가 호출될 때에서는 Activity의 모든 View가 만들어지고 난 다음이기 때문에 View를 변경하는 등의 작업을 할 수 있다.그러나, fragment에서 생성된 view의 size를 알려고 하니 이 메소드에서는 getWidth가 0으로 나와 따로 메소

1
2
3
4
public void onActivityCreated(Bundle savedInstanceState) {
    //Log.d(TAG,"onActivityCreated");
    super.onActivityCreated(savedInstanceState);
}


● onStart()

이 메소드가 호출되면 화면의 모든 UI가 만들어진 지고 호출이 된다.

1
2
3
public void onStart(){
    super.onStart();
}


● onResume()

이 메소드가 호출되고 난 다음에 사용자와 Fragment와 상호작용이 가능하다. 다시 말해서 이 곳에서 사용자가 버튼을 누르거나 하는 이벤트를 받을 수 있게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void onResume() {
    super.onResume();
 
    ⁄⁄mStatus.setText("현재 상태 : 서비스 시작");
    cellwidth = new int[5];
    desity = this.getResources().getDisplayMetrics().density;
    appendHeader();
    appendRowBlank();
    datainfo = selectData();
    appendRow(datainfo);   
    ViewTreeObserver vto = headerView[0].getViewTreeObserver();
    vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
      @SuppressWarnings("deprecation")
    @Override
      public void onGlobalLayout() {
        ⁄⁄Log.d(TAG, "Height = " + headerView[0].getWidth() + " Width = " + tv[0][0].getWidth());
        ViewTreeObserver obs = headerView[0].getViewTreeObserver();
        resizeRow();
        obs.removeGlobalOnLayoutListener(this);
      }
    });    
     
}  


● onPause()

이 메소드는 Fragment가 다시 돌아갈 때 (Back) 처음으로 불려지는 콜백 메소드이다. 

1
2
3
4
@Override
 public void onPause() {
     super.onPause();
 }


● onSaveInstanceState()

이 메소드에서는 Activity와 동일하게 Fragment가 사라질때 현재의 상태를 저장하고 나중에 Fragment가 돌아오면 다시 저장한 내용을 사용할 수 있게해주는 메소드이다.

1
2
3
4
5
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    super.onSaveInstanceState(savedInstanceState);
    savedInstanceState.putInt("text", "savedText");
}


● onStop()

Fragment의 onStop() 메소드는 Activity의 onStop()메소드와 비슷하다. 이 콜백 메소드가 호출되면 Fragment가 더이상 보이지 않는 상태이고 더이상 Activity에서 Fragment에게 오퍼레이션을 할 수 없게 된다.

1
2
3
4
@Override
public void onStop() {
    super.onStop();
}


 onDestroyView()

Fragment의 View가 모두 소멸될 때 호출되는 콜백 메소드이다. 이때 View에 관련된 모든 자원들이 사라지게 된다.

1
2
3
4
@Override
public void onDestroyView() {
    super.onDestroyView();
}


● onDestroy()

Fragment를 더이상 사용하지 않을 때 호출되는 콜백 메소드이다.  하지만 Activity와의 연결은 아직 끊어진 상태는 아니다.

1
2
3
4
@Override
public void onDestroy() {
    super.onDestroy();
}


● onDetach()

Fragment가 더이상 Activity와 관계가 없을 때 두 사이의 연결을 끊으며 Fragment에 관련된 모든 자원들이 사라지게 된다.

1
2
3
4
5
@Override
 public void onDetach() {
    super.onDetach();
    activity = null;
 }

 전체소스

 PublicWifi_0409.zip

참고사이트

http://developer.android.com/guide/components/fragments.html

http://blog.saltfactory.net/190



출처 : http://tigerwoods.tistory.com/31


예제 프로젝트 다운로드



 

  

1. 환경설정 개요 (Preferences)

 

안드로이드 플랫폼은 Data를 저장하는 방법으로 환경설정(이하 Preferences), 파일, Local DB, 네트워크를 제공한다.

 

그 중 Preferences는 가장 간단하게 정보를 저장하는 방법(mechanism)을 제공하며, App이나 그 컴포넌트 (Activity, Service 등)의 환경 설정 정보를 저장/복원하는 용도로 사용된다.

 

 

▌Preferences의 형태▐

안드로이드에서 Preferences는 ListView의 형태로 표현되며 쉬운 Preferences의 구현을 위해 PreferenceActivity 클래스를 제공한다. PreferenceActivity 클래스는 XML 기반의 Preference 정의 문서를 통해 App 파일 저장소에 Preferences 파일을 생성하고 사용하는 방식으로 작동한다. (참고: 일반 Activity를 사용해 ListView형태의 Preference Activity가 아닌 커스텀 Preference Activity를 구현 할 수도 있지만 (Container + 각종 UI 위젯 사용) 이 글에서는 그 방법에 대해 다루지 않으려고 합니다)

 

 

▌Preferences 데이터 저장▐

Preferences를 위한 데이터 저장 시 사용되는 자료구조는 Map 방식이다. 즉 키(key)-값(value) 한 쌍으로 이루어진 1~n개의 항목으로 구성된다. 키(key) 는 String 타입으로, 말 그대로 각 데이터에 접근할 수 있게 하는 유일한 구분자며, 값(Value)은 기본 자료형 (int, boolean, string 등) 기반의 데이터로 구성된다. 다음은 Map 데이터 구조의 한가지 예이다.

 키(Key) - String 형
 값(Value) - 기본자료형
 Font
 "Noraml"
 FontSize 20
 FontColor FFFFFFFF
 ... ...
 ... ...


 

위와 같은 Preferences데이터는 안드로이드 디바이스의

data/data/App 패키지 이름/shared_prefs/

경로에 XML형태의 파일로 생성된다. xml 파일 이름은, 파일 생성/접근을 위해 어떤 메서드를 사용하느냐에 따라 시스템이 지정 할 수도 있고, 개발자가 임의의 파일 이름을 지정할 수도 있다.

 

 

참고로, 안드로이드에서 Preferences 데이터 파일을 다수의 App간에 공유하는 것은 기본적으로 불가능 하게 디자인 되어 있다. (참고: 여기서 불가능하다는 뜻은 Preferences Framework에서는 타 App의 환경 설정 파일에 접근할 수 있는 메서드를 제공하지 않는다는 뜻이다. 그럴 필요가 있을지 모르겠지만, 굳이 접근해야 한다면 Preferences 파일 생성 모드를 MODE_WORLD_READABLE로 설정해 guest가 파일을 쓰고 읽을 수 있도록 하고(위에서 MyCustomePref.xml 처럼) Preferences 데이터 파일을 파일 스트림을 통해 읽어와 직접 파싱하고 사용할 수 있는 편법이 있긴 하다)

 

XML파일을 디바이스로부터 추출해 열어보면 Preference 데이터가 다음과 같은 형식으로 저장되어 있음을 볼 수 있다.

1<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
2<map>
3    <boolean name="GreetingMsg" value="true" />
4    <boolean name="sub_checkbox" value="true" />
5    <string name="AdditionalMsg">tigerwoods.tistory.com</string>
6    <string name="TextColor">FF00FF00</string>
7    <string name="Ringtone">content://settings/system/ringtone</string>
8</map>

 

 

Preferences 구현 시 위와 같은 XML 기반 Preferences 데이터 파일을 File I/O API를 사용해 직접 생성/사용 하는 것은 아니다. 안드로이드는 Preferences구현/운용을 위해 android.preference 패키지 및 몇 가지 유용한 클래스/인터페이스(PreferenceActivity, SharedPreferences 등)를 제공하는데, 그것들의 도움을 받으면 손쉽게 Preferences를 구현/운용 할 수 있다.

 

 

▌Preferences 구현▐

안드로이드에서는 Preference 구현을 위해 많은 관련 클래스를 제공하기 때문에 그 클래스들을 적절히 이용하여 다음과 같이 몇 가지 단계만 추가하기만 하면 쉽게 프로젝트에 Preferences 기능을 추가 할 수 있다.

  • 프로젝트 내 어떤 환경정보를 Preferences로 관리 할지 설계
  • 위 설계에 따라 Preferences XML 문서를 작성하고 프로젝트 리소스로 공급 (\res\xml\ 밑)
  • 작성된 Preferences XML문서를 사용하는 PreferenceActivity 구현
  • Preferences가 필요한 Activity로부터 PreferenceActivity를 호출 (Intent 이용)
  • App이 다시 실행될 때 이미 저장된 Preference데이터 파일이 있다면 onResume()과 같은 callback내부에서 환경 설정 값 설정을 불러와 사용

 

위에 나열된 사항 이외에 Preference 데이터 파일의 생성, 데이터 변경 이벤트 listener, 데이터의 저장과 같은 부분은 PreferenceActivity 클래스가 자동으로 관리해 줌으로 개발자는 별로 신경 쓸 필요가 없다. (참고: PreferenceActivity를 사용하지 않고 일반 Activity로 구현한다면 안드로이드가 제공하는 여러 Preferences 관련 클래스 등을 이용해 파일 생성, 이벤트 listener, 저장 등을 직접 구현 해야 한다.)

 

그럼 위 Preference 구현 5단계 중 두 번째인 Preference XML 문서 작성부터 한 번 살펴보자.

 

 

 

 

2. Preference XML 문서 작성하기

 

Preference XML 문서를 작성하기 전에 문서를 구성할 Preferences 관련 클래스들에 관해 약간의 지식이 필요함. 이 글 끝 부분에 "참고. Preference XML 문서 작성시 사용되는 클래스" 단락에 간단한 클래스 설명과 중요 XML 속성, 메서드 등이 설명 되어 있음으로 참고.

 

Preferences XML 문서는 프로젝트 폴더\res\xml\파일이름.xml의 위치에 생성 한다. xml 파일의 이름은 개발자 임의로 지정할 수 있으며 추 후 code 상에서 R.xml.파일이름으로 접근 할 수 있다.

 

이렇게 생성된 XML 문서 내부를 여러 Preference 관련 클래스를 사용해 정의 해야 하는데, 이 글에 포함된 예제 프로젝트에서 사용된 Preference XML 문서를 살펴보면 다음과 같다.

소스 펼치기

 

 

그럼 settings.xml에서 사용된 요소들을 하나씩 분석해 보자

 

 

<PreferenceScreen>

Preferences XML 문서의 root 요소는 항상 <PreferenceScreen>이며 root 요소 내부에는 반드시 xmlns 속성 (xmlns:android=http://......)을 정의해 주어야 한다.

 

root로 사용된 <PreferenceScreen>는 child를 포함하는 container 역할을 하며, PreferenceActivity에게 Preferences 계층 구조를 전달하는 역할을 한다. root로 사용된 <PreferenceScreen>은 container역할 만 하고 자기 자신은 화면에 직접 표현이 안됨으로 title, summary 속성을 지정할 필요가 없다.

 

key 속성에 지정된 문자열은 추 후 code에서 이 preference 계층 구조 내부의 특정 요소를 찾을 수 있는 키 값이 된다.


Preferencescreen root = (PreferenceScreen)getPreference("preference_root");

CheckBoxPreference cb = (CheckBoxPreference)getPreference("checkbox");

 

 

<CheckBoxPreference> & <EditTextPreference>

Root <PreferenceScreen> 밑에 처음 나오는 두 개의 child이며, XML에 추가된 순서대로 Preference view에 표시된다.

각 아이템은 key, title, summary 속성이 지정되어 있으며, checkbox의 경우 초기 설정이 check 상태로 지정되어 있다.

<EditTextPreference> 같은 경우 DialogPreference로부터 상속하며, 클릭 시 별도의 다이얼로그 창을 popup 한다.

 

 

<PreferenceCategory>

예제는 총 2개의 <PreferenceCategory>가 있으며 첫 번째는 <ListPreference>와 <RingtonePreference>를 하나의 Category로 묶는다. 예제 XML의 34번째 라인에 보면 두 번째 Category도 지정되어 있으며 <PreferenceScreen>을 Catefory 멤버로 가지고 있다.

 

<PreferenceCategory>에서 title속성은 다른 Preference 객체와 같이 제목을 표시하지만, summary는 지정되어도 화면에 표시 되지 않는다.

 

또, 첫 번째 category의 속성 중 android:enabled 속성이 false로 지정되어 있음으로 Preference초기 실행 시 child인 <ListPreference>와 <RingtonePreference>는 비활성화 상태로 표현된다. 이와 같이 여러 Preference 아이템을 하나의 category로 묶으면 그룹 속성을 지정하는 것이 가능하다. (Preference의 마지막 항목, checkbox를 check하면 첫 번째 category를 활성화 함)

 

 

<ListPreference>

다른 항목들과 마찬가지로 key, title, summary가 설정되어 있으며, entries 속성에는 사용자에게 보일 ListView 아이템을 지정하며, entryValues속성에는 컴퓨터가 처리할 처리 할 정보를 제공한다. 예를 들면 entries – "Red", "Green", "Blue" 등 사람이 읽을 수 있는 값을 제공하고, entryValues에는 "FFFF0000", "FF00FF00", "FF0000FF" 등 컴퓨터가 사용할 정보를 제공한다.

 

또, android:defaultValue 속성이 "FFFFFFFF"으로 지정되어 있어 사용자가 설정 값을 바꾸기 전까지는 "FFFFFFFF" (흰색)이 기본 값으로 사용된다.

 

 

<RingtonePreference>

key, title, summary가 지정되어 있으며, showDefault와 showSilent가 true로 설정되어 Default 항목과 Slient 항목이 리스트에 표시된다. (<RingtonePreference>는 사용자가 지정한 정보를 바탕으로 Ringtone 설정 기능을 제공하는 Activity들을 찾는 Intent를 발생 시킨다. 디바이스에 여러 종류의 ringtone과 ringtone 관련 activity를 제공하는 app이 설치되어 있다면 더 많은 ringtone 옵션이 화면에 표시 될 수도 있다)

 

 

<PreferenceCategory>

두 번째 그룹으로 CheckBox를 별도의 화면에 표현하는 <PreferenceScreen> child를 포함.

 

 

<PreferenceScreen>

<PreferenceScreen>가 child 요소로 사용된 경우. Preferences에서 하나의 아이템으로 표현되기 때문에 title, summary가 지정되어 있다. 이 아이템을 선택하면 별도의 Preference 창 (dialog 기반)이 Popup한다.

 

 

<CheckBoxPreference>

key, title이 지정되어 있으며, summayOn과 summaryOff에는 각각 check상태의 도움말과 uncheck상태의 도움말이 설정 되어 있다.

 

 

 

 

3. XML + PreferenceActivity이용해 Preference 구현하기

 

작성된 Preference XML 파일을 화면에 표현 가능한 Preference로 만드는데 핵심적인 역할을 하는 것이 바로 PreferenceActivity이다. 전에 살펴본 적이 있는 ListActivity, TabActivity와 마찬가지로 Activity로 상속하며, Preference를 화면에 표현하고 운영하는데 특화된 클래스이다.

 

PreferenceActivity는 Preference 생성/운영에 도움이 되는 다음과 같은 편리한 기능이 구현되어 있다.

  • XML부터 Preference 계층 구조를 전달 받는 메서드 제공
  • 타 Activity의 Preference 계층 구조를 받아오는 메서드 제공
  • 제공된 Preference 계층 구조를 ListView 형태로 자동 구성/표현
  • 제공된 Preference 계층 구조에 따라 디바이스에 Preference 데이터 파일 자동 생성
  • 사용자가 변경한 사항들을 Preference 데이터 파일에 자동 저장

 

 

PreferenceActivity를 이용해 Preference를 구현하려면 크게 두 가지 단계만 거치면 되는데 다음과 같다. (물론 PreferenceActivity도 Activity 생명 주기를 따르기 때문에 onPause(), onResume() 메서드 같은 생명 주기 관련 callback의 구현도 신경 써야 하지만 여기서는 Preference를 구현하는 최소한의 기능만 설명한다)

  • PreferenceActivity를 상속하는 커스텀 클래스 정의.
  • onCreate()에 내부에서 Preference 계층 구조를 가져오는 메서드 호출.

 

위에서 두 번째 구현 순서로 설명된 Preference 계층 구조를 가져오는 메서드는 2개가 제공된다.

우선, \res\xml 밑의 Preference XML로부터 Preference 계층 구조를 얻어올 수 있는 메서드는 다음과 같다.

void addPreferencesFromResource(int)

Parameter:

  • int: Preference XML Resource. (예. R.xml.preferences)

 

두 번째로 타 Activity가 사용하는 Preference 계층 구조를 가져와 적용 시킬 수도 있는데, 이 때 사용되는 메서드는 다음과 같다.

(이 부분은 해결이 안 되는 중요한 버그가 있습니다. main preference까지는 잘 가져와 지는데 별도의 dialog를 띄우는 Preference 아이템을 클릭하면 WindowsManager가 BadTokenException을 발생합니다. 자세한 증상은 첨부된 예제 프로젝트에서 확인하시고, 혹시 해결 방법을 아시는 분은 꼭 댓글 부탁 드립니다)

void addPreferencesFromIntent(Intent)

Parameter:

  • Intent: Preference 계층 구조를 제공할 Activity를 지정하는 Intent

 

구현이 완료된 PreferenceActivity는 타 Activity로 부터 호출 되면 ListView 형태의 Preference 화면을 사용자에게 제공하고, 사용자가 정보를 수정하면 자동으로 Preference 데이터에 저장 하게 된다.

 

PreferenceActivity가 많은 편의 기능을 제공하지만 저장된 설정 복원은 개발자의 몫이다. 즉, 포함된 App이 다시 실행될 때, 기존에 저장된 Preferences설정 값을 Preference 데이터 파일로부터 읽어와 App에 적용 시키는 작업 (ex. App시작 시 Preference 파일에 저장된 폰트 색깔 등을 로드 해 App에 적용시키는)은 개발자가 직접 구현해 주어야 한다. 이를 위해 다음 단락에는 저장된 Preference 데이터 파일에 접근하여 저장된 정보를 가져오는 방법에 대해 알아본다.

 

 

 

 

4. Preference 데이터 파일로부터 환경 복원하기

 

안드로이드에서는 SharedPreferences 인터페이스를 이용해 저장된 Preferences 데이터 파일에 접근한다.

 

 

▌SharedPreferences 인터페이스 얻기▐

 

안드로이드 코드 내에서 SharedPreferences 인터페이스를 얻는 방법은 다음 나열된 세 개의 메서드를 통해서 가능하다. 



SharedPreferences Activity.getPreferences(int)

Parameter:

  • int: Preference 데이터 파일의 생성 시 접근 권한을 설정. Context클래스에 상수로 선언된 MODE_PRIVATE, MODE_WORLD_READABLE, MODE_WORLD_WRITABLE을 설정 가능. MODE_PRIVATE는 지금 App 만 접근 가능하도록 파일을 생성하며 (_rw_rw____), MODE_WORLD_READABLE/WRITABLE은 타 App에서도 파일을 읽고/쓸 수 있도록 생성한다 (_rw_rw_r__ 또는 _rw_rw__w_ 또는 |를 이용해 두 속성을 모두 지정하면 _rw_rw_rw_)

Return:

  • SharedPreferences: 메서드가 호출되는 Activity 클래스의 이름으로 저장된 Preference 데이터 파일의 SharedPreferences 인터페이스를 리턴. 같은 이름의 Preference 데이터 파일이 없을 경우 새로 생성

 

예를 들면 Activity를 상속하는 A, B라는 클래스 이름의 커스텀 Activity가 정의 되어 있다. A 내부에서 getPreference를 호출할 경우 디바이스의 Preference 저장 위치(data/data/패키지이름/shared_prefs/)에 A.xml 이라는 Preference 데이터 파일을 검색하고 파일이 존재 한다면 해당 파일에 대한 SharedPreferences 인터페이스를 리턴 하며, 파일이 존재하지 않을 경우에는 A.xml 파일을 생성한 후 생성한 파일에 대한 SharedPreferences 인터페이스를 리턴한다. 마찬가지로, B 내부에서 getPreference를 호출할 경우 디바이스에서 B.xml 파일을 검색 후 해당 파일에 대한 SharedPreferences 인터페이스를 리턴 한다 (없으면 파일 생성 후 인터페이스 리턴).


 

SharedPreferences Context.getSharedPreferences(String, int)

Parameter:

  • String: 사용(생성)할 Preference 데이터 파일의 이름을 지정
  • int: 생성될 Preference 데이터 파일의 생성 시 접근 권한을 설정

Return:

  • SharedPreferences: 첫 번째 인자로 전달되는 문자열과 일치하는 이름의 Preference 데이터 파일의 SharedPreferences 인터페이스를 리턴. 만약 같은 이름의 파일이 없다면 파일을 새로 생성하고 생성된 파일에 대한 SharedPreferences 인터페이스를 리턴

 

한가지 참고할 사항은 이 메서드를 통해 개발자가 생성/사용될 Preference 데이터 파일의 이름을 임의로 설정할 수 있다는 것인데, 예를 들어, A라는 Activity에서 getSharedPreferences("initial_setting", MODE_PRIVATE)라는 메서드를 실행한다면, preference framework는 /data/data/패키지/shared_pref/ 밑에서 initial_setting.xml이라는 파일을 검색하고 없다면 이를 새로 생성 후 initial_setting.xml에 대한 SharedPreferences 인스턴스를 리턴 하게 된다..

 

제일 처음에 설명한 Activity.getPreferences(int)도 실제로는 자기자신을 호출한 Activity(또는 컴포넌트)의 이름과 입력된 int 형 인자를 가지고 본 메서드를 호출하는 형태로 구현되어 있다.




Static SharedPreferences PreferenceManager.getDefaultSharedPreferences(Context)

Parameter:

  • Context: 사용하기 원하는 SharedPreferences 인터페이스를 포함하는 Context 지정

Return:

  • SharedPreferences: 인자로 지정된 Context (app 구동 환경)가 기본으로 생성하는 Preference 데이터 파일에 대한 SharedPreferences를 리턴

 

이 메서드는 App level의 기본 Preference 데이터 파일을 생성/사용하는데 사용된다. 예를 들어 com.holim.test.aaa 라는 패키지에 포함되는 A Activity에서 본 메소드를 호출 하면 Preference 프레임웍은 /data/data/패키지이름/shared_pref/com.holim.test.aaa_preferences.xml 이라는 Preference 데이터 파일을 생성한다. 전달된 Context 인자에 따라 패키지이름_preferences.xml 형태로 생성/사용할 Preference 데이터 파일의 이름이 시스템에 의해 자동으로 지정되게 되기 때문에, 호출되는 위치가 어디이든 같은 Context를 인자로 전달해 호출한 getDefaultSharedPreferences(…)메서드는 항상 같은 SharedPreferences 인스턴스를 리턴한다.

 

한가지 참고할 사항은 본 메서드는 static 메서드이기 때문에 메서드를 제공하는 PreferenceManager 클래스를 따로 생성할 필요 없이 다음과 같이 사용하면 된다.

SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);

 

 

이와 같이 여러 메서드를 사용해 얻어온 SharedPreferences 인터페이스로 무엇을 할 수 있는 지 알아 보자. 다음은 SharedPreferences 인터페이스가 제공하는 중요 메서드들이다.

 

 

▌SharedPreferences 인터페이스▐

 

SharedPreferences 인터페이스가 제공하는 기능은 다음과 같다.

  • 디바이스에 저장되어 있는 Preference 데이터 파일로부터 데이터를 추출하는 여러 메서드
  • Preference 데이터를 수정하는 방법을 제공하는 Editor 내부 인터페이스
  • Preference 데이터가 변경되었을 때 호출되는 callback 인터페이스

 

중요 메서드

SharedPreferences.Editor edit() – SharedPreferences가 연결된 Preference 데이터 파일을 수정 할 수 있게 여러 메서드를 제공하는 Editor 인터페이스 리턴. Editor 인터페이스는 자료 수정을 완료 한 후 Editor.commit() 메소드를 사용해야 파일에 변경내용이 저장됨.

void registerOnSharedpreferenceChangeListener(…) – Preference 변경 이벤트 Listener 등록

void unregisterOnSharedPreferenceChangeListener(…) – 위에 등록한 이벤트 Listener 등록 해지

Map <String, ?> getAll() – 모든 Preferences 값을 Map 자료형으로 리턴

boolean contains(String) – 인자로 제공되는 문자열과 같은 key의 Preference 항목이 있으면 true 리턴

타입 get타입(String, 타입) – Boolean, Float, Int, Long, String 형 메서드가 있으며 첫 인자는 데이터를 가져올 Key 값이며, 두 번째 인자는 찾는 preference 데이터가 존재하지 않을 때 리턴 할 값이다.

Boolean isMarried = sharedPref.getBoolean("결혼여부", false);

int fontSize = sharedPref.getInt("폰트사이즈", 20);

 

중요 인터페이스

  • SharedPreferences.Editor – Preference 파일에 저장된 항목을 수정/제거 할 수 있는 여러 메서드를 제공하는 인터페이스
  • SharedPreferences.OnSharedPreferenceChangeListener – Preference 데이터가 변경되었을 때 호출되는 callback 인터페이스

 

 

예제 프로젝트 중 MainActivity.java의 onCreate(…)와 onPause(…)메서드에서 보면 지금까지 설명한 SharedPreferences 인터페이스 얻기와 인터페이스가 제공하는 여러 메서드를 사용해 Preference 데이터 파일로부터 저장된 정보를 가져와 사용하는 것을 볼 수 있다.


MainActivity.onCreate()

소스 펼치기

 

MainActivity.onResume() 

소스 펼치기

 

 

 

 

참고. Preference XML 문서 작성시 사용되는 클래스

 

전 글들에서 다루었던 XML을 이용한 UI 및 메뉴 구성에서도 봤듯이

XML을 이용하면 디자인과 Logic을 분리 시킬 수 있어 간결한 코드의 구성이 가능할 뿐 아니라 현지화/유지보수 등 작업을 하는데 훨씬 편리 할 수 있음으로 권장되는 구현 방식 이라고 설명 한 적이 있다.

 

Preference에서도 마찬가지로 XML을 이용해 Preferences 구조를 쉽게 정의할 수 있다.

 

Preference XML 문서 작성시 android.preference 패키지 밑의 여러 클래스가 사용되는데 패키지 내부의 중요한 클래스의 상속 구조는 다음과 같다.

 

 

붉은 색 사각형 내부의 클래스가 Preferences 과 직접적으로 연관이 있는 클래스이며, 그 중 Preference 클래스를 비롯한 하위 클래스들이 XML Preference 문서 작성에 사용된다.

 

그럼 Preference 클래스부터 한번 살펴보자

  


 

▌Preference 클래스▐

여러 Preference 관련 클래스의 최상위 부모 클래스 이므로, 제공하는 XML 속성, 메서드는 자식 Preferences 관련 클래스에서도 모두 사용할 수 있다. XML 내부에서 직접적으로 사용되지는 않는다.

 

중요 XML 속성

  • android:key – Preferences 데이터 저장/불러올 때 사용되는 키(key) 지정
  • android:title – Preference 항목의 제목을 지정
  • android:summary – Preference 항목을 자세히 설명하는 문자열 지정
  • android:enabled – Preference 항목의 활성화 비활성화 여부 지정 (true/false)
  • android:selectable – Preference 항목의 선택 가능 여부 결정 (true/false)
  • android:order – Preference 항목이 표시되는 순서를 결정 (0-based 정수 사용: 낮은 값 먼저 보임). 순서가 명시적으로 지정되지 않는다면 XML에 정의된 순서대로 표시됨

 

중요 메서드

  • void setKey/Title/Summary(…) – Preference 항목의 키/제목/설명을 지정
  • void setEnabled(boolean) - Preference항목의 활성화/비활성화 여부 지정
  • void setSelectable(boolean) – Preference 항목의 선택 가능 여부 결정
  • void setLayoutResource(int) – Preference 항목에 표시할 View를 R 클래스로부터 지정
  • void setOnPreferenceChangedListener(…) – Preference 항목에 변화가 있으면 호출될 callback을 지정
  • void setOnPreferenceClickListener(…) – Preference 항목에 click 이벤트가 발생하면 호출될 callback을 지정

 

중요 인터페이스

  • Preference.OnPreferenceChangeListener - 사용자가 Preference를 변경하였을 때 호출되는 callback에 대한 인트페이스
  • Preference.OnPreferenceClickListener - 사용자가 Preference를 클릭하였을 때 호출되는 callback에 대한 인터페이스

 

  


▌PreferenceGroup 클래스▐

 

Preferences를 구성하는 객체들을 담을 수 있는 Container 클래스. PreferenceCategory와 PreferenceScreeen을 파생. XML 내부에서 직접적으로 사용되지는 않음.

 

중요 메서드

  • boolean addPreference(Preference) – 새로운 Preference 항목을 그룹에 추가. 추가 성공/실패 리턴.
  • Preference findPreference(String) – 인자로 전달되는 문자열과 같은 key값을 갖는 group 내 (자기자신포함) Preference 항목 리턴
  • Preference findPreference(int) – 인자(0-based정수)에 명시된 위치의 Preference 항목 리턴. 기본적으로 가장 처음 추가된 항목이 index 0
  • void removeAll() – 이 그룹 내 모든 Preference 항목을 삭제함
  • void removePreference(Preference) - 인자로 전달된Preference 항목을 삭제함
  • void setEnabled(boolean) – 그룹 전체의 활성화/비활성화 여부 지정
  • void setOrderingAsAdded(boolean) – true일 경우 그룹에 추가된 순서로 표현. false일 경우 Preference아이템 내 순서 정보가 있으면 그걸 따르고 순서 정보가 없으면 Preference 아이템의 title을 이용해 알파벳순 정렬.

  


 

▌PreferenceScreen 클래스▐

 

PreferenceGroup에서 파생하며, 실제로 Preference XML 문서 내부에 사용 되는 클래스이다. 여러 Preference 구성 요소를 담을 수 있는 root container역할을 하며 Activity에 표현될 Preference의 계층구조를 나타내는 클래스이다. PreferenceActivity에 전달되어 이 클래스의 인스턴스가 포함하는 것들을 화면에 표현한다.

 

이 클래스가 Preference XML 문서에서 사용될 때는 다음과 같이 두 가지 용도로 사용된다.

  • Preference XML 문서의 root 요소 사용 시 - 경우에는 PreferenceActivity에 전달되어 preference의 전체 구조를 전달하는 용도로 사용. 이 경우 자기 자신은 화면에 표현 안되고 child 요소를 포함하는 Container의 용도로 사용됨 (XML UI에서 Containter는 화면에 표현 안되고 것과 같은 의미)
  • Preference XML 문서에서root가 아닌 child 요소로 사용 시 - Preference 아이템 중 하나로 취급. Preference 아이템 중 하나로 화면에 표시되며, 클릭 시 별도의 preference 화면으로 이동

 

!!! 그림 (main pref. -> sub pref)

 


 

▌PreferenceCategory 클래스▐

 

Preferences를 구성하는 객체들을 특정 category별로 분류 할 수 있게 하는 클래스. 중요한 XML 속성이나 메서드 없음

!!! 그림 (category 분류)

 

  


▌DialogPreference 클래스▐

 

모든 dialog 기반 Preference 객체의 부모 클래스. Preference 화면에는 링크만 표시되고, 링크를 클릭했을 때 실제 Preference를 컨트롤 할 수 있는 Dialog가 popup한다.

!!! 다이얼로그 그림

 

중요 XML 속성

  • android:dialogIcon – Dialog의 Icon 지정
  • android:dialogTitle – Dialog의 제목 지정
  • android:dialogMessage – Dialog 내에 표시될 문자열 내용 지정
  • android:negativeButton – 부정 버튼에 표시될 Text설정 (취소, cancel 등)
  • android:positiveButton – 긍정 버튼에 표시될 Text 설정 (적용, OK 등)

 

중요 메서드

  • void setDialogIcon(Drawable) – Dialog의 Icon 지정
  • void setDialogTitle(…) – Dialog의 제목 지정. 문자열 직접 지정 또는 문자열 리소스 사용
  • void setDialogMessage(…) – Dialog 내에 표시될 문자열 내용 지정
  • void setPositiveButtonText(…) – 부정 버튼에 표시될 Text설정 (취소, cancel 등)
  • void setNegativeButtonText(…) – 긍정 버튼에 표시될 Text 설정 (적용, OK 등)

  


 

▌EditeTextPreference 클래스▐

 

DialogPreference로부터 상속하며, 사용자로부터 문자열을 입력 받아 저장 하기 위한 Preference 아이템을 구현한 클래스.

 

중요 메서드

  • EditText getEditText() – Dialog가 포함하는 EditBox인스턴스를 리턴
  • String getText() – SharedPreferences 객체로부터 저장된 문자열 리턴
  • void setText() – SharedPreferences에 문자열 저장

  


 

▌ListPreference 클래스▐

 

DialogPreference로부터 상속하며, Dialog기반의 ListView 위젯에 미리 제공되는 문자열들을 표현하고 사용자가 그 문자열 중 하나를 선택하여 설정 값을 지정할 수 있도록 구현한 클래스.

 

중요 XML 속성

  • android:entries – ListView의 각 raw에 표현될 문자열을 array 리소스를 통해 공급. 사용자(human)를 위한 정보.
  • android:entryValues – ListView의 특정 raw가 사용자에 의해 선택되었을 때 프로그램 내부적으로 처리 할 문자열 data를 array 리소스 형식으로 제공. 컴퓨터가 처리 하기 위한 정보. (ex. 남/여: 사람을 위한 정보 -> 0/1: 컴퓨터를 위한 정보)
  • android:defaultValue – 초기 선택 항목 지정. (0-based 정수)

 

중요 메서드

  • CharSequence[] getEntries() – ListView의 각 row에 표현될 문자열들을 리턴 (사용자 위한 정보)
  • CharSequence[] getEntryValues() – Entry(사람을 위한 정보)와 연계된 컴퓨터가 처리할 문자열들을 리턴
  • void setEntries(…) – ListView의 각 row에 표현할 문자열 지정. Array리소스 또는 CharSequence[] 전달
  • void setEntryValues(…) – 컴퓨터가 처리할 문자열 지정. Array또는 CharSequence[] 전달

  


 

▌CheckBoxPreference 클래스▐

 

CheckBox 기능의 Preference 아이템을 구현한 클래스. SharedPreferences에 boolean 값으로 정보 저장

 

중요 XML 속성

  • android:summayOn – CheckBox가 check상태일 때 사용자에게 보일 안내문
  • android:summaryOff – CheckBox가 uncheck 상태일 때 사용자에게 보일 안내문

 

중요 메서드

  • boolean isChecked() – 현재 check/uncheck 상태 리턴
  • void setChecked(boolean) – CheckBox를 program 상에서 check/uncheck 함
  • void setSummaryOn(…) – CheckBox가 check상태일 때 사용자에게 보일 안내문 설정. String 리소스나 CharSequence 인스턴스 전달
  • void setSummaryOff(…) – CheckBox가 uncheck상태일 때 사용자에게 보일 안내문 설정

  


 

▌RingtonPreference▐

 

사용자가 디바이스에서 제공하는 전화 벨 소리를 지정할 수 있게 구현된 클래스. Intent를 이용해 어떤 벨 소리 Picker를 화면에 표시할 지 결정함.

 

중요 XML 속성

  • android:ringtoneType – 어떤 종류의 벨 소리 종류를 선택 가능하게 할지 지정. ringtone, notification, alarm, all 중에 하나 또는 복수(| 사용)개 지정 가능
  • android:showDefault – Default 벨소리 항목 표시 여부 결정 (true/false)
  • android:showSilent – 무음(Silent) 항목 표시 여부 결정 (true/false)

 

중요 메서드

  • setRingtoneType(int type) – XML 속성 중 ringtoneType에 대응. RingtoneManager 클래스에 선언되어 있는 상수 TYPE_RINGTONE, TYPE_NOTIFICATION, TYPE_ALARM, TYPE_ALL 중 하나 또는 복수(|사용) 전달
  • setShowDefault(boolean) – XML showDefault 속성에 대응
  • setShowSlient(boolean) – XML showSilent 속성에 대응

  


 

 PreferenceManager 클래스 

 

Activity나 XML로부터 Preferences 계층구조의 생성을 돕는 helper 클래스이지만 이 클래스를 이용하여 직접 Preferences를 구성하기보다는 PreferenceActivity.addPreferenceFromResource(int) 또는 PreferenceActivity.addPreferenceFromIntent(Intent)를 사용하는 것이 일반적이다.

 

혹시, PreferenceActivity를 사용하지 않고 Activity를 이용해 직접 Preferences 화면을 구성해야 한다면 필요한 클래스이다.

 

중요 메서드

  • Preference findPreference(CharSequence) – 인자로 전달되는 Key값을 가지는 Preference 항목의 인스턴스를 가져옴
  • SharedPreferences getDefaultSharedPreferences(Context) – 제공되는 context가 기본으로 사용하는 Preference파일로부터 SharedPreferences인스턴스 생성해 리턴
  • SharedPreferences getSharedPreferences() – this객체가 사용하는 context와 연결될 Preference파일로부터 SharedPreferences 인스턴스 생성해 리턴
  • void setDefaultValues(Context, int resId, boolean) – 제공되는 context가 사용하는 SharedPreferences를 두 번째 인자로 제공되는 XML 리소스에 정의된 기본 속성값들로 설정 함. 세 번째 인자가 false 일 경우엔 이 메서드가 전에 실행된 적이 있다면 무시하고, true일 경우 전에 실행 여부화 상관없이 재 실행된다. 참고: 마지막 인자를 true로 지정 했다고 해서 이 메서드를 이용해 Preferences를 바로 초기화 할 수 있는 것은 아니고, SharedPrefernces.clear() 메서드를 사용해 Preferences 항목을 모두 삭제 후 이 메서드를 사용해 XML에 지정된 본래의 값으로 Preferences를 초기화 할 수 잇다.

 

 

오늘은 JNI를 이용한 C모듈을 안드로이드에 적용하는 방법을 설명하겠습니다.

이 부분은 좀 방대한 부분이고, 저 또한 짧은 시간에 글을 적어야 하므로 충분한 캡처를 동반하지 못함을 아쉽게 생각합니다.

 

우선 기본환경은

이클립스  +  NDK r5b + 우분투 11.04 이며, 모듈 컴파일은 Cygwin에서 사용해도 상관 없습니다.

(참고로 저는 윈도우즈 환경에서 우분투를 VM 으로 사용합니다.)

 

작업순서는 다음과 같이 하려 합니다.

 

가. 모듈 컴파일을 위한 NDK  다운로드 및 환경설정

나. *.java 에서 *.h, *.c의 JNI 파일 만드는 방법

다. 안드로이드 프로젝트 내에 JNI 쓰는 방법

 

가. 리눅스에서 NDK  설정

 

1. 우선 NDK 부터 사이트에서 받도록 합니다. 윈도우즈 환경에서 MS cl.exe 컴파일러를 이용해 *.dll 모듈을 만들어도 되지만

 저는 리눅스의 *.so 모듈을 만들기 위해서 리눅스용 버전을 다운로드 받습니다.

http://developer.android.com/sdk/ndk/index.html  

 

2. 다운로드를 받았으면 압축을 해제하고 자신의 홈 계정 디렉토리 밑으로 옮깁니다.

 예) home/maluchi/android-ndk-r5b


maluchi@ubuntu:~/android-ndk-r5b$ ls
GNUmakefile  build               ndk-build  projects  tests
README.TXT   docs                ndk-gdb    samples   toolchains
RELEASE.TXT  documentation.html  platforms  sources
maluchi@ubuntu:~/android-ndk-r5b$ pwd
/home/maluchi/android-ndk-r5b

3. 폴더 위치에 상관없이 ndk-build 명령이 수행되도록 PATH를 다음처럼 추가합니다.

   자신의 홈으로 가서 .bash_profile 파일을 만들어서 다음처럼 추가하고 저장합니다.

   PATH=$PATH:$HOME/android-ndk-r5b

maluchi@ubuntu:~/android-ndk-r5b$ cd
maluchi@ubuntu:~$ ls
android-ndk-r5b          androiddev        workspace  문서      사진
android_gingerbread      examples.desktop  공개       바탕화면  음악
android_gingerbread.tgz  froyo2.2.tgz      다운로드   비디오    템플릿
maluchi@ubuntu:~$ vi .bash_profile 

4. 콘솔에서 source ~/.bash_profile 을 해서 바로 업데이트 하고, 잘 입력이 되었는지 env 명령을 넣어 확인합니다.

maluchi@ubuntu:~$ source ~/.bash_profile 
maluchi@ubuntu:~$ env
......

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/maluchi/android-ndk-r5b
....

 

5. 마지막으로 NDK 폴더 내 samples->hello-jni->jni->Android.mk 를 열어 보면 다음과 같은 부분이 있습니다.

LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c

 

LOCAL_MODULE 은 모듈명이며, LOCAL_SRC_FILES는 C의 구현파일입니다. 여러개 존재한다면 '\ ' 추가합니다.

차후에 Android.mk를 복사해서 수정할 것입니다.

예)

LOCAL_SRC_FILES := hello-jni.c \

                                    test.c

 

6. Android.mk 파일 내용을 봐 보겠습니다. 리눅스에서 해당 파일을 받기 힘드시면 아래 내용으로 파일을 만들어도 됩니다. 

maluchi@ubuntu:~/android-ndk-r5b/samples/hello-jni/jni$ more Android.mk 
# Copyright (C) 2009 The Android Open Source Project
#
# 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.
#
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES := hello-jni.c

include $(BUILD_SHARED_LIBRARY)

maluchi@ubuntu:~/android-ndk-r5b/samples/hello-jni/jni$

 

이상으로 리눅스에서 모듈을 컴파일을 위한 환경은 완료가 되었습니다.

 

-------------------------------------------------------------------------------------------------

 

나. *.java 에서 *.h, *.c의 JNI 파일 만드는 방법

 

1. 이제 C모듈(*.so)를 java로 가져오는 부분이 필요합니다.  CalcJni.java 라는 파일을 만듭니다.

그리고 다음처럼 추가합니다. 사칙연산을 수행하는 네이티브 메서드를 정의하고 "CalcJni" 라는 C 모듈을 로드하는 역할을 합니다.

01.//package com.maluchi.CalcJNITest;
02.public class CalcJni
03.{
04.public CalcJni(){};
05.public native int Sum(int a, int b);
06.public native int Sub(int a, int b);
07.public native int Mul(int a, int b);
08.public native int Div(int a, int b);
09.public native String message();
10. 
11.static{
12.System.loadLibrary("CalcJni");
13.};}


2. 위의 Java 파일로부터 JNI의 C헤더(*.h) 파일을 만드는 것을 수작업으로 하기에는 번거로운 면이 있습니다.

하지만, Java에서 친절하게도 C헤더 파일을 만들어주는 명령이 있습니다. cmd 창을 하나 띄웁니다.

 

3. 자바 SDK가 설치되었다는 전제하에 해당 경로로 이동하여 *.class 파일을 만듭니다.

 >javac CalcJni.java

 

4. *.h 헤더 파일을 만듭니다.

>javah CalcJni

 

5. 다음처럼 CalcJni.h 헤더가 만들어진 것을 확인할 수 있습니다.

C:\tmp\jni>dir
 C 드라이브의 볼륨: Win7
 볼륨 일련 번호: D037-8597

 C:\tmp\jni 디렉터리

2011-06-16  오후 04:23    <DIR>          .
2011-06-16  오후 04:23    <DIR>          ..
2011-06-16  오후 04:22               425 CalcJni.class
2011-06-16  오후 04:23             1,011 CalcJni.h
2011-06-16  오후 04:22               391 CalcJni.java
               3개 파일               1,827 바이트
               2개 디렉터리  20,543,619,072 바이트 남음

6. CalcJni.h 에 맞춰 CalcJni.c 파일을 만듭니다. 이 때 가장 중요한 메서드 명명 규칙이 있습니다.

javah로 *.h를 만들면 아래처럼 패키지명이 적용되지 않고 만들어집니다.

 JNIEXPORT jint JNICALL Java_CalcJni_Sum(JNIEnv *, jobject, jint, jint);

 

중요한 것은 JNICALL Java_패키지명_클래스명_메서드(...)  규칙으로 적용해야 한다는 것입니다.

실제 안드로이드 패키지를 만드고 그곳에서 C 모듈을 로드한다면 반드시 패키지명_클래스명 을 확인해야 합니다.

그렇지 않으면 에러의 원인이 됩니다. 더불어 JNI에서의 구현부는 반드시 파라미터명을 주도록 되어 있으므로 생략되어 있는 파라미터 변수명들을 넣습니다.

 

 다시 1번에 가서 보시면 //package com.maluchi.CalcJNITest; 주석 처리된 것을 볼수 있습니다.

이는 후에 저 패키지에 자바파일이 들어가게 되고  메서드명들을 아래처럼 모두 바꿀 필요가 있기에 임시 주석처리를 한 것입니다. 

예) JNIEXPORT jint JNICALL Java_com.maluchi.CalcJNITest_CalcJni_Sum(JNIEnv *, jobject, jint, jint);

 

CalcJni.h

01./* DO NOT EDIT THIS FILE - it is machine generated */
02.#include <jni.h>
03./* Header for class CalcJni */
04. 
05.#ifndef _Included_CalcJni
06.#define _Included_CalcJni
07.#ifdef __cplusplus
08.extern "C" {
09.#endif
10./*
11.* Class:     CalcJni
12.* Method:    Sum
13.* Signature: (II)I
14.*/
15.JNIEXPORT jint JNICALL Java_com.maluchi.CalcJNITest_CalcJni_Sum
16.(JNIEnv *, jobject, jint, jint);
17. 
18./*
19.* Class:     CalcJni
20.* Method:    Sub
21.* Signature: (II)I
22.*/
23.JNIEXPORT jint JNICALL Java_com.maluchi.CalcJNITest_CalcJni_Sub
24.(JNIEnv *, jobject, jint, jint);
25. 
26./*
27.* Class:     CalcJni
28.* Method:    Mul
29.* Signature: (II)I
30.*/
31.JNIEXPORT jint JNICALL Java_com.maluchi.CalcJNITest_CalcJni_Mul
32.(JNIEnv *, jobject, jint, jint);
33. 
34./*
35.* Class:     CalcJni
36.* Method:    Div
37.* Signature: (II)I
38.*/
39.JNIEXPORT jint JNICALL Java_com.maluchi.CalcJNITest_CalcJni_Div
40.(JNIEnv *, jobject, jint, jint);
41. 
42./*
43.* Class:     CalcJni
44.* Method:    message
45.* Signature: ()Ljava/lang/String;
46.*/
47.JNIEXPORT jstring JNICALL Java_com.maluchi.CalcJNITest_CalcJni_message
48.(JNIEnv *, jobject);
49. 
50.#ifdef __cplusplus
51.}
52.#endif
53.#endif


CalcJni.c

01.#include <stdio.h>
02.#include "CalcJni.h"
03. 
04.JNIEXPORT jint JNICALL Java_com_maluchi_CalcJNITest_CalcJni_Sum
05.(JNIEnv *env, jobject obj, jint a, jint b)
06.{
07.return a+b;
08.}
09./*
10.* Class:     CalcJni
11.* Method:    Sub
12.* Signature: (II)I
13.*/
14.JNIEXPORT jint JNICALL Java_com_maluchi_CalcJNITest_CalcJni_Sub
15.(JNIEnv *env, jobject obj, jint a, jint b)
16.{
17.return a-b;
18.}
19./*
20.* Class:     CalcJni
21.* Method:    Mul
22.* Signature: (II)I
23.*/
24.JNIEXPORT jint JNICALL Java_com_maluchi_CalcJNITest_CalcJni_Mul
25.(JNIEnv *env, jobject obj, jint a, jint b)
26.{
27.return a*b;
28.}
29./*
30.* Class:     CalcJni
31.* Method:    Div
32.* Signature: (II)I
33.*/
34.JNIEXPORT jint JNICALL Java_com_maluchi_CalcJNITest_CalcJni_Div
35.(JNIEnv *env, jobject obj, jint a, jint b)
36.{
37.if(a == 0 || b ==0)
38.return 0;
39.return a/b;
40.}
41./*
42.* Class:     CalcJni
43.* Method:    message
44.* Signature: ()Ljava/lang/String;
45.*/
46.JNIEXPORT jstring JNICALL Java_com_maluchi_CalcJNITest_CalcJni_message
47.(JNIEnv *env, jobject obj)
48.{
49.return (*env)->NewStringUTF(env, "Hello world!! Welcome to maluchi's world");
50.}


7. 이제 JNI 파일까지 모두 준비가 됐습니다.

 

------------------------------------------------------------------------------------------------------

 

다) 안드로이트 프로젝트에서 JNI 사용하는 방법

 가)에서 NDK를 설정하여 *.so 파일을 만드는 환경 설정을 했고, 

 나)에서 *.so 의 구현 C모듈에서 Java로 읽어들오도록 인터페이스를 만들고 c 파일 구현까지 했습니다.

 

이제 실질적으로 구현한 C 파일을 리눅스에서 컴파일을 하고 *.so 파일을 얻은 후 안드로이트 프로젝트에 넣어서 실제 동작하는지 테스트 해보는 일만 남았습니다.

이 작업을 설명하겠습니다.

 

우선 안드로이드 프로젝트를 하나 만듭니다.

 

1. CalcJNITest 라는 프로젝트를 만들고 패키지경로를 com.maluchi.CalcJNITest 로 합니다.

 

2. 이 프로젝트에 jni 라는 폴더를 만들고 아까 만들어둔 CalcJni.h, CalcJni.c, CalcJni.java 를 넣고, 가) 에서 봤던 Android.mk도 복사해서 넣습니다.

반드시 jni 라는 폴더에 넣어야 합니다. 그리고 Android.mk 의 LOCAL_MODULE 과  LOCAL_SRC_FILES을 다음처럼 수정합니다.

 

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := CalcJni
LOCAL_SRC_FILES := CalcJni.c

include $(BUILD_SHARED_LIBRARY)

 

3. 이제 CalcJNITest라는 프로젝트를 리눅스 NDK홈에 projects라는 폴더를 만들고 projects 폴더로 옮깁니다.

maluchi@ubuntu:~/android-ndk-r5b/projects$ pwd
/home/maluchi/android-ndk-r5b/projects

maluchi@ubuntu:~/android-ndk-r5b/projects$ ls
CalcJNITest

4. CalcJNITest에 프로젝트에 들어가서 ndk-build 명령을 넣습니다.

해당 프로젝트에 jni 폴더 밑에 c 파일이 있고, ndk-build 는 이를 바탕으로 Libs 폴더를 만들어 *.so 파일을 생성합니다.

진행은 다음처럼 하시면 됩니다..

 

maluchi@ubuntu:~/android-ndk-r5b/projects/CalcJNITest$ ls
AndroidManifest.xml  bin                 gen  proguard.cfg  src
assets               default.properties  jni  res
maluchi@ubuntu:~/android-ndk-r5b/projects/CalcJNITest$ ndk-build
Compile thumb  : CalcJni <= CalcJni.c
SharedLibrary  : libCalcJni.so
Install        : libCalcJni.so => libs/armeabi/libCalcJni.so
maluchi@ubuntu:~/android-ndk-r5b/projects/CalcJNITest$ 

5. 결국 프로젝트에 Libs/armeabi/libCalcJni.so 파일이 만들어짐을 알게 됩니다.

Libs를 통째로 복사해서 윈도우의 해당 안드로이드 프로젝트에 복사합니다.

 

6. 이제 이 C구현부를 java로 불러들이는 일만 남았습니다.

 아까 만들어둔 jni/CalcJni.java를 com.maluchi.CalcJNITest 패키지로 복사를 합니다.

 그리고 CalcJni.java 파일 내에 주석 처리한 package package com.maluchi.CalcJNITest; 주석해제 시킵니다.

 

7. 구현을 다음과 같습니다.

01.package com.maluchi.CalcJNITest;
02. 
03.import android.app.Activity;
04.import android.os.Bundle;
05.import android.os.Handler;
06.import android.util.Log;
07.import android.widget.TextView;
08.public class CalcJNITest extends Activity {
09./** Called when the activity is first created. */
10.@Override
11.public void onCreate(Bundle savedInstanceState) {
12.super.onCreate(savedInstanceState);
13.setContentView(R.layout.main);
14. 
15.CalcJni jni = new CalcJni();
16. 
17.String string ="";
18.string = "Sum :"+jni.Sum(1010)+
19.", Sub: "+jni.Sub(2010)+
20.", Mul: "+jni.Mul(10010)+
21.", Div: "+jni.Div(10010)+
22.", Message: "+jni.message();
23. 
24.Log.d("maluchi", string);
25.TextView v = (TextView) findViewById(R.id.txt);
26.v.setText(string);
27. 
28.// mHandler.sendEmptyMessageDelayed(0, 300);
29. 
30.}
31. 
32.Handler mHandler = new Handler()
33.{
34.public void handleMessage(android.os.Message msg) {
35. 
36.CalcJni jni = new CalcJni();
37.String string = "";
38.string = "Sum :" + jni.Sum(1010) + ", Sub: " + jni.Sub(2010)
39.", Mul: " + jni.Mul(10010) + ", Div: "
40.+ jni.Div(10010) + ", Message: " + jni.message();
41.TextView v = (TextView) findViewById(R.id.txt);
42.v.setText(string);
43.}; 
44.};
45.}


마지막으로 에러 관련 상황입니다.

06-16 06:33:34.304: ERROR/AndroidRuntime(602): Uncaught handler: thread main exiting due to uncaught exception
06-16 06:33:34.354: ERROR/AndroidRuntime(602): java.lang.UnsatisfiedLinkError: Sum
06-16 06:33:34.354: ERROR/AndroidRuntime(602):     at com.maluchi.CalcJNITest.CalcJni.Sum(Native Method)
 

위처럼 나타나는 에러는 JNI의 메서드명 규칙의 JNICALL Java_ 패키지명_클래스명_메서드명() 규칙이 잘못됐을 때 나타는 현상입니다.

 

만약 다음과 같은 예외가 발생한다면 라이브러리 패스가 올바르게 설정되어 있는지 확인합니다.

java.lang.UnsatisfiedLinkError: no hello in shared library path at java.lang.Runtime.loadLibrary(Runtime.java)
        at java.lang.System.loadLibrary(System.java)
       at java.lang.Thread.init(Thread.java)


<목표> [안드로이드] 터치화면, 제스처 기능을 이용한 터치 인식

       

 

   오늘은  터치화면을 이용한 제스처 기능에 대해서 알아보겠습니다. 제스처 기능은 말그래로 "동작, 움직임" 을 감지합니다. 하지만 핸드폰 자체의 센서를 이용한 움직임은 아닙니다. 즉, 핸드폰을 흔들거나, 기울이거나를 해서 얻는 이벤트를 이용해서 어떠한 기능을 수행하는 기능이 아닙니다. 제스처 기능은 화면 내의 터치를 감지하여, 어떠한 이벤트가 들어왔는지를 분석하여 어떤 기능을 수행할 수 있도록 합니다. 간단한 예로는 화면을 넘기는 것을 예로 들 수 있습니다. 흔히 메인 화면은 여러 개의 화면으로 구성되어 있습니다. 왼쪽에서 오른쪽으로 화면을 넘기기 위해서 손가락을 이용해서 왼쪽에서 오른쪽으로 드래그하는 동작을 취합니다. 이런 방식으로 손가락 모션을 인식하는 것이 제스처 기능입니다.

 

   아래의 예제 코드는 왼쪽에서 오른쪽으로 드래그했을 때, 오른쪽에서 왼쪽으로 드래그를 했을 때, 아래에서 위로, 위에서 아래로 손가락으로 드래그했을 시에 이벤트를 발생시킬 수 있도록 하는 기능을 담고 있습니다. 이벤트가 발생하면 간단하게 토스트를 띄워서 제대로 동작하고 있는지를 확인하도록 되어있습니다. 그리고 부가적으로, 한번 클릭했을 때, 길게 클릭했을 때 등의 기능도 포함되어 있습니다.

 

   Android & Amir 홈페이지에서 가져왔으며, 한 두가지 오류를 찾아 수정한 것입니다.

       

[ 제스처 결과 화면 ]

(오른쪽 방향으로 Swipe하였을 때)

       

STEP 1  Java Source Code

       

   예제는 간단한 한 화면만 있습니다. 상위에는 현재 마우스 클릭(손가락 터치)이 어떠한 방식으로 되고 있는지 출력하는 텍스트 박스가 있으며, 하위에는 마우스 제스처 (손가락 터치 모션) 를 테스트해볼 수 있는 화면이 구성되어 있습니다. 레이아웃은 Xml을 통해 구현하지 않았고, 바로 코드를 이용해서 추가하도록 하였습니다. 눈여겨 봐야할 것은, 제스처를 인식하는 방법입니다.

   

   

  [[ 예제 코드 ]]   Touch UI Gesture (제스처 기능)

   

 

package pulsebeat.example.gesture;

 

import android.app.Activity;

import android.graphics.Color;

import android.os.Bundle;

import android.view.GestureDetector;

import android.view.Gravity;

import android.view.MotionEvent;

import android.view.GestureDetector.OnGestureListener;

import android.widget.LinearLayout;

import android.widget.TextView;

import android.widget.Toast;

 

public class GestureActivity extends Activity implements OnGestureListener{

 

    private LinearLayout main;

    private TextView viewA;

 

    private static final int SWIPE_MIN_DISTANCE = 120;

    private static final int SWIPE_MAX_OFF_PATH = 250;

    private static final int SWIPE_THRESHOLD_VELOCITY = 200;

 

    private GestureDetector gestureScanner;

 

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        gestureScanner = new GestureDetector(this);

        main = new LinearLayout(this);

        main.setBackgroundColor(Color.GRAY);

        main.setLayoutParams(new LinearLayout.LayoutParams(320, 480));

        viewA = new TextView(this);

        viewA.setBackgroundColor(Color.WHITE);

        viewA.setTextColor(Color.BLACK);

        viewA.setTextSize(30);

        viewA.setGravity(Gravity.CENTER);

        viewA.setLayoutParams(new LinearLayout.LayoutParams(320, 80));

        main.addView(viewA);

        setContentView(main);

    }

 

    @Override

    public boolean onTouchEvent(MotionEvent me) {

        return gestureScanner.onTouchEvent(me);

    }

 

    public boolean onDown(MotionEvent e) {

        viewA.setText("-" + "DOWN" + "-");

        return true;

    }

    

    public boolean onFling(MotionEvent e1, MotionEvent e2, floatvelocityX, float velocityY) {

        try {

            if (Math.abs(e1.getY() - e2.getY()) > SWIPE_MAX_OFF_PATH)

                return false;

            

            // right to left swipe

            if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE

                    && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {

                Toast.makeText(getApplicationContext(), "Left Swipe",

                        Toast.LENGTH_SHORT).show();

            }

            // left to right swipe

            else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE

                    && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {

                Toast.makeText(getApplicationContext(), "Right Swipe",

                        Toast.LENGTH_SHORT).show();

            }

            // down to up swipe

            else if (e1.getY() - e2.getY() > SWIPE_MIN_DISTANCE

                    && Math.abs(velocityY) > SWIPE_THRESHOLD_VELOCITY) {

                Toast.makeText(getApplicationContext(), "Swipe up",

                        Toast.LENGTH_SHORT).show();

            }

            // up to down swipe

            else if (e2.getY() - e1.getY() > SWIPE_MIN_DISTANCE

                    && Math.abs(velocityY) > SWIPE_THRESHOLD_VELOCITY) {

                Toast.makeText(getApplicationContext(), "Swipe down",

                        Toast.LENGTH_SHORT).show();

            }

        } catch (Exception e) {

            

        }

 

        return true;

    }

 

    public void onLongPress(MotionEvent e) {

        Toast mToast = Toast.makeText(getApplicationContext(), "Long Press", Toast.LENGTH_SHORT);

        mToast.show();

    }

 

    public boolean onScroll(MotionEvent e1, MotionEvent e2, floatdistanceX, float distanceY) {

        viewA.setText("-" + "SCROLL" + "-");

        return true;

    }

 

    public void onShowPress(MotionEvent e) {

        viewA.setText("-" + "SHOW PRESS" + "-");

    }

 

    public boolean onSingleTapUp(MotionEvent e) {

        Toast mToast = Toast.makeText(getApplicationContext(), "Single Tap", Toast.LENGTH_SHORT);

        mToast.show();

        return true;

    }

}

   

 

   Swipe(휘두르다, 강타)라는 용어를 썼는데, 한국어로 어떻게 변경을 해야할지 몰라 그대로 두었습니다. 손가락을 이용하여 여러 개로 구성되어 있는 메인 화면을 넘기는 방법을 생각하시면 가장 간단할 것 같습니다.

 

   코드의 핵심은 GestureDetector 에 있습니다. 엑티비티에서 OnGestureListener 를 상속받으면, GestureDetector를 사용할 수 있습니다. GestureDetector를 엑티비티에 등록시켜면서 생성합니다. 그런 다음 onTouchEvent를 통해 GestureDetector 객체의 onTouchEvent로 등록시켜줍니다. 이렇게 하면 기본 세팅은 마무리가 됩니다. 그런 다음에 자신이 하고 싶은 기능을 상속받아 사용할 수 있습니다. 위에서 보시면, onDown, onLongPress, onScroll, onShowPress, onSingleTapUp, onFling라는 것을 상속받아 구현되어 있는 것을 확인할 수 있습니다. 함수명에서 보시듯 어떠한 터치 동작을 하고 있는 중인지에 따라 각각이 호출되고 있습니다. 다른 것은 직관적으로 아실 수 있을 거라 생각이 듭니다만, 복잡한 듯 보이는 onFling은 조금 생각을 해보아야 합니다. 아래의 세가지 변수를 먼저 알아봅시다. 이것만 이해되신다면 아주 간단한 구조로 되어 있다는 것을 알게 되실 겁니다.

 

 

static final int SWIPE_MIN_DISTANCE = 120;

static final int SWIPE_MAX_OFF_PATH = 250;

static final int SWIPE_THRESHOLD_VELOCITY = 200;

    

 

   터치 이벤트를 통해서 onFling이벤트로 들어옵니다. onFling으로 들어오는 인자를 보시면, 모션 이벤트가 2개가 들어오고, float 형의 velocity(속도)가 들어옵니다. 벌써 이미 느낌이 오실 겁니다. 첫 번째 인자는 터치를 시작한 그 순간의 이벤트이며, 두 번째 인자는 터치를 땐 그 시점의 이벤트 입니다. 세 번째 인자는 X축으로의 속도이며, 네 번째 인자는 Y 축으로의 인자입니다. 즉 이 네 가지의 이벤트를 가지고, 어떤 방향으로의 Swipe동작인지 구분할 수가 있습니다.

 

   네 가지 방향으로의 Swipe동작을 어떻게 구분하는가 하는 것은 거리 측정과, 속도를 통해서 이루어집니다. SWIPE_MIN_DISTANSE 인자는 Swipe를 인식하는 가장 작은 거리를 뜻합니다. 설정된 거리 이상이 되어야 Swipe동작으로 인식한다는 것이죠. SWIPE_MAX_OFF_PATH는 인식하는 최대 거리라고 보시면 됩니다. 그 이상의 거리를 Scrolling하면 제외하겠다는 의미를 가지고 있습니다. 마지막 SWIPE_THRESHOLD_VELOCITY는 속도입니다. 각 방향으로 임의의 속도 이상으로 터치동작이 이루어져야 Swipe로 인식하겠다는 것이지요.

 

  이제 위의 코드를 살펴보시면, 어떤 구조로 되어 있는지 한 눈에 들어오실 겁니다. 첫 좌표와 끝 좌표를 비교하여 거리를 비교하여 적정 수준의 거리와 속도를 가지면 Swipe로 인식하며 토스트를 띄웁니다. 이러한 방식을 이용하여 화면 전환 등의 자신이 하고 싶은 동작을 넣으시면 됩니다.

 

    

STEP 2  Xml Code

         

    Xml 코드를 제외하고 코드로 바로 레이아웃을 설정하였습니다. onCreate에 있는 추가동작을 xml로 구현하셔도 무방합니다.

             

STEP 3  AndroidManifest.xml Code

       

   안드로이드 자체에서 제공되는 서비스기 때문에  메니페스트는 손댈 필요가 없습니다.

 



 마무리 >   터치화면, 제스처 기능을 이용한 터치 인식

       

   

   일반적으로 다이나믹한 UI를 사용자에게 제공하기 위해서는 터치 기능을 이용하여 다양한 서비스를 제공해야합니다. 제스처(Gesture)를 인식하기 위해서는 이벤트 핸들러를 통해서 현재 어떠한 모션을 취하고 있는지를 확인하는 절차가 필요합니다. onGestureListener를 Activity로 상속받고, GestureDetector를 생성하여, 터치이벤트로 등록시키며, 자신이 원하는 동작을 가져올 수 있도록, 터치 이벤트의 동작 지점, 거리, 터치 속도 등을 고려하여 설계를 하시면 됩니다. 간단한 Swipe기능에 대한 예제를 알아보고, 제스처가 어떠한 방식으로 등록되고 사용되는지에 대해서 알아보았습니다. 이것 외에도 안드로이드에서 제공하는 기본 제스처, GestureLibrary 등이 있는 것으로 생각됩니다만, 아직는 그것을 쓸 필요가 없었던 관계로 추후에 기회가 된다면 다시 포스팅 하겠습니다.    

     

출처 : http://pulsebeat.tistory.com/27

xml파일내에서 

<LinearLayout

android:layout_width="240dip"

android:layout_height="320dip" />


설정해줘도 LayoutParams 사용할 경우 안되는 경우가 있었었었었었다



LayoutParams params = new LayoutParams( 240, 320 ) 해줘도 단위가 달라져서 찾아보던 중


final int width = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 240, getResources().getDisplayMetrics());

final int height = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 320, getResources().getDisplayMetrics());


LinearLayout.LayoutParams paramlinear = new LinearLayout.LayoutParams(width,height);


하니까 됨ㅋ





읽어볼만해서 펌

출처 :http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&wr_id=34284

이번 컬럼은 가상의 시나리오를 통해 실제 프로젝트 진행 중에 벌어질 수 있는 문제상황을 제시하고 동시에 이 문제를 해결해나가는 과정을 알아보도록 하겠다.

등장인물

● 허우대 대리 : 몇달전 경력사원 공채로 새로 입사한 인물로 Java를 사용해봤다는 이유만으로  난생 처음으로 안드로이드 플랫폼에 기존에 C++로 작성한 소스를 마이그레이션 하는 업무를 맞는다. 허우대는 멀쩡 하지만 성과가 없다고 일정만 차장으로 부터 허우대라는 별명을 얻었다.

● 신익한 선배 : 허우대 대리의 학교 선배로 모바일 분야에서 안해본게 없는 대단한 실력자 이지만, 일을 너무 사랑하는 나머지 인간관계는 별로 소질이 없어 보이는 시니컬한 성격의 소유자, 허우대 대리의 질문에 매번 퉁명스럽게 대답 하지만 그 속마음에는 후배를 스스로 깨우치게 하려는 나름의 철학을 가지고 있다.

● 일정만 차장 : 이번 프로젝트의 책임자로, 일에 대한 책임감이 투철하지만 상냥한 성격은 아니다. 특히 일정을 어기는 사람을 무척 싫어한다.


허우대 대리는 얼마전 일정만 차장으로 부터 1주일간의 시간동안 이전 누군가가 C++ 로 작성한 방대한 양의 xml 문서 parser를 이번 프로젝트에 재사용 할 수 있도록 안드로이드 플랫폼에 마이그레이션 하라는 지시를 받았다. 허우대 대리에게 주어진 시간은 고작 7일. 7일 동안 허우대 대리가 어떻게 이 프로젝트를 진행하는지 함께 지켜보도록 하자.

선배, 안드로이드 애플리케이션 개발은 Java로 하는거지?
생전 처음 안드로이드 플랫폼에서 개발을 시작하는 허우대 대리는 신익한 선배를 찾아가서 조언을 구하게 되었다.

“선배, 차장님께 안드로이드 플랫폼에 기존에 c++로 작성한 엄청난 양의 parser 소스를 마이그레이션 하라는 지시를 받았는데, 나 이거 다 java로 다시 만들어야 하는건가?”

선배는 한심하다는 반응으로 특유의 시니컬한 한숨을 내쉬며 이렇게 얘기한다.

“안드로이드= java의 공식은 아니야. 안드로이드 플랫폼이 어떻게 구성돼있는지 먼저 확인해 보는게 어때?”


안드로이드 플랫폼은 크게 리눅스 커널과, C/C++로 작성한 라이브러리(보통 네이티브 라이브러리라 부른다), 안드로이드 응용프로그램의 뼈대를 제공하는 애플리케이션 프레임워크와 기본 애플리케이션으로 구성돼 있다. 

리눅스 커널은 OS의 기본 기능인 태스크간의 스케줄링과 메모리, 디스크, 네트워크 등의 자원을 사용할 수 있도록 시스템 콜을 제공한다. 이는 리눅스 커널 윗부분에 초록색으로 표시된 네이티브 라이브러리를 통해 응용프로그램이 편하게 사용 할 수 있는 형태로 제공된다.

안드로이드 응용프로그램은 애플리케이션 프레임워크(응용프로그램의 구조를 제공하고 제작에 도움을 주는 Java class로 구성)을 이용하고 역시 Java언어로 작성한다.

이 Java파일을 안드로이드 SDK를 이용해 빌드하면 내부적으로 Java컴파일러와 dex converter를 거쳐 Dalvik VM(안드로이드 응용프로그램을 동작시키는 VM으로 일반적인 Java VM과는 다르다.) 위에서 동작하는 byte 코드로 변환하게 된다.

결론적으로 안드로이드 응용프로그램(Java로 작성)은 애플리케이션 프레임워크(.jar형태의 Java class.)를 이용해 VM에서 제공하는 코어 라이브러리(core library) 기능을 사용하게 되고, 이 코어 라이브러리는 리눅스 커널 위에서 동작하는 다양한 C/C++ 라이브러리(c/c++)를 호출하게 된다. 이 라이브러리는 필요에 따라 리눅스 커널의 system call을 호출하게 된다. 위 그림에서 화살표 방향을 따라 가면서 확인하자.

위 그림에서 안드로이드 런타임 계층과 라이브러리 계층을 연결하는 왼쪽 화살표로 표현된 부분에서 일반적인 함수호출 관계 외에 추가적으로 C/C++ ↔ java 호출 사이의 ‘glue’가 존재한다. 결국 이 덕분에 안드로이드 프레임워크는 자연스럽게 OS의 복잡한 기능을 리눅스 커널로부터 빌려 쓸 수 있는 것이다.

허우대 대리는 결국 C/C++ ↔ Java사이의 ‘glue’를 만들면 기존 소스를 리눅스 상에 포팅 하는 정도의 노력으로 재사용 할 수 있겠다는 결론을 내리고, 곧바로 Java ↔ C/C++ 를 호출 할 수 있는 방법을 찾아보기 시작했다. 


 
Java ↔ C/C++ 사이의 glue는 어떤것을 사용할까?
일반적으로 Java에서 C/C++ 모듈을 사용하기 위해서는 다음과 같은 방법을 사용한다.

1. Java Native Access (JNA)  

자바 코드에서 네이티브 코드와 같은 형태로 네이티브 함수를 호출하는 방식으로 기존 코드의 최소한의 수정으로 응용이 가능해, 최상의 방법으로 생각되나 현재 안드로이드 Dalvik VM에서 지원하지 않는 방법이다.

2. Java Native Interface (JNI)
Java 초창기부터 존재한 Java/Native interface 방법으로 안드로이드 Dalvik VM에서 지원하는 방법으로 실제 안드로이드 플랫폼 소스 여러 부분에서 사용되고 있다. 그러나 이 방식은 raw form을 다루는 형태로 개발해야 하기 때문에 개발 시간이 많이 소요되고 에러를 발생 시킬만한 요소가 많다.

3. Simplified Wrapper and Interface Generator (SWIG)
JNI를 좀 더 쉽게 사용 할수 있도록 해주는 도구로 C/C++ 인터페이스를 정의한 파일을 입력 받아 C/C++ JNI wrapper 코드를 생성한다. 이 방법 역시 Dalvik VM과는 호환성 문제가 존재한다.

Native library(C/C++) 로 개발할 것 인지에 대한 선택 기준

안드로이드에서는 J2SE의 대부분의 기능과, 애플리케이션 프레임워크를 통해 다양한 API 셋을 제공하고, Java를 사용하여 대부분의 응용프로그램을 제작 하는데 부족함이 없도록 배려하고 있다.

신규 개발시 애플리케이션 프레임워크를 통해 제공받을 수 없는 일부 기능(대부분 외부 기기 나 디바이스 종속적인) 부분과 성능이 아주 중요한 일부 기능을 제외하고는 Java를 사용하여 개발하는 것이 프로젝트 장기적인 관점에서는 나은 판단이라 생각된다.

C/C++로 코드로 개발은 유지보수와 디버깅 측면에서 번거롭고 불편한 부분이 있으며 JNI를 이용하여 Java 쪽으로 glue를 만드는 일이 예상하는 것만큼 간단한 일은 아니기 때문이다. 여기서 제시하는 가상의 시나리오 역시 주제를 이끌어 가기 위해 단순화시킨 예일 뿐이지 실제로 기존에 C/C++로 작성한 코드를 마이그레이션 하는 작업이 주어질 경우 얼마나 인터페이스가 단순한지, 기존코드에서 플랫폼 종속적인 부분이 얼마나 있으며 이것을 안드로이드(Linux kernel)에 포팅하기 위해서 얼마나 노력이 필요한지, 혹은 HTTP나 STL등 안드로이드 Native library단에서 충분히 제공하지 않는 기능을 사용한 부분이 있어서 재작업이나 추가 포팅 작업이 필요한지 충분히 고려하여 기존 모듈을 재활용할 것인지, Java코드로 다시 작업할 것인지 결정해야 할 것 이다.

허우대 대리는 Dalvik VM과의 호환성 문제와 보편적으로 사용하는 기술이라는 점을 감안해 JNI를 이용해 프로젝트를 진행하기로 했다.

점심시간에 허우대 대리는 자신이 JNI를 이용하여 기존 parser소스를 거의 재활용 할수있는 방법을 생각해 냈다고 신익한 선배에게 자랑을 늘어놓았다. 이때, 밥을 먹던 신익한 선배는 이런 이야기를 남긴 뒤, 아무일도 없었다는 듯 식사를 계속 했다. 신익한 선배가 남긴 말은 이렇다.

“그럼 안드로이드 시스템에 포함해서 빌드 하겠다는 얘기야? 배포는 어떻게 할건데?”

그렇다. 그림에서 보는 C/C++ 라이브러리는 안드로이드 시스템의 영역으로 허우대 대리는 배포방법에 대해 고려하지 못한 것이다. 안드로이드 에뮬레이터를 실행하고 <화면 1>에서 초록색으로 표시한 C/C++ 라이브러리 파일의 실제 위치를 확인해보자. 아래와 같이 커맨드 창에서 emulator를 실행하거나 이클립스 IDE를 통해 emulator를 실행한다.

에뮬레이터가 정상적으로 실행되면 위와 같이 에뮬레이터 화면을 확인할 수 있는데, 이때 콘솔에서는 <리스트 1>과 같이 adb 명령을 이용하여 shell을 실행한다.

<리스트 1> adb shell을 이용하여 네이티브 라이브러리 확인하기
>adb shell
adb shell 이 실행되면 아래와 같이 library path를 확인해보자
#echo $LD_LIBRARY_PATH
#/system/lib
system/lib가 library path로 잡혀있는 것을 확인하고 /system/lib directory로 이동한다.
#cd /system/lib

<리스트 1>의 내용을 <화면 3>를 통해 확인해보자.

<화면 3>에서는 안드로이드 플랫폼에서 제공하는 네이티브 라이브러리가 위치하는 것을 확인할 수 있다.

허우대 대리가 준비하는 프로젝트의 결과물은 다운로드 가능한 형태로 배포되어야 하는데 /system 디렉토리는 다운로드한 애플리케이션이 설치할 수 있는 영역이 아니기 때문에 /system 디렉토리 대신 /data 디렉토리에서 동작 할 수 있는 방법을 찾아야 한다.

안드로이드 시스템 이미지
안드로이드 시스템 이미지는 리눅스 운영체제에서 파일시스템은 단순히 자료의 저장기능 뿐 아니라 운영체제 동작에 필요한 필수 정보를 포함하고 장치 관리 등의 특별한 용도로도 사용하므로 선택항목이 아니라 필수 항목이다. 시스템 이미지는 안드로이드 프레임워크를 구동하기 위해 필수적인 항목으로 <안드로이드 SDK설치 디렉토리>/platforms/<플랫폼버전>/images/system. img 에서 확인 할 수 있다.

이 이미지파일은 실제 에뮬레이터가 구동될 때 /system directory에 마운트하게 되는데, 이내용은 adb shell을 실행시킨 후 최상위 디렉토리의 init.rc파일에서 확인 할 수 있다.

/system 디렉토리는 임베디드하여 탑재할 기본 응용프로그램을 포함해 안드로이드 프레임워크에 필요한 라이브러리 설정파일 등이 들어있는 영역으로 추가로 다운로드받은 응용프로그램과는 구별된다. 다운로드 받은 응용프로그램은 /data 디렉토리에 위치하게 된다.
 
안드로이드 SDK 설치
앞서 설명된 부분들을 실습하기 위해서는 안드로이드 SDK를 설치하고 버추얼 디바이스를 생성해야 하는데, 이 부분은 이번 컬럼의 주제와는 벗어나므로 아래 URL을 참고해 설치하기 바란다.

http://developer.android.com/sdk/index.html
 
허우대 대리, NDK와 만나다

다음날 배포문제로 고민하던 허우대 대리는 아래의 안드로이드 개발자 웹사이트를 통해 NDK에 대한 내용을 접했다. 
 
http://developer.android.com/sdk/ndk/1.6_r1/index.html

NDK는 C/C++로 작성한 코드로 리눅스 OS 기반의 ARM binary를 생성하기 위한 크로스 툴체인을 제공한다. 여기서 ‘크로스 툴체인’이란 타깃머신과는 다른 환경에서 타깃 머신용으로 바이너리를 생성할 수 있도록 제공되는 툴인데 컴파일러 링커 그리고 기타 컴파일에 필요한 유틸리티를 포함하고 있다.

<리스트 2> Application.mk 파일 

APP_PROJECT_PATH := $(call my-dir)/project
APP_MODULES      := hello-jni

일반적인 x86호환 CPU의 윈도우나 리눅스 PC기반 환경에서 ARM이나 MIPS등 임베디드 머신의 CPU에서 동작하는 바이너리를 컴파일 할 수 있도록 해준다.

그리고 아래와 같은 라이브러리를 사용할 수 있도록 header file을 제공하는데, 이 내용은 앞서 제시된 adb 쉘에서 확인한 /system/lib 디렉토리에 있는 라이브러리의 일부임을 확인할 수 있다.

(1)  libc(C library)
(2)  libm(math library)
(3)  JNI interface
(4)  libz (zip 압축 library)
(5)  liblog(Android logging library)
(6)  OpenGL ES library (3D graphic library, NDK 1.6버전부터)
 
추가적으로 안드로이드 응용프로그램 패키지 파일(.apk)에 native library를 포함할 수 있는 방법을 제공한다. 허우대 대리가 찾던 바로 그 방법이다. 그럼 이제 허우대 대리와 함께 NDK를 설치해 보도록 하자.
 
(1) http://developer.android.com/sdk/ndk/1.6_r1/index.html 에서 윈도우용 패키지를 다운로드 받는다.
(2) 적당한 위치에 (1)에서 다운로드 받은 압축파일을 푼다.
(3) www.cygwin.com에서 최신 cygwin 을 설치한다.
(4) 설치한 Cygwin bash shell을 실행하고 bash shell에서 /cygdrive/<NDK 설치 경로> 로 이동해 ‘Build/host-setup.sh’ 라고 명령을 수행한다.

이처럼 순차적으로 따라하고 나면, <화면 4>와 같이 ‘Host setup complete…’라는 문구가 나타나는데, 이럴 경우 셋업이 성공한 것이다.

이제 설치가 끝났으니 먼저 NDK에 샘플로 제공되는 hello-jni 예제를 빌드해 보자. 빌드시에는 $make APP=hello-jni라고 명령을 수행한다. 재빌드시에는 -B 옵션을 사용하고 실제 build command를 모두 확인하고 싶은 경우 V=1 옵션을 사용한다.

빌드결과로 apps/hello-jni/project/libs/areabi/libhello-jni.so가 생성되었는지 확인하자.

어떻게 NDK에서 제공하는 툴을 이용하여 예제가 빌드 되는지 알고 싶다면, apps/hello-jni/Application.mk 와 sources/ hello-jni/Android.mk 파일을 열어보자.
일반적인 make 파일과 같은 형태이며 빌드를 위한 변수를 설정하는 부분을 확인 할 수 있다. 또한 Application.mk 파일에는 응용프로그램 에서 어떤 네이티브 라이브러리 모듈을 사용할 것인지에 대해 기술해야 한다. 이 부분은 네이티브 라이브러리를 빌드하는 것과는 직접적인 관계가 없으나 이 파일을 참조해 빌드할 타깃 모듈을 결정하기 때문에 필수적으로 작성해야 한다.

<리스트 3> Android.mk 파일
… 중략…
 
LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES   := hello-jni.c
 
include $(BUILD_SHARED_LIBRARY)

Android.mk 파일에는 실제 shared library로 빌드할 c/c++ 파일 목록 과 사용하는 라이브러리 목록, compile/link option 등을 기술해야 하는데, 이때 변수 LOCAL_MODULE 에 모듈이름을 기술하게 된다. 이어 변수 LOCAL_SRC_FILES 에 빌드하려고 하는 소스 파일명을 기술하고, 파일명 사이는 스페이스로 구별한다.

마지막 줄이 shared library, 즉 .so 파일로 빌드 하겠다는 뜻이다. 이렇게 기술하면 자동으로 lib+모듈명+.so 형태의 파일을 빌드하게 된다.

추가로 특정 라이브러리를 사용하기 위해서는 아래와 같이 LOCAL_LDLIBS 변수에 내용을 기술한다. 아래는 안드로이드 로그 라이브러리를 사용하기 위한 예이다. 또한 이에 대한 추가적인 내용은 NDK document 폴더의 ‘ANDROID-MK.TXT’,’APPLICATION-MK.TXT’,’HOWTO.TXT’ 문서를 참고하기 바란다.

LOCAL_LDLIBS     :=-L$(SYSROOT)/usr/lib/ -llog

이제 네이티브 라이브러리 빌드가 끝났으니 네이티브 라이브러리를 사용하는 Java 코드를 살펴보자. <화면 6>과 같이 apps/hello-jni 프로젝트를 이클립스에서 열어보도록 하자. 왼쪽 패키지 익스플로러 창에서 마우스 오른쪽 버튼을 클릭한 뒤 Import 메뉴를 선택한다.

이어 Existing Projects into Workspace를 선택한후 app/hello-jni/ 디렉토리를 선택한다. 이를 실행하면 <화면 7>과 같이 Package Explorer에  HelloJni 프로젝트가 로딩되면 성공한것이다.

이클립스 메뉴에서 Run->Run을 선택하거나 단축키로 Ctrl+F11을 선택하면 에뮬레이터에서 Hello-jni 프로젝트의 결과를 확인할수 있다.

에뮬레이터가 실행되는데 제법 시간이 걸리므로 인내심을 가지고 기다리기 바란다. 에뮬레이터를 동작하기 위해서는 플랫폼 버전에 맞는 AVD 파일을 미리 생성해야 하는데 이 내용은 http://developer.android.com/guide/developing/eclipse-adt.html을 참고하길 바란다.

더불어 이전에 adb shell 명령을 이용한 안드로이드 파일시스템을 살펴보았는데, 이번에는 이클립스 IDE에서 DDMS툴을 이용하여 손쉽게 파일시스템을 확인해보자. 이때 이클립스 툴바 오른편에서 DDMS perspective 버튼을 선택한다. 오른편에 File Explorer 창을 이용하여 data/data/com.example.hellojni 폴더의 내용을 확인할 수 있으며, lib폴더아래에 libhello-jni.so 파일이 있는 것 또한 확인할 수 있다.

<화면 9>에서 보여지듯, 결국 안드로이드 응용프로그램은 system/lib 폴더의 라이브러리 이외에 /data/data/<자신의 package이름>/lib 아래 네이티브 라이브러리를 추가로 참고하는 것을 확인할 수 있다. 
 
네이티브 라이브러리를 사용하는 Java 코드 맛보기 

허우대 대리는 Java코드 에서 어떻게 C로 작성한 네이티브 코드를 참고하는지 궁금해졌다. 이번에는 허우대 대리와 함께 src/com/example/HelloJni.java 코드를 살펴보자

<리스트 4> HelloJni.java 파일
… 중략…
public class HelloJni extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        TextView  tv = new TextView(this);
       tv.setText( stringFromJNI() ); //-- (3) 
        setContentView(tv);
    }
 
public native String  stringFromJNI(); //-- (1) 
 
… 중략…
   static {
        System.loadLibrary("hello-jni"); //-(2) 
    }
}

<리스트 4>는 stringFromJNI 함수를 호출하여 얻은 문자열을 textView에 내용으로 넣어주는 간단한 소스로, 3가지 의미로 이용된다. 

1) native 키워드를 붙혀서 선언부만 작성한다. 네이티브 라이브러리에 구현부가 있음을 알려준다.

2) static {..} 부분은 이 class가 로딩될 때 호출되는 되는데 이때 ‘hello-jni’ 라는 이름의 네이티브 라이브러리를 로딩하라는 의미이다.

3) 네이티브 라이브러리 함수라도 보통의 Java 메소드와 같은 방식으로 사용한다.
 
이어 Source/samples/hello-jni.c 파일을 열어서 네이티브 라이브러리 구현부도 함께 살펴보도록 하자.

<리스트 5> 네이티브 라이브러리 구현부 코드 

Jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
                                            jobject thiz )
{
    return (*env)->NewStringUTF(env, "Hello from JNI !")
}

<리스트 5>를 보면 함수이름 앞에 ‘Java_com_example_ hellojni_HelloJni_’ 가 붙어 있는데, 이 부분은 JNI 함수이름 규칙에 의해 package명_클래스명이 Java에서 선언한 함수 이름 앞에 붙게 된다. 함수의 내용은 단순히 “Hello from JNI!” 라는 문자열을 Java에서 사용하는 String 타입으로 변환하여 반환하는 내용이다.

 

참고자료 
1. http://java.sun.com/javase/6/docs/technotes/guides/jni/spec/jniTOC.html
2. http://developer.android.com/sdk/ndk/1.6_r1/index.html
3. http://www.aton.com/android-native-libraries-for-java-applications/
4. http://www.swig.org/

자동 로그인 여부, 또는 설정에서 저장했던 값 등


앱이 종료되어도 보존되어야 하는 데이터를 저장할 때 


흔히 SharedPreferences를 사용한다. 


1SharedPreferences pref = mContext.getSharedPreferences(com.exam.pref,
2            Activity.MODE_PRIVATE);
3SharedPreferences.Editor editor = pref.edit();
4editor.putString("key", value);
5editor.commit();


보통 이런식으로 사용하는데 이는 키 값을 수정 할 일이 있거나


찾을 일이 있을 때 따로 키 목록을 작성해 


놓은 곳이 없다면 나중에 관리가 힘들어지는 단점이 있다. 




그래서 아예  Preference 클래스를 하나 만들어 두고 그 클래스에 


int, String, boolean을 담고 꺼내는 getter, setter 매소드와 


사용하는 키 값을 모두 선언하여 


클래스에 점만 찍으면 키, 저장, 꺼내쓰기가 가능하도록 하였다. 



01public class RbPreference {
02 
03    private final String PREF_NAME = "com.rabiaband.pref";
04 
05    public final static String PREF_INTRO_USER_AGREEMENT ="PREF_USER_AGREEMENT";
06    public final static String PREF_MAIN_VALUE = "PREF_MAIN_VALUE";
07     
08 
09    static Context mContext;
10 
11    public RbPreference(Context c) {
12        mContext = c;
13    }
14 
15    public void put(String key, String value) {
16        SharedPreferences pref = mContext.getSharedPreferences(PREF_NAME,
17                Activity.MODE_PRIVATE);
18        SharedPreferences.Editor editor = pref.edit();
19 
20        editor.putString(key, value);
21        editor.commit();
22    }
23 
24    public void put(String key, boolean value) {
25        SharedPreferences pref = mContext.getSharedPreferences(PREF_NAME,
26                Activity.MODE_PRIVATE);
27        SharedPreferences.Editor editor = pref.edit();
28 
29        editor.putBoolean(key, value);
30        editor.commit();
31    }
32 
33    public void put(String key, int value) {
34        SharedPreferences pref = mContext.getSharedPreferences(PREF_NAME,
35                Activity.MODE_PRIVATE);
36        SharedPreferences.Editor editor = pref.edit();
37 
38        editor.putInt(key, value);
39        editor.commit();
40    }
41 
42    public String getValue(String key, String dftValue) {
43        SharedPreferences pref = mContext.getSharedPreferences(PREF_NAME,
44                Activity.MODE_PRIVATE);
45 
46        try {
47            return pref.getString(key, dftValue);
48        catch (Exception e) {
49            return dftValue;
50        }
51 
52    }
53 
54    public int getValue(String key, int dftValue) {
55        SharedPreferences pref = mContext.getSharedPreferences(PREF_NAME,
56                Activity.MODE_PRIVATE);
57 
58        try {
59            return pref.getInt(key, dftValue);
60        catch (Exception e) {
61            return dftValue;
62        }
63 
64    }
65 
66    public boolean getValue(String key, boolean dftValue) {
67        SharedPreferences pref = mContext.getSharedPreferences(PREF_NAME,
68                Activity.MODE_PRIVATE);
69 
70        try {
71            return pref.getBoolean(key, dftValue);
72        catch (Exception e) {
73            return dftValue;
74        }
75    }
76}


위와 같이 상단에 각각 사용할 키를 선언하고 타입별로 같은 이름의 setter,getter 매소드를


만들어 놓으면 어디서든 위 클래스를 이용하여 해당키와 한가지 매소드로 원하는 작업 수행이 가능하다.



1RbPreference pref = new RbPreference(this);
2 
3// set
4pref.put(RbPreference.PREF_USER_AGREEMENT, true);
5 
6// get
7pref.getValue(RbPreference.PREF_USER_AGREEMENT, false);


이런식으로 사용된다. 



  원문https://source.android.com/devices/tech/input/keyboard-devices.html#keyboard-classification

  밑줄 내용참고

Keyboard Devices



Android supports a variety of keyboard devices including special function keypads (volume and power controls), compact embedded QWERTY keyboards, and fully featured PC-style external keyboards.


This document decribes physical keyboards only. Refer to the Android SDK for information about soft keyboards (Input Method Editors).

Keyboard Classification


An input device is classified as a keyboard if either of the following conditions hold:

  • The input device reports the presence of any Linux key codes used on keyboards including 0 through 0xff orKEY_OK through KEY_MAX.

  • The input device reports the presence of any Linux key codes used on joysticks and gamepads including BTN_0 through BTN_9BTN_TRIGGER through BTN_DEAD, or BTN_A through BTN_THUMBR.

Joysticks are currently classified as keyboards because joystick and gamepad buttons are reported by EV_KEY events in the same way keyboard keys are reported. Thus joysticks and gamepads also make use of key map files for configuration. Refer to the section on Joystick Devices for more information.

Once an input device has been classified as a keyboard, the system loads the input device configuration file and keyboard layout for the keyboard.

The system then tries to determine additional characteristics of the device.

  • If the input device has any keys that are mapped to KEYCODE_Q, then the device is considered to have an alphabetic keypad (as opposed to numeric). The alphabetic keypad capability is reported in the resource Configuration object as KEYBOARD_QWERTY.

    -> 키 KEYCODE_Q 는 알파벳 문자 입력장치로 인식.

  • If the input device has any keys that are mapped to KEYCODE_DPAD_UPKEYCODE_DPAD_DOWNKEYCODE_DPAD_LEFT,KEYCODE_DPAD_RIGHT, and KEYCODE_DPAD_CENTER (all must be present), then the device is considered to have a directional keypad. The directional keypad capability is reported in the resource Configuration object asNAVIGATION_DPAD.

    -> KEYCODE_DPAD_UPKEYCODE_DPAD_DOWNKEYCODE_DPAD_LEFT,KEYCODE_DPAD_RIGHT, and KEYCODE_DPAD_CENTER 는 방향 키패드 입력장치로 인식

  • If the input device has any keys that are mapped to KEYCODE_BUTTON_A or other gamepad related keys, then the device is considered to have a gamepad.

    -> KEYCODE_BUTTON_A or other gamepad related keys 면 게임패드로 인식

Keyboard Driver Requirements


  1. Keyboard drivers should only register key codes for the keys that they actually support. Registering excess key codes may confuse the device classification algorithm or cause the system to incorrectly detect the supported keyboard capabilities of the device.

    -> 실제로 사용하기 위해선 key의 key code들을 등록해야만 한다. Key code들을 너무 많이 등록하면 제대로 인식 못하거나 Device에 혼란을 줄 수 있으므로 주의

  2. Keyboard drivers should use EV_KEY to report key presses, using a value of 0 to indicate that a key is released, a value of 1 to indicate that a key is pressed, and a value greater than or equal to 2 to indicate that the key is being repeated automatically.

    -> key press를 위해선 EV_KEY 를 사용해야한다. '0'은 key release, '1'은 key press, '2'이상은 반복적으로 인식한다.

  3. Android performs its own keyboard repeating. Auto-repeat functionality should be disabled in the driver.

    -> 안드로이드에선 자체적으로 keyboard repeating을 수행하므로, 드라이버에서 auto_repeat 은 중지해야한다. ( ? : auto repeat이 뭘 의미하지..??)

  4. Keyboard drivers may optionally indicate the HID usage or low-level scan code by sending EV_MSC with MSC_SCANCODE and a value indicating the usage or scan code when the key is pressed. This information is not currently used by Android.

  5. Keyboard drivers should support setting LED states when EV_LED is written to the device. The hid-input driver handles this automatically. At the time of this writing, Android uses LED_CAPSLOCKLED_SCROLLLOCK, andLED_NUMLOCK. These LEDs only need to be supported when the keyboard actually has the associated indicator lights.

  6. Keyboard drivers for embedded keypads (for example, using a GPIO matrix) should make sure to send EV_KEYevents with a value of 0 for any keys that are still pressed when the device is going to sleep. Otherwise keys might get stuck down and will auto-repeat forever.

Keyboard Operation (안드로이드에서 Keyboard 동작 요약)


  1. The EventHub reads raw events from the evdev driver and maps Linux key codes (sometimes referred to as scan codes) into Android key codes using the keyboard's key layout map.

    -> EventHub 는 evdev 드라이버에서 이벤트들을 읽고,  리눅스 Key code(또는 Scan Code)들을 'Keyboard Key layout map'을 사용해서 안드로이드 key로 매핑한다.

  2. The InputReader consumes the raw events and updates the meta key state. For example, if the left shift key is pressed or released, the reader will set or reset the META_SHIFT_LEFT_ON and META_SHIFT_ON bits accordingly.

  3. The InputReader notifies the InputDispatcher about the key event.

  4. The InputDispatcher asks the WindowManagerPolicy what to do with the key event by calling WindowManagerPolicy.interceptKeyBeforeQueueing. This method is part of a critical path that is responsible for waking the device when certain keys are pressed. The EventHub effectively holds a wake lock along this critical path to ensure that it will run to completion.

  5. If an InputFilter is currently in use, the InputDispatcher gives it a chance to consume or transform the key. The InputFilter may be used to implement low-level system-wide accessibility policies.

  6. The InputDispatcher enqueues the key for processing on the dispatch thread.

  7. When the InputDispatcher dequeues the key, it gives the WindowManagerPolicy a second chance to intercept the key event by calling WindowManagerPolicy.interceptKeyBeforeDispatching. This method handles system shortcuts and other functions.

  8. The InputDispatcher then identifies the key event target (the focused window) and waits for them to become ready. Then, the InputDispatcher delivers the key event to the application.

  9. Inside the application, the key event propagates down the view hierarchy to the focused view for pre-IME key dispatch.

  10. If the key event is not handled in the pre-IME dispatch and an IME is in use, the key event is delivered to the IME.

  11. If the key event was not consumed by the IME, then the key event propagates down the view hierarchy to the focused view for standard key dispatch.

  12. The application reports back to the InputDispatcher as to whether the key event was consumed. If the event was not consumed, the InputDispatcher calls WindowManagerPolicy.dispatchUnhandledKey to apply "fallback" behavior. Depending on the fallback action, the key event dispatch cycle may be restarted using a different key code. For example, if an application does not handle KEYCODE_ESCAPE, the system may redispatch the key event as KEYCODE_BACK instead.

Keyboard Configuration


Keyboard behavior is determined by the keyboard's key layout, key character map and input device configuration.

Refer to the following sections for more details about the files that participate in keyboard configuration:

Properties

The following input device configuration properties are used for keyboards.

keyboard.layout

Definition: keyboard.layout = <name>

Specifies the name of the key layout file associated with the input device, excluding the .kl extension. If this file is not found, the input system will use the default key layout instead.

Spaces in the name are converted to underscores during lookup.

Refer to the key layout file documentation for more details.

keyboard.characterMap

Definition: keyboard.characterMap = <name>

Specifies the name of the key character map file associated with the input device, excluding the .kcm extension. If this file is not found, the input system will use the default key character map instead.

Spaces in the name are converted to underscores during lookup.

Refer to the key character map file documentation for more details.

keyboard.orientationAware

Definition: keyboard.orientationAware = 0 | 1

Specifies whether the keyboard should react to display orientation changes.

  • If the value is 1, the directional keypad keys are rotated when the associated display orientation changes.

  • If the value is 0, the keyboard is immune to display orientation changes.

The default value is 0.

Orientation awareness is used to support rotation of directional keypad keys, such as on the Motorola Droid. For example, when the device is rotated clockwise 90 degrees from its natural orientation, KEYCODE_DPAD_UP is remapped to produce KEYCODE_DPAD_RIGHT since the 'up' key ends up pointing 'right' when the device is held in that orientation.

keyboard.builtIn

Definition: keyboard.builtIn = 0 | 1

Specifies whether the keyboard is the built-in (physically attached) keyboard.

The default value is 1 if the device name ends with -keypad0 otherwise.

The built-in keyboard is always assigned a device id of 0. Other keyboards that are not built-in are assigned unique non-zero device ids.

Using an id of 0 for the built-in keyboard is important for maintaining compatibility with theKeyCharacterMap.BUILT_IN_KEYBOARD field, which specifies the id of the built-in keyboard and has a value of 0. This field has been deprecated in the API but older applications might still be using it.

A special-function keyboard (one whose key character map specifies a type of SPECIAL_FUNCTION) will never be registered as the built-in keyboard, regardless of the setting of this property. This is because a special-function keyboard is by definition not intended to be used for general purpose typing.

Example Configurations

# This is an example input device configuration file for a built-in
# keyboard that has a DPad.

# The keyboard is internal because it is part of the device.
device
.internal = 1

# The keyboard is the default built-in keyboard so it should be assigned
# an id of 0.
keyboard
.builtIn = 1

# The keyboard includes a DPad which is mounted on the device.  As the device
# is rotated the orientation of the DPad rotates along with it, so the DPad must
# be aware of the display orientation.  This ensures that pressing 'up' on the
# DPad always means 'up' from the perspective of the user, even when the entire
# device has been rotated.
keyboard
.orientationAware = 1

Compatibility Notes

Prior to Honeycomb, the keyboard input mapper did not use any configuration properties. All keyboards were assumed to be physically attached and orientation aware. The default key layout and key character map was named qwerty instead of Generic. The key character map format was also very different and the framework did not support PC-style full keyboards or external keyboards.

When upgrading devices to Honeycomb, make sure to create or update the necessary configuration and key map files.

HID Usages, Linux Key Codes and Android Key Codes


The system refers to keys using several different identifiers, depending on the layer of abstraction.

For HID devices, each key has an associated HID usage. The Linux hid-input driver and related vendor and device-specific HID drivers are responsible for parsing HID reports and mapping HID usages to Linux key codes.

As Android reads EV_KEY events from the Linux kernel, it translates each Linux key code into its corresponding Android key code according to the key layout file of the device.
-> 안드로이드는 리눅스 커널에서 EV_KEY event 를 읽고, Device의 key layout file을 이용해 각 리눅스 Key code를 안드로이드 Key code로 번역한다.

When the key event is dispatched to an application, the android.view.KeyEvent instance reports the Linux key code as the value of getScanCode() and the Android key code as the value of getKeyCode(). For the purposes of the framework, only the value of getKeyCode() is important.

Note that the HID usage information is not used by Android itself or passed to applications.

Code Tables


The following tables show how HID usages, Linux key codes and Android key codes are related to one another.
-> HID가 어떻게 리눅스 키코드와 안드로이드 키코드가 연결되어 사용되는지 보여줌.

The LKC column specifies the Linux key code in hexadecimal.

The AKC column specifies the Android key code in hexadecimal.

The Notes column refers to notes that are posted after the table.

The Version column specifies the first version of the Android platform to have included this key in its default key map. Multiple rows are shown in cases where the default key map has changed between versions. The oldest version indicated is 1.6.

  • In Gingerbread (2.3) and earlier releases, the default key map was qwerty.kl. This key map was only intended for use with the Android Emulator and was not intended to be used to support arbitrary external keyboards. Nevertheless, a few OEMs added Bluetooth keyboard support to the platform and relied on qwerty.kl to provide the necessary keyboard mappings. Consequently these older mappings may be of interest to OEMs who are building peripherals for these particular devices. Note that the mappings are substantially different from the current ones, particularly with respect to the treatment of the HOME key. It is recommended that all new peripherals be developed according to the Honeycomb or more recent key maps (ie. standard HID).

  • As of Honeycomb (3.0), the default key map is Generic.kl. This key map was designed to support full PC style keyboards. Most functionality of standard HID keyboards should just work out of the box.

The key code mapping may vary across versions of the Linux kernel and Android. When changes are known to have occurred in the Android default key maps, they are indicated in the version column.

Device-specific HID drivers and key maps may apply different mappings than are indicated here.


팝업으로 띄우는 액티비티 클래스 내에 다음 코드를 추가하면 끝.


 protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {

   

    super.onApplyThemeResource(theme, resid, first);

   

    theme.applyStyle(style.Theme_Panel, true);

}  

     


또는



 getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));



안드로이드 TextView는 글씨를 다루는 뷰인 만큼 폰트를 교체 할 수 있습니다 .

안드로이드에서는 네가지의 기본 폰트를 제공하고, 추가로 폰트를 추가하면 해당 폰트를 

사용 할 수 있는데요, 먼저 기본폰트는 narmal , sans, serif, monospace 네가지로 


이런 폰트 모양입니다. 폰트 적용은 간단한데요 xml의 TextView에서 

android:typeface= "normal" 이런식으로 추가해 주시면 됩니다. 


이제 다른 폰트를 다운받아 적용시키는 방법인데요 , 



위에 Frutiger55Roman 이라는 이름의 폰트가 있습니다 , 이걸 프로젝트의 assets 폴더에 넣어주시고



소스 상에서 추가를 해주여야 합니다. 간단한 테스트 이므로 저는 onCreate에 추가를 하였습니다.



두번째 줄이 핵심입니다, 뒤에 확장자까지 써주셔야하는거 잊지마시구요 . 저렇게 등록을 해주면 


이런 추가된 폰트로 글씨가 나타나게 됩니다 ! 


+ Recent posts