리소스로 된 이미지를 Bitmap 으로 변환시킨후에 SD 카드에 JPG 파일로
저장시키는 루틴


  int resID = R.drawable.bgImage

  String filePath = Environment.getExternalStorageDirectory().toString();
  Bitmap bgImage = BitmapFactory.decodeResource(getResources(), resID);
  
  File file = null;
      
  OutputStream outStream = null;
  file = new File(filePath);
   
  outStream = new FileOutputStream(file);
   
  bgImage.compress(Bitmap.CompressFormat.JPEG, 50, outStream);
   
  outStream.flush();
  outStream.close();

  MediaStore.Images.Media.insertImage(getContentResolver(), file.getAbsolutePath(), file.getName(), file.getName());

개발환경 : JDK 1.5, eclipse-galileo, android googleAPI 7, window XP


모든 프로그램에서 이미지 관리의 기본은 비트맵이다. 안드로이드에서도 마찬가지로

이미지 관리와 표현을 위해서는 비트맵을 익히는게 가장 기본이다. 그 비트맵 관련

내용들을 소개한다.

 

안드로이드에서 비트맵 관련 클래스는 android.graphics.Bitmap 이다. 그래픽 관련

클래스들은 android.graphics 패키지에 있으며 여기에 포함된 것이다.

그리고 객체 Factory 관리를 위한 BitmapFactory 클래스가 있다. BitmapFactory

여러가지 이미지 포맷을 decode 해서 bitmap 으로 변환하는 함수들로 되어있는데

그 이름들은 decodeXXX 로 되어있어서 쉽게 원하는 기능의 함수를 찾을수 있을

것이다.

 

(1) BitmapFactory 에서 주로 사용하고 있는 함수와 옵션에 대한 설명


BitmapFactory.decodeByteArray()
: Camera.PictureCallBack  으로부터 받은 Jpeg 사진

데이터를 가지고 Bitmap  으로 만들어 줄 때 많이 사용한다.

Camera.PictureCallback 에서 들어오는 데이터가 byte[] 배열로 들어오기 때문이다.

 

BitmapFactory.decodeFile() : 로컬에 존재하는 파일을 그대로 읽어올 때 쓴다. 파일경로를

파라미터로 넘겨주면 FileInputStream 을 만들어서 decodeStream 을 한다.
1 Bitmap orgImage = BitmapFactory.decodeFile(“/sdcard/test.jpg”);

BitmapFactory.decodeResource() : Resource 폴더에 저장된 그림파일을 Bitmap 으로

만들어 리턴해준다
1 Bitmap orgImage =
2      BitmapFactory.decodeResource(getResources(), R.drawable.test02);

BitmapFactory.decodeStream() : InputStream 으로부터 Bitmap 을 만들어 준다.

 

BitmapFactory.Options : BitmapFactory 가 사용하는 옵션클래스이다. Options 객체를 생성하고

설정하고자 하는 옵션을 넣은후 BitmapFactory 의 함수 실행시 파라미터로 넘기면된다.

inSampleSize : decode 시 얼마나 줄일지 설정하는 옵션인데 1보다 작을때는 1이 된다.

1보다 큰값일 때 1/N 만큼 이미지를 줄여서 decoding 하게 된다. 보통 2의 배수로 설정한다.
1 BitmapFactory.Options options = new BitmapFactory.Options();
2 options.inSampleSize = 4;
3 Bitmap orgImage = BitmapFactory.decodeFile(“/sdcard/test.jpg”, options);


(2) Bitmap 과 BitmapFactory 을 사용한 여러가지 예제 


BitmapFactory 로 이미지를 읽어온뒤 Bitmap.createScaledBitmap() 사용해서 크기를 재조정

할수 있다. 하지만 예를 들어 크기를 일정하게 2, 4 배등으로 줄일거면 굳이 createScaledBitmap

을 사용하지 않고 위에서 설명한 BitmapFactory.Options inSampleSize 를 사용하면 된다.

아래는 SD 카드에서 이미지를 불러와 Bitmap 을 원하는 크기 만큼 줄인 예제이다.

1 Bitmap orgImage = BitmapFactory.decodeFile(“/sdcard/test.jpg”);
2 Bitmap resize = Bitmap.createScaledBitmap(orgImage, 300, 400, true);

다음은 BitmapFactory.Options 사용해서 이미지를 4배로 줄인것인데 createScaledBitmap

사용해서 용량을 줄인 이미지에 다시 입력한 크기만큼 가로, 세로 크기를 줄인 것이 된다.

1 BitmapFactory.Options options = new BitmapFactory.Options();
2 options.inSampleSize = 4;
3 Bitmap orgImage = BitmapFactory.decodeFile(“/sdcard/test.jpg”, options);
4 Bitmap resize = Bitmap.createScaledBitmap(orgImage, 300, 400, true);

아래 방법은 SD카드 가 아닌 device 의 메모리에 이미지를 저장, 로드 , 삭제 하는 방식이다.

SD카드에 하려면 경로명을 정확히 넣어주고 메니페스트 파일에

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

이내용을 추가함을 잊지말자

 

 

1. Bitmap 저장

 

     imgview = (ImageView)findViewById(R.id.imageView1);       
     Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.comp1_16);     
     imgview.setImageBitmap(bm);     
     

try{
     
     File file = new File("test.png");          
     FileOutputStream fos = openFileOutput("test.png" , 0);          
     bm.compress(CompressFormat.PNG, 100 , fos);          
     fos.flush();     
     fos.close();
     
     Toast.makeText(this, "file ok", Toast.LENGTH_SHORT).show();     
     }catch(Exception e) { Toast.makeText(this, "file error", Toast.LENGTH_SHORT).show();}
     

 

2. 저장된 Bitmap 불러오기

 특이한것은 저장할때는 파일 이름만 있어도 알아서 app 의 data 폴더에 저장되지만 불러올때는

 전체 패스를 다 적어줘야한다.

try{    
    imgview = (ImageView)findViewById(R.id.imageView1);       
       String imgpath = "data/data/com.test.SDCard_Ani/files/test.png";
       Bitmap bm = BitmapFactory.decodeFile(imgpath);       
       imgview.setImageBitmap(bm);
       Toast.makeText(getApplicationContext(), "load ok", Toast.LENGTH_SHORT).show();
    }catch(Exception e){Toast.makeText(getApplicationContext(), "load error", Toast.LENGTH_SHORT).show();}

    

 

3. 저장된 Bitmap 파일 삭제하기

try{
    File file = new File("data/data/com.test.SDCard_Ani/files/");
    File[] flist = file.listFiles();
    Toast.makeText(getApplicationContext(), "imgcnt = " + flist.length, Toast.LENGTH_SHORT).show();
    for(int i = 0 ; i < flist.length ; i++)
    {
     String fname = flist[i].getName();
     if(fname.equals("test.png"))
     {
      flist[i].delete();
     }
    }
    }catch(Exception e){Toast.makeText(getApplicationContext(), "파일 삭제 실패 ", Toast.LENGTH_SHORT).show();}


프로그래밍 디자인 패턴중 싱글턴패턴이 이러한 방법으로 쓰는 것인데
해당 클래스의 인스턴스를 여기저기 다른 클래스에서 새롭게 인스턴스를
만들지 못하고 하나의 인스턴스만 가지고 공유해서 쓰기 위해서
이러한 방법을 씁니다.
싱글턴 클래스로 쓰는 것들은 보통 생성자를 public 이 아닌 private 으로 선언해서
다른 클래스에서 이 클래스의 인스턴스를 new 를 통해서 생성하지 못하게 하고
항상 getInstance() 함수를 통해서 인스턴스를 갖도록 제한을 하죠.
싱글턴 패턴은 유용하게 많이 쓰이는 것이므로 알아두면 편리합니다.
네이버 검색에서 "프로그래밍 패턴 싱글턴" 이렇게 치면 자료가 많이 나옵니다.

 

 

2.2 객체와 인스턴스


클래스로부터 객체를 만드는 과정을 클래스의 인스턴스화(instantiate)라고 하며, 어떤 클래스로부터 만들어진 객체를 그 클래스의 인스턴스(instance)라고 한다.

예를 들면, Tv클래스로부터 만들어진 객체를 Tv클래스의 인스턴스라고 한다. 결국 인스턴스는 객체와 같은 의미이지만, 객체는 모든 인스턴스를 대표하는 포괄적인 의미를 갖고 있으며, 인스턴스는 어떤 클래스로부터 만들어진 것인지를 강조하는 보다 구체적인 의미를 갖고 있다.
예를 들면, '책상은 인스턴스다.'라고 하기 보다는 '책상은 객체다.'라는 쪽이, '책상은 책상 클래스의 객체이다.'라고 하기 보다는 '책상은 책상 클래스의 인스턴스다.'라고 하는 것이 더 자연스럽다.

인스턴스와 객체는 같은 의미이므로 두 용어의 사용을 엄격히 구분지을 필요는 없지만, 위의 예에서 본 것과 같이 문맥에 따라 구별하여 사용하는 것이 좋다.


안드로이드 개발을 하다모면 액티비티간 데이터를 주고받아야 하는 경우가 있습니다.
이런 경우 startActivityForResult() 와 onActivityResult()를 이용해 처리할 수 있습니다.

순서는 아래와  같습니다.
1. 액티비티 호출시 리퀘스트 코드를 추가한다.
2. 호출된 액티비티는  액션을 한 후 결과를 세팅하고 종료한다.
3. 기존 액티비티는 결과를 받아 처리한다.






 1. 액티비티 호출시 리퀘스트 코드를 추가한다
 
기본적으로는 액티비티를 호출할 때, startActivity() 메소드를 사용하지만,
이 경우에는 startActivityForResult() 메소드를 사용해야 합니다.

액티비티 A가 있다고 해봅니다.
액티비티 A는 intent에 데이터를 넣고, 액티비티 B를 호출할 것입니다.
이때, 액티비티 A는 자신이  호출한 액티비티 B에서 처리된 결과를 데이터로 받고 싶어 합니다.


처음에는 아래와 같이 Activity A 만 존재하고 있습니다.





이제 이 Activity A는 B를 호출합니다. 호출은 아래와 같이 이루어 집니다.
intent에 호출할 Activity B에 대한 정보와,  여러 데이터들을 담습니다.
 





Activity B가 호출 되면 아래와 같이 될것입니다.





1
startActivityForResult(intent, requestCode);





 2. 호출된 액티비티는 액션을 한 후 결과를 세팅하고 종료한다
 
호출된 Activity B는 자신을 호출한 Activity A로 부터 받은 데이터를 가지고 무엇인가 자신의 일을 수행할 것입니다.
그리고 모든 수행을 마쳤다면, 아래와 같이 합니다.

결과로 돌려줄 데이터가 있다면, setResult(RESULT_OK, intent) 를 실행해 결과를 함께 넘겨주고,
결과는 주지 않고, 뭔가 수행했다는 신호정도만을 돌려줄땐, setResult(RESULT_OK) 를 실행합니다.

- 여기서는  RESULT_OK 를 넣었지만, 실제로 RESULT_CANCELED 와  RESULT_FIRST_USER 라는 플래그도 있습니다.
- 중요한건 프로그래머가 식별할 수 있는 플래그를 사용하면 되는 것입니다.

그리고 모든 처리가 끝났다면 Activity B는 자신을 finish() 하면 됩니다.
 



1
2
3
4
Intent intent = new Intent();
intent.putExtra("resultSetting", "결과를 처리하였습니다.");
this.setResult(RESULT_OK, intent);
finish();






 3. 기존 액티비티는 결과를 받아 처리한다
 
Activity B는 자신을 종료시키면서 Activity A를 다시 화면위로 올려놓습니다.
그럼 Activity A에서는 자동으로 onActivityResult() 메소드가 호출됩니다. 

프로그래머는 Activity A에서 onActivityResult() 메소드를 override해서 자신이 되돌려 받은 결과로 처리하고 싶은 것을 구현하면 됩니다.







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* (non-Javadoc)
 * @see android.app.Activity#onActivityResult(int, int, android.content.Intent)
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
    super.onActivityResult(requestCode, resultCode, data);
      
    // 수행을 제대로 한 경우
    if(resultCode == RESULT_OK && data != null)
    {
        String result = data.getStringExtra("resultSetting");           
    }
    // 수행을 제대로 하지 못한 경우
    else if(resultCode == RESULT_CANCELED)
    {
                      
    }
}








Handler 는 내부적으로 쓰레드를 생성해 메시지큐의 데이터의 입출력을 처리하는 녀석이다.

메시지큐에 메시지를 전송하거나 해당 메시지를 얻어올수 있다.

메시지를 전송할때 지연시간을 두어 전송토록 하는 메쏘드도 포함되어 있어

쓰레드간 메시지 스케줄링시 이래저래 많이 사용된다.

 

1. 메시지를 수신할 녀석을 설정한다.

public Handler handler = new Handler() {

@Override

public void handleMessage(Message msg)

{

 

}

};

 

별거읍넹.. 이렇게 설정하면 해당 쓰레드의 메시지 큐에 메시지가

존재하는 경우 handleMessage() 메쏘드가 호출된다.

 

switch( msg.what ) {

 

}

 

과 같이 handleMessage() 메쏘드에 메시지 처리루틴을 넣어주면 된다.

 

2. 메시지 만들기

쓰레드마다 메시지큐가 생성되므로, 다른쓰레드에서 위의 핸들러에 메시지를

보내기 위해서는 다른쓰레드에서 위의 handler 객체에 접근가능해야 한다.

 

싱글턴을 사용하던, static 멤버를 사용해 만들던, 위 객체를 접근가능하게 한뒤

메시지를 하나 만든다.

new Message로 만들어도 되나, 매번 객체가 생성되는 단점이 있으므로

핸들러에게 메시지를 하나 달라고 요청하는 방식을 사용한다.

그리고, 해당 메시지를 원하는 값을 설정한다.

 

Message msg = handler.obtainMessage();

msg.what = 1234;

msg.arg1 = 1111;

msg.arg2 = 2222;

msg.object = Object;

 

위를 하나로 처리할 수도 있는데,

Message msg = Message.obtain( handler, 1234, 111, 222 );

 

요렇게 해도 된다.

 

3. 메시지 전송

handler.sendMessage(msg);

 

메시지 전송은 여러 메쏘드 들이 존재한다.


프로그래밍을 하다보면, 유일하게 인스턴스가 하나이였으면 좋겠고,

전역적인 접근이 필요 한 경우가 있습니다.

예를 들면 사운드 엔진이나, 텍스쳐메니져 같은경우가 그렇습니다.

이런경우 싱글톤으로 만들어서 사용하게 됩니다.


public class AriClassName {

       static AriClassName _shared = null;

       public static AriClassName shared()
     {
               synchronized(AriClassName.class)
               {
                        if(_shared == null)
                                 _shared = new AriClassName();
               }
               return _shared;
     }

    
public void test()
      {
      }

}



사용방법은 이렇습니다.

AriClassName.shared().test();

Gallery 어플에 있는 이미지를 Crop 하여 가져오는 방법으로
intent 간에 data 가 100kb 가 넘어갈경우 에러가 발생하므로
아래와같이 파일로 저장후 읽어들여 가져옴

private static final String TEMP_PHOTO_FILE = "temporary_holder.jpg";   
 
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 
photoPickerIntent
.setType("image/*"); 
photoPickerIntent
.putExtra("crop", "true"); 
photoPickerIntent
.putExtra(MediaStore.EXTRA_OUTPUT, getTempUri()); 
photoPickerIntent
.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); 
startActivityForResult
(photoPickerIntent, REQ_CODE_PICK_IMAGE); 
 
 
   
private Uri getTempUri() { 
   
return Uri.fromFile(getTempFile()); 
   
} 
 
   
private File getTempFile() { 
   
if (isSDCARDMounted()) { 
 
   
File f = new File(Environment.getExternalStorageDirectory(),TEMP_PHOTO_FILE); 
   
try { 
    f
.createNewFile(); 
   
} catch (IOException e) { 
 
   
} 
   
return f; 
   
} else { 
   
return null; 
   
} 
   
} 
 
   
private boolean isSDCARDMounted(){ 
   
String status = Environment.getExternalStorageState(); 
   
if (status.equals(Environment.MEDIA_MOUNTED)) 
   
return true; 
   
return false; 
   
} 
 
 
 
 
protected void onActivityResult(int requestCode, int resultCode, 
       
Intent imageReturnedIntent) { 
   
super.onActivityResult(requestCode, resultCode, imageReturnedIntent); 
 
   
switch (requestCode) { 
   
case REQ_CODE_PICK_IMAGE: 
       
if (resultCode == RESULT_OK) {   
         
if (imageReturnedIntent!=null){ 
 
 
 
               
File tempFile = getTempFile(); 
 
             
String filePath= Environment.getExternalStorageDirectory() 
           
+ "/temporary_holder.jpg"; 
             
System.out.println("path "+filePath); 
 
 
   
Bitmap selectedImage =  BitmapFactory.decodeFile(filePath); 
    _image
= (ImageView) findViewById(R.id.image); 
    _image
.setImageBitmap(selectedImage ); 
 
  } 
 } 
}

보통 BitmapFactory의 decode 함수들은 메모리 Leak이 존재한다고 알려져 있습니다.

(2.1에서 수정이 되었는지 아직도 그대로인지는 잘 모르겠습니다.)

 실제로 안그럴지 몰라도,

decode를 하면 할 수록 메모리 Leak의 위험부담은 더 커지기 마련이죠.

제가 처음에 Drawable을 Bitmap으로 바꿀 때 BitmapFactory를 사용 했었습니다.

 

정확히 말하면 Drawable을 Bitmap으로 바꾼 것이 아니라

RawResourceInputStream으로 얻어와서 BitmapFactory로 decode한 것이었죠.

Bitmap bitmap;
InputStream stream;
stream = context.getResources().openRawResource(resource);
try {
    bitmap = BitmapFactory.decodeStream(stream);

finally {
    try { stream.close(); } 
    catch(IOException e) {}
}

하지만 위의 코드는 계속 BitmapFactory를 호출 하기 때문에

잠재적인 위험을 가지고 있습니다.

 

그렇다면 리소스로 부터 Bitmap을 얻어내고 싶다면 어떻게 해야 할까요?

 

 

[Googling...]

 

일단 포스팅 하기전에 구글링을 좀 해봤습니다.

상위 몇개의 검색 결과를 살펴보니...

 

구글링으로 살펴본 결과들은 대부분 아래와 같이 되어있었습니다.

Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, w, h);
drawable.draw(canvas);

1. 빈 Bitmap을 만들고

2. Canvas를 연결 한 뒤

3. Drawable의 draw메소드를 통해 Bitmap과 연결된 Canvas에 Drawable의 내용을 그립니다.

 

물론 이 방법이 틀린 것은 아닙니다.

하지만 만들어 줘야 하는것이,

 

1. Drawable 크기만한 빈 Bitmap

2. Bitmap에 연결할 Canvas

3. 크기를 가지는 Width, Height 변수

 

Drawable 객체를 제외하고 두개의 객체두개의 변수를 더 만들어야 합니다.

중간에 setBounds() 메소드도 호출 해야 겠죠.

 

근데 여기서 한가지 잘 생각해 봐야 할 것이 있습니다.

View안에서는 Drawable을 잘 사용해서 이미지를 표시해 주고 있다는 점입니다.

Layout XML 파일 안에서 ImageView의 이미지를 지정해 줄 때

Drawable을 잘~ 사용해왔다는 것이 하나의 예가 될 수 있겠네요.

 

그렇다면 Drawable이 당연히 Bitmap을 가지고 있어야 하지 않을까요?

 

 

[BitmapDrawable]

 

질문에 대한 답은 바로 BitmapDrawable에 담겨있습니다.

아래의 코드를 보시죠!

BitmapDrawable drawable =
            (BitmapDrawable) getResources().getDrawable(R.drawable.icon);
Bitmap bitmap = drawable.getBitmap();

읭...? 이게 끝입니다.

BitmapDrawable을 사용하면 Bitmap을 손쉽게 얻어 올 수 있습니다.

위의 길고 긴 코드가 단 두 줄로 줄어 들었습니다.

 

따로 Bitmap을 만들지 않아도 됩니다.

그냥 Drawable안에 있는 Bitmap을 사용하기만 하면 됩니다.

 

 

[주의사항!]

 

BitmapDrawable을 사용하면 손쉽게 Bitmap을 얻을 수는 있지만,

Drawable이 꼭 BitmapDrawable만 존재 하는 것은 아닙니다.

대표적인 예로 ShapeDrawable이 있을 수 있겠네요.

ShapeDrawable을 사용하면 원하는 도형(Shape 객체)을 Drawable로 사용 할 수 있습니다.

 

하지만 getBitmap() 메소드가 없기 때문에

ShapeDrawable로 부터 Bitmap을 얻어 올 수는 없습니다.

굳이 도형을 Bitmap으로 바꾸고 싶다면 위에서 봤던 빈 Bitmap과 Canvas를 만들어서

draw() 메소드를 통해 그리는 방법 밖에는 없습니다.

 

아마도 대부분의 경우 drawable 디렉토리에 있는 이미지들을 Bitmap으로 사용하려고 하지,

Shape을 Bitmap으로 사용하려고 하지는 않을 거라 생각합니다.

네네... 그럴겁니다...

 

 

[BitmapDrawable Bitmap의 특징]

 

BitmapDrawable에서 얻어온 Bitmap 객체는 보통녀석이 아닙니다.

특징을 한번 살펴 봅시다.

 

1. 우선, Bitmap을 얻어 올 때는 final 입니다.

 

레퍼런스에 보면 final로 선언되어 있습니다.

즉, 변경하지 않겠다는 의지를 표현 한 것이죠.

사실 리턴에 final을 붙여봤자 대입되는 변수와는 아무 상관이 없습니다... 네... 넘어가죠.

 

2. Immutable 입니다.

 

좀 더 강력한 녀석이 나왔습니다. Immutable, 즉, 변경 불가입니다.

Canvas canvas = new Canvas(bitmap);

만약 위와 같은 시도를 한다면, 아래와 같은 Exception이 발생 할겁니다.

Caused by: java.lang.IllegalStateException: Immutable bitmap passed to Canvas constructor

3. 절대 recycle() 메소드를 호출 해서는 안됩니다!

 

Bitmap을 얻어와서 그릴거 다 그렸다고 무의식적으로 recycle() 메소드를 호출 했다...

그럼 아래와 같은 메세지를 볼 수 있습니다.

java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@43774438

위의 상황은 ImageView 같은데서 사용하고 있던 Drawable의 Bitmap 객체를 얻어와서

그 Bitmap 객체에 recycle() 메소드를 호출 한 상황입니다.

한마디로 Bitmap 객체를 공유해서 사용한다고 볼 수 있겠죠.

 

 

[One more Tip - Bitmap copy]

 

그렇다면 BitmapDrawable로 부터 얻어낸 Bitmap 객체를

마음대로 바꾸고, 쓰고, 버리고 싶다면 어떻게 해야 할까요?

 

그냥 copy() 하면 됩니다.

Bitmap bitmap = drawable.getBitmap().copy(Config.ARGB_8888, true);

Mutable로 복사하면 마음대로 변경해서 사용 할 수 있습니다!

 

 

[Outro]

 

이번에는 BitmapDrawable과 Bitmap에 대해서 살짝 살펴보았습니다.

 

앞에서는 BitmapDrawable을 Bitmap으로 바꾸는 이야기만 했었지만,

반대로 Bimap을 Drawable로 바꾸고 싶다면

BitmapDrawable의 생성자를 사용하면 간단히 Drawable로 만들 수 있습니다.

 

물론 구글링해서 찾은 방법이 틀린 방법은 아닙니다.

이미지와 관련이 없는 Drawable을 다루고자 할 때는

번거롭게도 draw() 메소드를 이용 해야 하는것이 맞지만,

이미지와 관련된 Drawable을 다루고자 할 때는 분명 BitmapDrawable을 사용하는 것이 더 편합니다.


일반적인 경우, 안드로이드 용 어플리케이션을 작성하게 되면 여러가지 Activity 들을 생성하게 됩니다. 이때, 가장 골치가 아픈 일 중 하나는 바로 Activity 와 Activity 간의 Flow 를 설계하고 이를 적절하게 구현하는 일입니다. (특히 안드로이드를 사용해보지도 않은 UX 팀과 함께 일하게 되는 경우라면 더욱 그러합니다...)

 기본적으로 안드로이드 플랫폼 상에서 Activity 는 또 다른 Activity 를 시작할 수 있고, 각각의 Activity 는 차곡 차곡 Task 라고 불리우는 Activity Stack 에 쌓이게 됩니다. 사용자는 일반적으로 Back 버튼을 이용해서 현재 화면상에 보이는 Activity 를 종료 시키고, 바로 직전에 사용된 Activity 로 돌아갈 수 있습니다. 안드로이드 펍의 회색님의 말을 빌리자면, 인터넷 브라우저를 통해 웹페이지를 검색하는 것과 유사한 방식입니다. 

 하지만 이러한 방법만으로는 효과적인 UX Flow 를 구축하는데 어려움이있습니다. 다행히, 구글에서는 Activity 를 호출할 때 사용되는 Intent 에 적절한 플래그 값을 설정해서 Activity Stack 을 제어할 수 있는 방법을 제공해 줍니다. 이 플래그들은  FLAG_ACTIVITY 라는 접두어를 갖고 있는데, 종류가 다양해 헷갈리는 수도 있는데, 개인적으로 제가 가장 요긴하게 사용하고 있는 FLAG_ACTIVITY 네 가지를 소개해 봅니다. 

 먼저 FLAG_ACTIVITY_SINGLE_TOP 입니다. 우선 간단하게 그림으로 표현해 보았습니다. A 와 B  두 가지 Activity 가 있는 데, A 라는 Activity 는 B 를 B 라는 Activity 는 다시 자기 자신인 B 를 호출 하는 경우라고 가정해 보겠습니다. 
< FLAG_ACTIVITY_SINGLE_TOP >

 호출하는 Activity B 가 이미 Task 의 가장 앞에 위치하는 경우, 또 하나의 B 를 생성하는 대신, 기존에 존재하는 B Activity 가 재활용됩니다. 이 때 B 가 재활용된다는 것을 개발자가 알아채고 새롭게 전달되는 Intent 를 사용할 수 있도록 B Activity 의 onPause() / onNewIntent() / onResume() 가 순차적으로 호출됩니다. 별 생각없이 동일한 Activity 를 여러번 생성하는 것은 메모리 사용량과 Activity 시작 속도 양쪽 모두에 악영향을 끼칠 수 있습니다. (특히 이미지가 덕지덕지 붙어 있는 Activity 라면). 이런 경우 FLAG_ACTIVITY_SINGLE_TOP 를 적절하게 활용하면 제법 큰 효과를 볼 수 있습니다.

 두 번째는, FLAG_ACTIVITY_NO_HISTORY 플래그입니다. 우선 간단하게 그림으로 표현해 보았습니다. A 와 B  두 가지 Activity 가 있는 데, A 라는 Activity 는 B 를 B 라는 Activity 는 A 를 호출한 후 에 (A->B->A) 사용자가 Back 키를 누르는 경우를 가정해 보겠습니다.
<FLAG_ACTIVITY_NO_HISTORY>

 말 그대로, FLAG_ACTIVITY_NO_HISTORY 로 설정된 Intent 로 시작된 Activity B 는 Task 에 그 흔적을 남기지 않게 됩니다. B 에서 또다른 A 를 시작한 후, Back 을 누르면 일반적인 경우 이전에 실행되었던 B 가 나타나지만, NO_HISTORY 를 사용하는 경우 맨 처음에 실행 되었던 A 가 화면에 표시됩니다. 몇 가지 주의할 점이 있습니다. 우선 NO_HISTORY 를 사용하게 되면 Task 에 해당 Intent 의 정보가 기록되지 않기 때문에, A->B 인 상황 (그림에서 두 번째 단계...) 에서 홈키등을 눌러 다른 Task 로 전환된 후, 다시 본 Task 로 돌아오게 되면, A Activity 가 화면에 표시됩니다. 또한, B Activity 의 onDestroy() 가 호출되는 시점이 조금 애매합니다.일반적인 예상과는 달리, B 에서 또다른 A Activity 를 호출하는 세 번째 단계에서는 onStop 까지만 호출되고, 이 후에 새롭게 호출된 A Activity 가 사라지는 순간 (네 번째 단계) 에서야 onDestroy() 가 호출 됩니다.

 FLAG_ACTIVITY_NO_HISTORY 는 여러가지로 쓸모가 있는데, 특히 특정한 이벤트에 대한 알람등을 위해 다이얼로그 형태로 화면에 표시되는 Activity 들에 적용하기에 편리합니다. (대게의 경우 팝업은 해당 시점에 한번만 보여주면 되니까.)

 다음으로 굉장히 유용한 플래그 두 가지를 동시에 설명해보고자 합니다. FLAG_ACTIVITY_REORDER_TO_FRONT 와 FLAG_ACTIVITY_CLEAR_TOP 플래그입니다. 우선 간략하게 그림으로 살펴 보겠습니다. A Activity 에서 B Activity 를 그리고 B 에서 A 를 호출하는 상황을 가정해보았습니다. (A->B->A)


 FLAG_ACTIVITY_REORDER_TO_FRONT 는 매우 특이하게도 Task 의 순서 자체를 뒤바꿔 줍니다. 이 플래그를 사용하면, 런치하고자 하는 Activity 가 이미 Task 상에 존재하는 경우 해당 Activity 를 새롭게 생성하는 대신, 아래쪽에 위치한 Activity 의 순서를 Task 의 가장 위로 끌어올려줍니다. 따라서 A->B->A 의 순서로 Activity 호출이 일어날때, 새로운 A 가 생성되는 대신 아래쪽에 위치한 A 가 위로 올라와 최종적으로 B->A 의 형태로 Task 가 구성되게 됩니다. 어떤 Activity 에서 특정 Activity 로 점프하는 형식의 Flow 를 구성해야하는 경우 요긴하게 사용될 수도 있지만, Task 의 순서를 뒤섞는 다는 점에서 사용에 주의를 기울일 필요가 있습니다.  (별 생각없이 남발하게 되면 Back 키가를 누를 때 엉뚱한 Activity 가 표시되어 사용자들이 굉장히 혼란스러워 하는 경우가 있습니다.) 

 마지막으로 소개해 드릴 플래그는 바로 FLAG_ACTIVITY_CLEAR_TOP 입니다. 제가 개인적으로 가장 사랑스럽게 생각하는 녀석입니다. 이 플래그가 사용되는 경우  
런치하고자 하는 Activity 가 이미 Task 상에 존재하는 경우, 해당 Activity 위에 존재하는 다른 Activity 를 모두 종료시켜 줍니다. 따라서 A->B->A 로 호출이 일어나는 경우, B Activity 가 종료 되고, A Activity 만 Task 에 남게 됩니다. (A->B->C->A 인 경우에도 마찬가지로 B와 C 가 종료되고 A 만 남게 됩니다.)

  이 Flag 는 특정 어플리케이션의 대쉬보드 (혹은 홈) Activity 를 호출할 때 굉장히 유용하게 사용될 수 있습니다. 즉 하나의 어플리케이션이 하나 혹은 두 가지의 주요한 Activity 와 그 외 특정 값을 선택하는등 단순한 일을 수행하기 위한 여러 개의 Sub-Activity 로 구성되어 있다면, 주요 Activity 를 호출하는데 이 Flag 를 사용함으로서 어플리케이션의 홈버튼등을 손쉽게 구현할 수 있습니다. 또 이 Flag 는 FLAG_ACTIVITY_REORDER_TO_FRONT 와는 달린 Task 의 순서를 뒤섞지 않음으로 사용자에게도 큰 혼란을 주지 않습니다. (사용을 적극 권장합니다.)

 한 가지 주의해야할 점은 A->B->A 형태로 Activity 를 호출 하고자 할 때, 단순히 FLAG_ACTIVITY_CLEAR_TOP 만을 사용하게 되면, 기존에 생성되었던 A Activity 가 종료된 후 (onDestroy() 가 호출됨) 새롭게 A 가 생성 (onCreate()) 됩니다. 만일 기존에 사용된 A Activity 가 계속 사용되기를 원한다면, SINGLE_TOP 플래그와 함께 사용해야 합니다.

 휴... 생각보다 내용이 길어졌네요. 대부분 SDK 문서를 읽어보면 잘 나와있는 내용이라 새로운 내용은 없습니다만... 문서 읽기를 게을리한 덕분에 한창을 고생했던 기억이 떠올라 (특히 CLEAR_TOP) 유용하게 사용한 Flag 를 정리하는 겸 작성해 보았습니다~

+ Recent posts