posted by By훈트 2011.02.15 09:42

안드로이드에서 기본적으로 지원하지 않는 UI 를 만들때 CustomView 를 사용합니다.

이러한 CustomView 의 기본적인 작성방법을 알아보도록 하겠습니다.

 

CustomView 는 “android.view.View” 클래스를 상속해서 만들어 집니다.

기본적으로 onDraw() 메소드만 재정의해서 xml 에 view 태그만 추가하면 오류없이 출력되는것을 볼 수 있습니다.

 

이번 포스트에서는 간단히 클릭하면 반응하는 CustomView 를 만들어 보도록 하겠습니다.

먼저 CustomView 소스를 확인해 보도록 하겠습니다.

 

- CustomView.java

package net.cranix.android.customviewtest;

import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View;
public class CustomView extends View {
private String text = null; private int backgroundColor = Color.RED;
private String tempText;
// 속성이 없는 생성자는 소스상에서 직접 생성할때만 쓰인다. public CustomView(Context context) { super(context); Log.w(Constants.TAG,"CustomView("+context+")"); }
/* * 리소스 xml 파일에서 정의하면 이 생성자가 사용된다. * * 대부분 this 를 이용해 3번째 생성자로 넘기고 모든 처리를 3번째 생성자에서 한다. */ public CustomView(Context context,AttributeSet attrs) { this(context,attrs,0); Log.w(Constants.TAG,"CustomView("+context+","+attrs+")"); }

/* * xml 에서 넘어온 속성을 멤버변수로 셋팅하는 역할을 한다. */ public CustomView(Context context,AttributeSet attrs,int defStyle) { super(context,attrs,defStyle); this.text = attrs.getAttributeValue(null,"text"); Log.w(Constants.TAG,"CustomView("+context+","+attrs+","+defStyle+"),text:"+text); }

/* * xml 로 부터 모든 뷰를 inflate 를 끝내고 실행된다. * * 대부분 이 함수에서는 각종 변수 초기화가 이루어 진다. * * super 메소드에서는 아무것도 하지않기때문에 쓰지 않는다. */ @Override protected void onFinishInflate() { setClickable(true); Log.w(Constants.TAG,"onFinishInflate()"); }
/* * 넘어오는 파라메터는 부모뷰로부터 결정된 치수제한을 의미한다. * 또한 파라메터에는 bit 연산자를 사용해서 모드와 크기를 같이 담고있다. * 모드는 MeasureSpec.getMode(spec) 형태로 얻어오며 다음과 같은 3종류가 있다. * MeasureSpec.AT_MOST : wrap_content (뷰 내부의 크기에 따라 크기가 달라짐) * MeasureSpec.EXACTLY : fill_parent, match_parent (외부에서 이미 크기가 지정되었음) * MeasureSpec.UNSPECIFIED : MODE 가 셋팅되지 않은 크기가 넘어올때 (대부분 이 경우는 없다) * * fill_parent, match_parent 를 사용하면 윗단에서 이미 크기가 계산되어 EXACTLY 로 넘어온다. * 이러한 크기는 MeasureSpec.getSize(spec) 으로 얻어낼 수 있다. * * 이 메소드에서는 setMeasuredDimension(measuredWidth,measuredHeight) 를 호출해 주어야 하는데 * super.onMeasure() 에서는 기본으로 이를 기본으로 계산하는 함수를 포함하고 있다. * * 만약 xml 에서 크기를 wrap_content 로 설정했다면 이 함수에서 크기를 계산해서 셋팅해 줘야한다. * 그렇지 않으면 무조껀 fill_parent 로 나오게 된다. */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// height 진짜 크기 구하기 int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = 0; switch(heightMode) { case MeasureSpec.UNSPECIFIED://mode 가 셋팅되지 않은 크기가 넘어올때 heightSize = heightMeasureSpec; break; case MeasureSpec.AT_MOST://wrap_content (뷰 내부의 크기에 따라 크기가 달라짐) heightSize = 20; break; case MeasureSpec.EXACTLY://fill_parent, match_parent (외부에서 이미 크기가 지정되었음) heightSize = MeasureSpec.getSize(heightMeasureSpec); break; }
// width 진짜 크기 구하기 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = 0; switch(widthMode) { case MeasureSpec.UNSPECIFIED://mode 가 셋팅되지 않은 크기가 넘어올때 widthSize = widthMeasureSpec; break; case MeasureSpec.AT_MOST://wrap_content (뷰 내부의 크기에 따라 크기가 달라짐) widthSize = 100; break; case MeasureSpec.EXACTLY://fill_parent, match_parent (외부에서 이미 크기가 지정되었음) widthSize = MeasureSpec.getSize(widthMeasureSpec); break; }
Log.w(Constants.TAG,"onMeasure("+widthMeasureSpec+","+heightMeasureSpec+")"); setMeasuredDimension(widthSize, heightSize); }
/* * onMeasure() 메소드에서 결정된 width 와 height 을 가지고 어플리케이션 전체 화면에서 현재 뷰가 * 그려지는 bound 를 돌려준다. * 이 메소드에서는 일반적으로 이 뷰에 딸린 children 들을 위치시키고 크기를 조정하는 작업을 한다. * 유의할점은 넘어오는 파라메터가 어플리케이션 전체를 기준으로 위치를 돌려준다. * * super 메소드에서는 아무것도 하지않기때문에 쓰지 않는다. */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { Log.w(Constants.TAG,"onLayout("+changed+","+left+","+top+","+right+","+bottom+")"); }
/* * 이 뷰의 크기가 변경되었을때 호출된다. * * super 메소드에서는 아무것도 하지않기때문에 쓰지 않는다. */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { Log.w(Constants.TAG,"onSizeChanged("+w+","+h+","+oldw+","+oldh+")"); }
/* * 실제로 화면에 그리는 영역으로 View 를 상속하고 이 메소드만 구현해도 제대로 보여지게 된다. * * 그릴 위치는 0,0 으로 시작해서 getMeasuredWidth(), getMeasuredHeight() 까지 그리면 된다. * * super 메소드에서는 아무것도 하지않기때문에 쓰지 않는다. */ @Override protected void onDraw(Canvas canvas) { final Paint p = new Paint(); p.setColor(backgroundColor); canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(), p); if (text != null) { p.setColor(Color.BLACK); canvas.drawText(text, 10, 15, p); // 왼쪽 아래를 0,0 으로 보고있음 } Log.w(Constants.TAG,"onDraw("+canvas+")"); }
/* * 현재 view 가 focus 상태일때 key 를 누르면 이 메소드가 호출됨. * 즉 이 메소드를 사용하려면 setFocusable(true) 여야함. *
     * 그리고 super 메소드에서는 기본적인 키 작업(예를들면 BACK 키 누르면 종료)을 처리하기 때문에 
     * 일반적으로 return 시에 호출하는게 좋다.
     * 만약 기본적인 작업을 하지않게 하려면 super 함수를 호출하지 않아도 된다.
     * 
     * 다른 event 메소드들도 유사하게 동작한다.
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        Log.w(Constants.TAG,"onKeyDown("+keyCode+","+event+")");
        return super.onKeyDown(keyCode, event); 
    }
/* * 이 view 에 touch 가 일어날때 실행됨. * * 기본적으로 touch up 이벤트가 일어날때만 잡아내며 * setClickable(true) 로 셋팅하면 up,move,down 모두 잡아냄 */ @Override public boolean onTouchEvent(MotionEvent event) { Log.w(Constants.TAG,"onTouchEvent("+event+")"); switch(event.getAction()) { case MotionEvent.ACTION_UP: backgroundColor = Color.RED; text = tempText; break; case MotionEvent.ACTION_DOWN: backgroundColor = Color.YELLOW; tempText = text; text = "Clicked!"; break; case MotionEvent.ACTION_MOVE: backgroundColor = Color.BLUE; text = "Moved!"; break; } invalidate(); return super.onTouchEvent(event); }
public String getText() { return text; }
public void setText(String text) { this.text = text; } }

- 크기 계산하기

여기서 중요한 메소드는 onMeasure() 메소드 입니다.

이 메소드는 뷰의 전체 크기를 정하는 메소드 인데 안드로이드의 크기 정하는 방법에 따라 구현법이 달라져야 합니다.

 

안드로이드 레이아웃 xml 파일에서 크기를 지정하는 방법은 4가지가 있습니다.

   - fill_parent (상위 View 의 크기에 따름)

   - match_parent (상위 View 의 크기에 따름)

   - fixed (100px 와 같이 픽셀로 박아놨을때)

   - wrap_content (현재 뷰의 내용에 따름)

 

이렇게 4가지 방법의 특성에 따라서 넘어오는 크기의 종류는 3가지로 구분됩니다.

   - MeasureSpec.EXACTLY : fill_parent, match_parent, fixed 와 같이 상위에서 이미 결정되어버린 크기가 넘어올때 선택됩니다.

   - MeasureSpec.AT_MOST : wrap_content 를 선택했을때 선택됩니다.

   - MeasureSpec.UNSPECIFIED : xml 에 의하지 않고 소스상에서 직접 넣었을 때 나옵니다.

 

여기서 EXACTLY 과 UNSPECIFIED 는 외부에서 크기가 구해져서 내려오는 것이기 때문에 따로 계산할 것이 없으나 AT_MOST 는 내부적으로 크기계산을 해 주어야 합니다.

위의 소스에서는 간단하게 100,20 으로 박아놨지만 실제로 CustomView 를 구현하게 된다면 뷰의 특성에 따라 구현이 달라져야 할 것입니다.

 

 

- xml 에서 파라메터 받아내기

안드로이드 리소스 xml 에서 파라메터를 받아내려면 위 소스의 3번째 생성자에 있는것 처럼 아래와 같은 구문을 써야 합니다.

this.text = attrs.getAttributeValue(null,"text");

 

 

- xml 파일 구성하기

이렇게 만든 CustomView 를 xml 파일에서 사용하려면 아래와같은 xml 구성이 필요합니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<view class="net.cranix.android.customviewtest.CustomView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
text="test"
/>
</LinearLayout>

 

- 실행해 보기

이렇게 구성된 뷰를 Activity 에 넣고 실행해 보면 아래와 같은 화면이 나옵니다.

마우스를 클릭,이동 할때마다 색깔이 변경되는것을 볼 수 있습니다.

image

image

image


[출처] 닉스로그님의 블로그


저작자 표시
신고
posted by By훈트 2011.01.28 12:18

01./** Get Bitmap's Width **/
02.public static int getBitmapOfWidth( String fileName ){
03.try {
04.BitmapFactory.Options options = new BitmapFactory.Options();
05.options.inJustDecodeBounds = true;
06.BitmapFactory.decodeFile(fileName, options);
07.return options.outWidth;
08.catch(Exception e) {
09.return 0;
10.}
11.}
12. 
13./** Get Bitmap's height **/
14.public static int getBitmapOfHeight( String fileName ){
15. 
16.try {
17.BitmapFactory.Options options = new BitmapFactory.Options();
18.options.inJustDecodeBounds = true;
19.BitmapFactory.decodeFile(fileName, options);
20. 
21.return options.outHeight;
22.catch(Exception e) {
23.return 0;
24.}
25.}

[출처] 안드로이드펍 SSamDDak님의 답변
저작자 표시
신고
posted by By훈트 2011.01.26 12:54
  Low density
ldpi (120)
Medium density
mdpi (160)
High density
hdpi (240)
Extra high density
xhdpi (320)
Small QVGA (240x320)      
Normal
WQVGA400 (240x400)
WQVGA432 (240x432)
 HVGA (320x480)
WVGA800 (480x800)
WVGA854 (480x854)
 
Large  
WVGA800 (480x800)
WVGA854 (480x854)
   
Extra Large        


저작자 표시
신고
posted by By훈트 2011.01.24 20:35


내 클래스 내 마음대로!

 

 

[Intro]

 

어플리케이션, 특히 UI 프로그래밍을 하다 보면 메세지 처리 라는 녀석을 피할 수 없습니다.

예를 들어, 로딩 중에 '로딩 중' 이라고 알려 주는 다이얼로그를 표시하고 싶다고 할 때,

단순히 스레드로만 처리를 하면 서로 엉켜서 다이얼로그가 제대로 표시 되지 않을 때가 많지요.

그래서 아래와 같이 살짝 딜레이를 주게 되면 엉키는 것을 조금이나마 완화 시킬 수 있습니다.

Handler handler = new Handler();
handler.postDelayed(rShowDialog, 1000);

Handler는 이 뿐 아니라 UI Thread 실행 기능도 가지고 있죠.

스레드 여러개 사용해 보신 분들은 금방 아실겁니다.

 

위에서 Handler에 대해 잠깐 언급 하긴 했지만,

제가 여기서 다루고자 하는 것은 Handler가 뭐하는 것인지, 어떻게 사용하는 것인지가 아닙니다.

일단 Handler를 사용해 보신 분들을 대상으로 설명을 해 나갈 것이니,

그 점 양해 부탁 드립니다.

 

 

[흘러가는 메세지는 잡을 수 없다!] 

 

일단 Handler는 내부적으로 MessageQueue를 가지고 있습니다.

그래서 Handler로 무조건 적으로 메세지를 보내게 되면,

 

뭐... 쌓이겠죠.

 

하지만 이것은 어디까지나 물리적인 측면에서 볼 때 쌓이는 것입니다.

논리적인 측면에서 볼 때는 충분히 메세지를 놓칠 수 있습니다.

 

예를 들어 봅시다.

onDown 이벤트가 발생 한 뒤에 onSingleTapUp 이벤트가 발생 하는 상황을 살펴 보겠습니다.

보통 onDown - onSingleTapUp 이벤트가 발생 했다는 것은 한번 터치 했을 경우입니다.

그렇다는 것은 터치가 되는 대상이 존재 한다는 말이겠죠.

그럼, 이제 여기서 좀 더 세부적으로 살펴 보겠습니다.

 

터치가 되는 대상이 어떤 하얀색 사각형 '들' 이라고 했을때,

onDown 이벤트 시에는 어떤 사각형이 선택이 되었는지 검색을 하며,

onSingleTapUp 이벤트 시에는 선택된 사각형을 빨간색으로 만드는 일을 할 것입니다.

연결 동작으로 보면 터치 했을 때 선택된 사각형이 빨간색으로 변하게 되겠죠?

 

그런데 만약에 onDown 이벤트 시에 어떤 사각형이 선택이 되었는지 미처 찾기도 전에

onSingleTapUp 이벤트가 발생한다면 어떻게 될까요?

네... 물론 아무것도 변하지 않겠죠.

그렇게 되면 결과적으로 onSingleTapUp 이벤트는 아무것도 안하고 그냥 흘러가는 메세지가 됩니다.

 

물리적으로는 onDown 이벤트와 onSingleTapUp 이벤트 둘 다 발생 했지만,

논리적으로 봤을땐... 하나는 실패한 이벤트입니다.

마치 하드웨어에서 인터럽트가 많아지면 나중에 들어온 인터럽트는 무시당하는 경우와 비슷하죠.

 

그렇다면 위와 같은 경우에는 흘러가는 메세지를 그냥 보내야만 할까요?

잡아둘 방법은 없을까요?

 

 

[메세지를 흘러가지 못하도록 막자!]

 

이제부터 본격적으로 제가 하고싶은 이야기를 할 것입니다.

그림이 없어서 복잡할지도 모르겠지만, 최대한 잘 설명해 보겠습니다.

위에서 언급했던 onDown - onSingleTapUp 이벤트를 다시 한번 봅시다.

 

선택된 사각형을 찾았을 때 onRectangleFounded 메소드가 호출 되며,

onSingleTapUp 이벤트 안에서 선택된 사각형의 색을 바꾼다고 합시다.

 

그렇다면, 동작이 원하는데로 이루어지기 위해서는 아래와 같은 순서로 진행 되어야 합니다.

onDown - onRectangleFounded - onSingleTapUp - (선택된 사각형의 색이 변함)

하지만 만약 사각형을 찾는 시간이 onSingleTapUp 이벤트 보다 느리다면,

onDown - onSingleTapUp (색을 바꿀 사각형이 없음) - onRectangleFounded - (변화 없음)

 

onSingleTapUp 보다는

onRectangleFounded의 이벤트를 먼저 처리 해야 한다는 것을 다시 한번더 정리해 봤습니다.

항상 첫 번째의 가장 좋은 경우가 일어나면 괜찮은데,

두 번째의 경우가 일어나지 말라는 법은 없습니다. 보장 할 수 없죠.

그래서 한번 생각해 봤습니다. Lock을 걸면 되지 않을까...?

 

Lock을 걸어본다면 흐름은 아마 아래와 같을 것입니다.

onDown with Lock - onSingleTapUp (Lock에 의해 메세지가 실행 대기 상태가 됨) 
- onRectangleFounded with Unlock (대기 하고 있던 onSingleTapUp 메세지가 실행 됨)

onSingleTapUp 메세지는 Lock에 의해 Queue에 저장이 되고,

Unlock시에 Queue에 쌓인 메세지들을 실행 하면 되겠거니 하고 생각해봤습니다.

 

 

[Extends Handler]

 

우선 Handler를 extends 하기 위해서는 반드시 아래의 메소드를 구현 해야 합니다.

public void handleMessage(Message msg)
Subclasses must implement this to receive messages.

하지만 Handler에게 메세지를 보낸다고 해서

모두 handleMessage() 메소드에 들어가는것은 아닙니다.

 

 

다음으로, Handler에게 메세지를 보내는 방법은 여러가지 방법이 있습니다.

 

sendEmptyMessage Family of Handler class

public final boolean sendEmptyMessage(int what)
public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis)
public final boolean sendEmptyMessageDelayed(int what, long delayMillis)

sendMessage Family of Handler class

public final boolean sendMessage(Message msg)
public final boolean sendMessageAtFrontOfQueue(Message msg)
public boolean sendMessageAtTime(Message msg, long uptimeMillis)
public final boolean sendMessageDelayed(Message msg, long delayMillis)

sendEmptyMessage는 내용 없는 메세지만, sendMessage는 실제 Message를 보냅니다.

어떤 메세지인가? 혹은 메세지에 데이터가 필요한가?에 따라서

유용하게 사용 할 수 있을 것입니다.

 

Message.sendToTarget()

메세지를 보내는 또 한가지 방법은

Message 객체의 sendToTarget() 메소드를 호출 하는 방법입니다.

Message 객체 안에 설정된 Target Handler에게 Message 객체 자신이 직접 보내지는 것이죠.

 

(Handler를 많이 써오시던 분들은 post Family를 많이 사용하셨을 겁니다.

post Family는 실행 가능한 Runnable을 직접 Message Queue에 넣는일을 하기 때문에

일단 여기서는 제외 시켰습니다.)

 

 

sendMessage를 하던 sendToTarget으로 자신을 직접 보내던,

중요한것은 Message 객체를 생성하는 일입니다.

Constructor를 사용하여 직접 Message 객체를 생성해도 되지만,

Message.obtain() 메소드나 Handler.obtainMessage() 메소드의 사용을 권장하고 있습니다.

 

Constructor로 만드는것과 obtain 메소드를 사용하는 것의 큰 차이는

obtain 메소드를 사용하면 재활용 될 객체를 pool에서 뽑아온다는 것이

큰 차이점이라고 볼 수 있습니다. 새로 만드는 것 보단 낫겠죠?

 

Message.obtain()과 Handler.obtainMessage() 메소드의 차이점은,

Handler.obtainMessage()의 경우 자동으로 Target Handler가

호출 하는 Handler로 정해진다는 것이 차이점입니다.

즉, Message.obtain() 메소드를 사용하면 Target Handler를 지정해 줘야 한다는 말과도 같겠죠.

 

Handler.sendMessage() 메소드와 Message.sendToTarget() 메소드,

Handler.obtainMassage() 메소드와 Message.obtain() 메소드,

이 중에서 마음에 드는 것으로 사용 하시면 되겠습니다.

 

 

[Implements LockableMessageHandler]

 

이제 실제로 구현에 대해서 살펴 보겠습니다.

앞서 이야기 했던 extends 방법을 조합하면 대충 머릿속에 그려지지 않나요?

 

더 앞서 이야기 했듯이,

제가 하고 싶은 것은 Lock일 때 메세지를 쌓고, Unlock일 때 쌓인 메세지를 실행 하는 것입니다.

그럼 Lock 상태와 Unlock 상태가 필요 하고, 메세지를 쌓아 둘 Queue도 필요 합니다.

 

이 점을 유념해서 한번 만들어 보겠습니다.

맨 처음으로 Handler를 상속받은 클래스를 만들고, handleMessage() 메소드를 Override 해줍시다.

public final class LockableMessageHandler extends Handler {
    @Override
    public void handleMessage(Message msg) { ... }
}

이제 Lock과 Unlock에 대한 메세지를 만들겁니다.

@Override
public void handleMessage(Message msg) {
    if(msg.what == 1) {
        isLocked true;
    }
    else if(msg.what == 2) { 
        isLocked false;
        
        // RUN!!!
        while(mRunnableQueue.size() > 0) {
            Runnable runnable = mRunnableQueue.poll();
            this.post(runnable);
        }
    }
    ...

Message.what은 사용자 정의 메세지 코드이며, integer 값입니다.

그래서 지금은 편의상 1과 2로 나눠 놨습니다.

 

메세지 코드 1번은 Lock 동작으로, isLocked 필드에 true 값을 세팅하기만 하고,

메세지 코드 2번은 Unlock 동작으로, isLocked 필드에 false 값을 세팅하고,

mRunnableQueue에 쌓여있던 Runnable 들을 모두 빼내어

Handler.post() 메소드를 통해 모두 실행하게 합니다.

 

여기까지는 제가 원하는 Lock과 Unlock 동작이 간단하게 구현 되어있습니다.

그럼 이제 Queue에 쌓는 메소드만 만들면 되겠군요!

@Override
public void handleMessage(Message msg) {
    ...
    else if(msg.what == 3) {
        Runnable callback = (Runnable) msg.obj;
        if(callback != null) {
            try mRunnableQueue.put(callback); }
            catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
    ...
}

메세지 코드 3번은 매우 간단합니다.

Message.obj는 Message.obtain()을 통해 넘겨받은 Object 객체입니다. 

어떤 객체든 다 들어가지요~ 그래서 Message.obtain()을 통해

실행 하고자 하는 코드가 들어있는 Runnable 객체를 받은 뒤에,

무조건 mRunnableQueue에 쌓습니다.

 

이 코드는 지금 현재 Lock 여부에 상관 없이 무조건 쌓고,

Unlock 할 때 모조리 실행 되는 코드입니다.

근데 이런 동작은 뭔가... 좀 부족하죠~

 

그래서 좀 더 좋게 만들어 봤습니다!

@Override
public void handleMessage(Message msg) {
    ...
    else if(msg.what == 4) {
        Runnable callback = (Runnable) msg.obj;
        if(callback != null) {
            if(isLocked == false) { this.post(callback); }
            else {
                try mRunnableQueue.put(callback); }
                catch (InterruptedException e) { e.printStackTrace(); }
            }
        }
    }
}

메세지 코드 4번도 매우 간단합니다.

Message.obtain()을 통해 실행 하고자 하는 코드가 들어있는 Runnable 객체를 받은 뒤에,

만약 Lock 상태라면 mRunnableQueue에 쌓고, 아니면 그냥 Handler.post()로 실행 시켜줍니다.

 

이것도 참 별것 없는 코드네요...

하지만 3번 보다는 좀 더 나은 것 같습니다.

 

 

[Use LockableMessageHandler]

 

만들었으니 써봅시다!

앞서 이야기 했던 Message.obtain() 메소드를 사용하여 사용 할 수 있습니다.

일단 만들고...

LockableMessageHandler mHandler = new LockableMessageHandler();

아까 Lock이 1번이었죠? 그렇다면 이렇게 사용 할 수 있습니다.

Message.obtain(mHandler, 1).sendToTarget();
    -> Message.obtain(Handler h, int what)

귀찮으니 한줄에... 그리고 쌓는 동작은 3번이었죠?

Message.obtain(mHandler, 3, new Runnable() { ... }).sendToTarget();
    -> Message.obtain(Handler h, int what, Object obj)

이렇게 사용 할 수 있습니다.

 

 

[Complete Source]

 

클래스 밖에서 Message.obtain()을 호출해서 사용 할 수 도 있지만,

사실 클래스 안에서도 Message.obtain()을 호출해서 사용 할 수 있습니다.

그렇게 되면 코드가 한결 더 간결해 지겠죠~

 

그리하여 정리해 본 전체 소스 입니다.

 

public final class LockableMessageHandler extends Handler {
    // Fields
    private boolean isLocked;
    private ArrayBlockingQueue<Runnable> mRunnableQueue;
    
    private final int CODE_LOCK          = 1;
    private final int CODE_UNLOCK        = 2;
    private final int CODE_PUT           = 3;
    private final int CODE_PUT_IF_LOCKED = 4;
    
    // Constructors
    public LockableMessageHandler() {
        isLocked false;
        mRunnableQueue new ArrayBlockingQueue<Runnable>(100);
    }

    // Override Methods
    @Override
    public void handleMessage(Message msg) {
        if(msg.what == CODE_LOCK) {
            isLocked true;
        }
        else if(msg.what == CODE_UNLOCK) {
            isLocked false;
        
            // RUN!!!
            while(mRunnableQueue.size() > 0) {
                Runnable runnable = mRunnableQueue.poll();
                this.post(runnable);
            }
        }
        else if(msg.what == CODE_PUT) {
            Runnable callback = (Runnable) msg.obj;
            if(callback != null) {
                try mRunnableQueue.put(callback); }
                catch (InterruptedException e) { e.printStackTrace(); }
            }
        }
        else if(msg.what == CODE_PUT_IF_LOCKED) {
            Runnable callback = (Runnable) msg.obj;
            if(callback != null) {
                if(isLocked == false) { this.post(callback); }
                else {
                    try mRunnableQueue.put(callback); }
                    catch (InterruptedException e) { e.printStackTrace(); }
                }
            }
        }
    }

    // Handle Methods
    public void lock() {
        Message.obtain(thisCODE_LOCK).sendToTarget();
    }
    public void unlock() {
        Message.obtain(thisCODE_UNLOCK).sendToTarget();
    }  
    public void put(Runnable r) {
        Message.obtain(thisCODE_PUT, r).sendToTarget();
    }
    public void putIfLocked(Runnable r) {
        Message.obtain(thisCODE_PUT_IF_LOCKED, r).sendToTarget();
    }
}

깔끔하게 정리가 되었네요!

Handler를 extends 했기 때문에

평소에 쓰던 post(), postDelayed() 같은 메소드도 사용 가능합니다~

 

 

[Outro]

 

점점 내용도 난해해 지고 양도 많아지고 있습니다.

아무래도 기초지식 보다는 좀 Advance한 내용을 다루고자 하는 컨셉 때문인지도...

여튼 이 포스트에서 중점적으로 이야기 하고 싶은 것은

Handler를 원하는 대로 extends 하여 사용하는 방법입니다.

 

만약 Handler를 변경 하고자 하시는 분들에게 조금이나마 도움이 되었으면 합니다.

 

참고로 전 예전에 만들어 둔 AdvancedGestureDetectorWrapper와 함께

아주 잘~ 사용하고 있답니다. 으흐흐...

 

 

[Post Script]

 

이 내용에 대해 1월 부터 기획하고 있었는데 이제서야 마무리를 하게 됬습니다.

Nexus One이 튀어 나오는 바람에 리뷰를 한다고 설쳐대던 영향도 없지 않아 있었군요...

여튼 오래전 부터 쓰고 싶었던 포스팅을 마무리 하게 되어서 기분은 좋네요!

 

앞으로 더 난해한 내용들에 대해서 심도 있게 다루어 보고 싶은데...

시간이 별로 없군요...


[출처] 비즈페이님의 블로그

저작자 표시
신고
posted by By훈트 2011.01.24 18:59



가끔 귀찮다면 이렇게!

 

 

[Intro]

 

TextView를... 즉 글씨를 다루다 보면 폰트 설정 하는게 여간 귀찮은게 아닙니다.

14dip가 화면상에서 얼마나 큰지는 직접 찍어봐야 아는것이죠...

 

그냥 단순히 큰 글씨! 작은 글씨!를 보여주고 싶다~라고 할 때

이 방법을 사용해 보시죠!

 

 

[R.attr?]

 

이 녀석은 참 흥미로운 녀석입니다.

제가 다 뜯어 보진 않았지만... 뭔가 복잡한 속성 세팅을 한방에 해결해 주는 녀석이죠.

꼭 TextView에만 쓰이는건 아닙니다. ProgressBar를 만들 때도 쓰이더군요...

(사실 ProgressBar의 노란 그래디언트 부분은 이미지가 아니라

GradientDrawable에 R.attr의 값중 하나를 세팅한겁니다! 놀라워라...)

 

 

[R.attr.textAppearance Family]

 

그렇습니다... 그냥 쓰면 되는겁니다.

textAppearance
textAppearanceButton
textAppearanceInverse
textAppearanceLarge
textAppearanceLargeInverse
textAppearanceMedium
textAppearanceMediumInverse
textAppearanceSearchResultSubtitle (API Level : 5)
textAppearanceSearchResultTitle (API Level : 5)
textAppearanceSmall
textAppearanceSmallInverse

http://developer.android.com/reference/android/R.attr.html

 

위의 주소에서 좀 더 자세한 내용을 볼 수 있습니다.

그냥 변수 이름만 봐도 뭐하는지 딱 감이 잡힙니다.

 

그래서 한 번 직접 찍어 봤습니다!

 

 


 

 

Inverse 계열은 검은색 글씨라 배경을 빨갛게 칠해 봤습니다.

SearchResult 계열은 Level 5이고 나머지는 Level 1인데,

API설치 해 놓은게 Level 4 인지라 테스트는...

여튼! 뭐 그냥 이것만 보면 더 설명 할 게 없군요~

 

 

[Usage]

 

그럼 어떻게 사용하느냐! 아주 간단합니다.

TextView에 이런 Attribute 하나 넣어주면 됩니다.

android:textAppearance="?android:attr/textAppearanceSmall"

참 쉽죠?

폰트 설정하기 귀찮다면 이렇게...


[출처] 비즈페이님의 블로그

저작자 표시
신고
posted by By훈트 2011.01.24 18:39


의외로 쉬운 방법이 존재합니다!

 

 

[Intro]

 

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

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

 

실제로 안그럴지 몰라도,

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

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

 

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

RawResource를 InputStream으로 얻어와서 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. 크기를 가지는 WidthHeight 변수

 

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_8888true);

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

 

 

[Outro]

 

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

 

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

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

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

 

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

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

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

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

 

네... 제가 하고 싶은말은 그겁니다.

이미지 파일 힘들게 바꾸지 맙시다!


[출처] 비즈페이님의 블로그

저작자 표시
신고
posted by By훈트 2011.01.24 18:36

생각보다 숨겨진 TIP이 많아요

 

 

[Intro]

 

Android에서 사용하는 이미지는 Bitmap이라는 클래스에서 다~ 알아서 해줍니다.
그리고 이런 Bitmap Object를 쉽게 만들 수 있도록 도와주는 
BitmapFactory 클래스 라는 것도 있습니다.

 

BitmapFactory는 여러가지 소스로 부터 Bitmap Object를 만들어 주는 일을 하는데,
전부 static이며 decodeXXX 라는 이름을 가진 메소드들로 이루어져 있습니다.

XXX에는 어떤 것으로 부터 decode를 하여 
Bitmap Object를 만들어 낼지에 대한 말들이 들어 가겠죠.

 


[Decoding Methods]

 

BitmapFactory.decodeByteArray() 메소드는 Camera.PictureCallback 으로 부터 받은
Jpeg 사진 데이터를 가지고 Bitmap으로 만들어 줄 때 많이 사용 합니다.
Camera.PictureCallback에서 들어오는 데이터가 byte[] 형식이기 때문에
저 메소드를 사용 해야 하는 것이죠.

 

BitmapFactory.decodeFile() 메소드는 파일을 그대로 읽어 옵니다.
내부적으로는 파일 경로를 가지고 FileInputStream을 만들어서 decodeStream을 합니다.
그냥 파일 경로만 쓰면 다 해주는게 편리 한 것이죠.

 

BitmapFactory.decodeResource() 메소드는 Resource로 부터 Bitmap을 만들어 내며
BitmapFactory.decodeStream() 메소드는 InputStream으로 부터 Bitmap을 만들어 냅니다.
뭐 그냥 이름만 봐도 알 수 있는 것들이지요.

 


[OutOfMemoryError??]

 

보통 이미지 파일을 읽어서 Resizing을 해야 할 때가 있는데, 
그럴때는 BitmapFactory로 읽어서 Bitmap.createScaledBitmap() 메소드를 사용하여 줄이면

간단하게 처리 할 수 있습니다.

 

그런데 BitmapFactory를 사용할 때 주의해야 할 점이 있습니다.
아래의 예를 한번 보시죠.

Bitmap src = BitmapFactory.decodeFile("/sdcard/image.jpg");
Bitmap resized = Bitmap.createScaledBitmap(src, dstWidth, dstHeight, true);

이미지 파일로부터 Bitmap을 만든 다음에

다시 dstWidth, dstHeight 만큼 줄여서 resized 라는 Bitmap을 만들어 냈습니다.
보통이라면 저렇게 하는게 맞습니다.

 

읽어서, 줄인다.

 

그런데 만약 이미지 파일의 크기가 아주 크다면 어떻게 될까요?
지금 Dev Phone에서 카메라로 촬영하면
기본적으로 2048 x 1536 크기의 Jpeg 이미지가 촬영된 데이터로 넘어옵니다.
이것을 decode 하려면 3MB 정도의 메모리가 필요 할 텐데,

과연 어떤 모바일 디바이스에서 얼마나 처리 할 수 있을까요?

 

실제로 촬영된 Jpeg 이미지를 여러번 decoding 하다보면

아래와 같은 황당한 메세지를 발견 할 수 있습니다.

java.lang.OutOfMemoryError: bitmap size exceeds VM budget

네... OutOfMemory 입니다.
더 이상 무슨 말이 필요 하겠습니까...
메모리가 딸려서 처리를 제대로 못합니다.

 

이것이 실제로 decoding 후 메모리 해제가 제대로 되지 않아서 그런 것인지, 
하더라도 어디서 Leak이 발생 하는지에 대한 정확한 원인은 알 수 없습니다.
이것은 엔지니어들이 해결해야 할 문제 겠죠...

 

하지만 메모리 에러를 피할 수 있는 방법이 있습니다.

 


[BitmapFactory.Options.inSampleSize]

 

BitmapFactory.decodeXXX 시리즈는 똑같은 메소드가 두 개씩 오버로딩 되어 있습니다.
같은 이름이지만 Signature가 다른 메소드의 차이점은
BitmapFactory.Options를 파라메터로 받느냐 안받느냐의 차이죠.

BitmapFactory.Options를 사용하게 되면 decode 할 때 여러가지 옵션을 줄 수 있습니다.


여러가지 많지만 저희가 지금 사용할 것은 inSampleSize 옵션 입니다.

 

inSampleSize 옵션은,
애초에 decode를 할 때 얼마만큼 줄여서 decoding을 할 지 정하는 옵션 입니다.

 

inSampleSize 옵션은 1보다 작은 값일때는 무조건 1로 세팅이 되며,
1보다 큰 값, N일때는 1/N 만큼 이미지를 줄여서 decoding 하게 됩니다.
즉 inSampleSize가 4라면 1/4 만큼 이미지를 줄여서 decoding 해서 Bitmap으로 만들게 되는 것이죠.

 

2의 지수만큼 비례할 때 가장 빠르다고 합니다.
2, 4, 8, 16... 정도 되겠죠?

 

그래서 만약 내가 줄이고자 하는 이미지가 1/4보다는 작고 1/8보다는 클 때,
inSampleSize 옵션에 4를 주어서 decoding 한 다음에,

Bitmap.createScaledBitmap() 메소드를 사용하여 한번 더 줄이면 됩니다.

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
Bitmap src = BitmapFactory.decodeFile("/sdcard/image.jpg", options);
Bitmap resized = Bitmap.createScaledBitmap(src, dstWidth, dstHeight, true);

당연한 이야기 이겠지만,
내가 원하고자 하는 사이즈가 딱 1/4 크기라면

Bitmap.createScaledBitmap() 메소드를 쓸 필요가 없지요.

 

inSampleSize 옵션을 잘 활용하면 메모리 부족 현상을 대략적으로 해소 할 수 있습니다.
참고로 제가 저 옵션을 사용한 뒤로는 메모리 에러를 본적이 한~번도 없답니다.

 


[Appendix]

 

inSampleSize 옵션을 사용하면

SkScaledBitmapSampler Object (Library Level) 를 생성 하게 되는데,
Object를 만들때 정해진 SampleSize 만큼 축소하여 width와 height를 정한 뒤에 만들게 됩니다.
그러니까 애초에 축소된 사이즈로 이미지를 decoding 하는 것이죠.

 


[Outro]

 

Android의 기본 어플리케이션 소스를 분석 하다보면
상당히 테크니컬한 기법들을 많이 얻을 수 있습니다.
어떻게 이런 방법으로 만들었나 싶을 정도로 매우 정교하고 복잡하게 만들어져 있지요.
참 대단한 것 같습니다.

 

아 그리고 왜 dstWidth와 dstHeight 변수 선언이 없냐고 따지시는 분들 설마 없겠죠?



[출처] 비즈페이님의 블로그

저작자 표시
신고
posted by By훈트 2011.01.24 18:13
가끔씩 마주치게 되는 "OutOfMemoryError : bitmap size exceeds VM budget" 에러는 메모리 누수가 주요 원인입니다. 이와 관련된 링크를 모아봤습니다.

* 액티비티가 멈출 때 비트맵을 재활용(즉 GC)되게 하라

- bitmap 이미지인 경우 recycle() 호출
- onPause에서 수행하는게 좋음
- ((BitmapDrawable)imageView.getDrawable()).getBitmap().recycle();

* 이미지를 미리 줄여서 읽어들여라

- BitmapFactory.Options.inSampleSize 활용

* Activity Context에 대한 참조(reference)를 오랫동안 유지하지 말아라

- Drawable.setCallback(null) 사용
- WeakReference를 가진 static 내부 클래스
- 이미지를 static 변수로 처리하지 마라

* 외부(outer) 클래스의 상태에 의존하지 않는 내부(inner) 클래스는 static으로 선언하라
- 내부클래스는 외부 클래스 인스턴스를 크게 만들며 또한 외부클래스 객체가 필요이상으로 오래 살아있게 되어 메모리를 더 차지할 수 있음
- 외부클래스의 상태 필드에 접근하지 않는(즉 외부 객체의 상태에 의존하지 않는) 내부클래스는 static으로 선언

* Attacking memory problems on Android



저작자 표시
신고
posted by By훈트 2011.01.21 16:59
안드로이드용 어플을 개발할때 안드로이드 SDK에서 제공하는 에뮬레이터를 사용해도 상관은 없지만, 배포하기 전에 최종 테스트는 반드시 실기로 해야 하며, 에뮬레이터 속도가 많이 느리기 때문에 USB 디버깅을 사용하여 실기에서 개발합니다.

디버깅시 케이블 연결이 번거롭거나, 단말이 USB 디버깅을 지원하지 않는 경우 원격으로 디버깅 환경을 구성 할 수 있습니다.

먼저 단말의 터미널에서 루트 권한을 획득합니다.
su

다음으로 adbd가 사용할 포트를 지정합니다.
setprop service.adb.tcp.port 5554
다음으로 adbd를 재시작 합니다.
stop adbd
start adbd


안드로이드 SDK의 tool 디렉토리에서 다음과 같은 명령으로 단말과 연결합니다.
adb connect <단말IP주소>:5554


정상적으로 연결되었는지 확인합니다.
adb devices



저작자 표시
신고
posted by By훈트 2011.01.20 11:03
세로: portrait, 세로의 길이가 가로 보다 길때
가로: Landscape, 가로의 길이가 세로 보다 길때 

만약 코드상에서 알고 싶다면
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int deviceWidth = displayMetrics.widthPixels;
int deviceHeight = displayMetrics.heightPixels;

if( deviceWidth > deviceHeight) 
     // 가로
else
  // 세로

* 나중에 알게된 사실이지만 현재 상태를 알려주는 함수가 있다.
droid.view.Display.getRotation() 를 이용해 보자..

* 참고자료
- 스크린 사이즈 구하는 방법
DisplayMetrics displayMetrics = new DisplayMetrics();

getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int pxWidth  = displayMetrics.widthPixels;
int pxHeight = displayMetrics.heightPixels;

//--- displayMetrics.density : density / 160, 0.75 (ldpi), 1.0 (mdpi), 1.5 (hdpi)
int dipWidth  = displayMetrics.widthPixels  / displayMetrics.density;
int dipHeight = displayMetrics.heightPixels / displayMetrics.density

- Activity 화면 사이즈를 구하는 방법
StringBuffer buf = null;
 WindowManager wm = null;
 Display display = null;
 DisplayMetrics metrics = null;
     
 wm = getWindowManager();
 display = wm.getDefaultDisplay();
 metrics = new DisplayMetrics();
 display.getMetrics(metrics);
     
 buf = new StringBuffer();
 buf.append("Window display id: " + display.getDisplayId() + "\n");
 buf.append("Window orientation: " + display.getOrientation() + "\n");
 buf.append("Window width: " + display.getWidth() + "\n");
 buf.append("Window height: " + display.getHeight() + "\n");
 buf.append("Window pixel format: " + display.getPixelFormat() + "\n");
 buf.append("Window refresh rate: " + display.getRefreshRate() + "\n");
 buf.append("Window width pixels: " + metrics.widthPixels + "\n");
 buf.append("Window height pixels: " + metrics.heightPixels + "\n");
 testMsg.setText(buf.toString());


[출처] 파이드라님 블로그

저작자 표시
신고