본문 바로가기

프로그래밍/안드로이드

[안드로이드] 커스텀 위젯(Custom Widget)

※ 저는 안드로이드 프로그래밍 정복(김상형 著, 한빛미디어) 책을 이용해 공부하고 있으며

예제와 코드는 이 책을 통해 공부중임을 밝힙니다.

개인적인 공부를 하면서 정리한 형식이기 때문에 심각한 오류가 있을 수 있습니다. 피드백 주시면 정말 감사하겠습니다.

※ 안드로이드 프로그래밍 정복 1판 참조



커스텀 : 내 입맛대로 만들겠다!

커스텀 위젯 : 내 입맛대로 위젯을 만들겠다!


안드로이드가 커스텀 위젯 제작을 지원하는 방법


1. 기존 위젯 클래스를 상속 → 기능을 확장하거나 수정


2. 단순한 기능을 제공하는 위젯을 결합하여 복잡한 동작을 수행하는 위젯 그룹을 정의

   VIewGroup이나 파생 클래스를 확장하여 만든다.


3. 기존에 없었던 새로운 위젯을 만든다.

   최상위 위젯 클래스 VIew로부터 상속받는다.


< 1번 예제 : 기존 위젯 클래스를 상속 >



주의할 점은 SoundEditWidget을 작성할 때

풀패키지 명(名).SoundEditWidget으로 작성해주어야 한다.


public class SoundEdit extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.soundedit);
}
}

class SoundEditWidget extends EditText { // 기존 위젯 클래스(EditText)를 상속받음
SoundPool mPool = null;
int mClick;

public SoundEditWidget(Context context) { // 생성자 1
super(context); // 인수 1개, 슈퍼클래스의 생성자를 호출후 상속받은 멤버 초기화
init(context); // 고유의 초기화
}

public SoundEditWidget(Context context, AttributeSet attrs) { // 생성자 2
super(context, attrs); // 인수 2개, 슈퍼클래스의 생성자를 호출후 상속받은 멤버 초기화
init(context); // 고유의 초기화
}

public SoundEditWidget(Context context, AttributeSet attrs, int defStyle) { // 생성자 3
super(context, attrs, defStyle); // 인수 3개, 슈퍼클래스의 생성자를 호출후 상속받은 멤버 초기화
init(context); // 고유의 초기화
}

void init(Context context) { // 고유의 초기화
mPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
mClick = mPool.load(context, R.raw.click, 1);
}

protected void onTextChanged(CharSequence text, int start, int before, int after) {
if (mPool != null) { // 널(Null) 체크
mPool.play(mClick, 1, 1, 0, 0, 1);
}
}
}


0. 먼저 클릭할 때 나는 소리를 res/raw 폴더에 넣어주어야 한다.

1. EditText 클래스를 확장(Extends)하여 SOundEditWidget 클래스를 파생시켰다.


2. 생성자는 세 개를 정의했다. (범용성, 즉 사용의 편리성을 높이기 위해서 다양한 형태로 오버라이딩)


3. 텍스트가 입력되거나 삭제되면 onTextChanged 메서드가 호출된다.


4. if(mPool != null) 널체크


위에서 if (mPool != null ) 문장을 빼보자. 위와 같은 오류가 발생한다.


만약 자바에서 변수값이 null인 상태에서 이를 가지고 작업하려 하면 NullPointerException 오류가 발생한다.

따라서 보통 다음과 같은 널 체크(Null Check)를 실시한다


if (param == null || param.length() == 0) { // 값이 있는 경우 처리
} else {
// 값이 없는 경우 처리
}
또는
if (param == null || param.length() == 0) {
// 값이 없는 경우 처리
} else {
// 값이 있는 경우 처리
}



< 2번 예제 : 위젯 조합 >


복잡한 작업을 수행하기 위해서 관련된 위젯을 하나의 그룹으로 묶어 새로운 위젯을 정의한다.


예제에 들어가기 앞서 내가 복습해야 할 것들이 있다.


1. 위젯(Widget)은 사용자와 통신하면서 상호작용하는 역할을 한다.

가장 빈번하게 사용하는 위젯이 "명령을 입력받는 Button"과 문자열을 입력받는 "EditText"이다.


2. TextView에서는 문자열 포맷팅, 출력, 입력, 편집에 관련된 기능(function)이 구현되어 있다.

그러나, 대부분의 기능은 숨겨져 있으며, "문자열 출력 기능"만 노출한다.

즉, Button(버튼)과 EditText(에딧)은 TextView의 숨겨진 기능을 활성화 + 스타일을 입힌것 뿐이다.


3. 문자열 변경 리스너


void addTextChangedListener (TextWatcher watcher)

텍스트가 변경되는 시점에 특정 작업을 하고 싶다면 위 메서드로 리스너를 "등록"한다.

위 인수에 편집 이벤트를 처리하는 TextWatcher가 있다.

TextWatcher 객체를 생성한 후 리스너를 등록하면 사용자가 문자열을 편집할 때마다

TextWatcher 인터페이스의 다음 메서드가 호출된다.


void beforeTextChanged (CharSequence s, int start, int count, int after)

void afterTextChanged (Editable s)

void onTextChanged (CharSequence s, int start, int before, int count)





public class ExerciseExam extends AppCompatActivity {

public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_excercise_exam);
}
}


// NumEditWidget은 LinearLayout을 상속받아 차일드 위젯을 일렬로 배치하는 능력을 물려받는다.


class NumEditWidget extends LinearLayout implements TextWatcher {
EditText mEdit;
TextView mText;

public NumEditWidget(Context context) {
super(context);
init();
}

public NumEditWidget(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

void init() {
//* 코드로 직접 차일드 생성
setOrientation(LinearLayout.VERTICAL);
mEdit = new EditText(getContext()); // 객체 생성1
mText = new TextView(getContext()); // 객체 생성2
mText.setText("Now Length : 0 Characters");

LinearLayout.LayoutParams param = new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
addView(mEdit, param);
addView(mText, param);
//*/

/* 레이아웃 전개


LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.numeditwidget, this, true);
mEdit = (EditText)findViewById(R.id.limedit_edit);
mText = (TextView)findViewById(R.id.limedit_text);
//*/

mEdit.addTextChangedListener(this); EditText의 텍스트 변경 리스너를 this로 지정
}

public void afterTextChanged(Editable s) {
}

public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

public void onTextChanged(CharSequence s, int start, int before, int count) {
mText.setText("Now Length : " + s.length() + " Characters");
}
}


앞 예제는 EditText를직접 상속받은 것이므로 

protected 속성을 가지는 onTextChanged 메서드를 바로 재정의할 수 있다.

(앞 예제를 다시 가져왔다.)


하지만 이 예제는 EditText를 멤버로 포함할 뿐이므로 바로 재정의할 수 없다.

그래서 와처(Watcher)를 등록하고  문자열 변경 시점을 알아낸다.


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<lkcompany.exercise.NumEditWidget
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>

레이아웃에는 NumEditWidget 하나만 배치했다.

커스텀 위젯은 반드시 전체 패키지 경로 (위 예제에서 lkcompnay.exercise) 를 모두 밝혀야 한다.



< 3번 예제 : 커스텀 속성 >


레이아웃 작성시에는 XML문서를 주로 활용한다.

그렇다면, 어떤 과정을 거쳐서 XML 문서의 위젯이 생성되고 속성이 초기화될까?


XML 문서는 aapt 툴에 의해 이진(binary) 형태로 컴파일되어 실행 파일에 포함되며

전개자(Inflater)가 이 정보를 읽어 각 위젯을 "생성"한다.


전개자는 XML 문서에 쓰여진 속성 집합을 "하나의 객체"로 포장하여 위젯의 생성자로 전달한다.

생성자는 멤버를 초기화하는 것이므로, 위젯 스스로 초기화되도록 지시한다.


이 속성 목록이 두 번쨰 인수인 AttributeSet 객체이며 이 안에 초기화할 속성 목록과 속성갑시 컴파일되어 있다.


int getAttributeCount() // (정수형) 속성의 개수

String getAttributeName (int index) // 이름 조사

String getAttributeValue (int index) // 값 조사

int getAttributeIntValue (int index, int defaultValue)    

boolean getAttributeBooleanValue (int index, boolean defaultValue) // (진위형)

float getAttributeFloatValue (int index, float defaultValue) // (실수형)



먼저 결과를 위와같이 출력하려고 한다.



<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<lkcompany.exercise.AttrButton
android:id="@+id/attrbtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Attr Button"
android:textSize="30sp"
android:textColor="#ff0000"
/>
<TextView
android:id="@+id/attrtext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
/>
</LinearLayout>


LinearLayout 안에 버튼 위젯(Button Widget)을 배치하고 여러 가지 속성(textSize, textColor)을 적용했다.


public class ExerciseExam extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_excercise_exam);

AttrButton btn = (AttrButton) findViewById(R.id.attrbtn);
TextView text = (TextView) findViewById(R.id.attrtext);
text.setText(btn.mText);
}
}

class AttrButton extends Button {
String mText = "";

public AttrButton(Context context, AttributeSet attrs) {
super(context, attrs);

int i;
String Name;
String Value;
for (i = 0; i < attrs.getAttributeCount(); i++) {
Name = attrs.getAttributeName(i); // 이름을 조사함
Value = attrs.getAttributeValue(i); // 값을 조사함.
mText += (Name + " = " + Value + "\n");
}    
}
}


생성자로 전달되는 속성을 확인하려면 서브 클래스를 파생하여 생성자를 가로채야 한다.

AttrButton 클래스는 생성자에서 속성의 목록을 "문자열 형태"로 바꿔 자신의 멤버 mText에 저장한다.



< 4번 예제 : 새로운 위젯 만들기 >


새로운 위젯을 만들 떄는 뷰의 최상위 클래스인 View로부터 상속받는다.

View는 "그리기 메서드"와 사용자와 통신하기 위한 "이벤트 핸들러"의 기본 원형을 제공한다.


그러나 뷰의 기능은 너무 일반적이어서 (최상위 클래스니까) 그 자체로는 쓸모가 없다.

따라서 상속받은 후, 반드시 "재정의(overriding)"를 해야한다.


재정의 해야 하는 메서드는 onDraw, onMeasure (위젯의 크기를 결정) 메서드이다.


void onMeasure (int widthMeasureSpec, int heightMeasureSpec)


인수로 전달되는 Spec은 부모 레이아웃이 차일드(자손)에게 제공하는 여유 공간의 폭과 높이에 대한 정보이다. 이 안에 공간의 성질을 지정하는 모드와 공간의 크기값이 저장되어 있다.

 

1. 크기 정하기


위와 같은 예제를 만드려고 한다.

빨간섹 네모는 위젯(widget)이며 수평 리니어 레이아웃에 배치했으므로

 <Linearayout> <Button> , <Custom Widget> , <Button> </LinearLayout><TextView>

이런 구조를 가지게 될 것이다.



<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="100px"
android:background="#ffff00"
>
<Button
android:layout_width="100px"
android:layout_height="wrap_content"
android:text="Left"
/>
<lkcompany.exercise.MeasView
android:id="@+id/meas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<Button
android:layout_width="100px"
android:layout_height="wrap_content"
android:text="Right"
/>
</LinearLayout>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dip"
android:background="#ffffff"
android:textColor="#000000"
android:textSize="14sp"
/>
</LinearLayout>


public class ExerciseExam extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_excercise_exam);

final MeasView meas = (MeasView)findViewById(R.id.meas);
final TextView text = (TextView)findViewById(R.id.text);
text.postDelayed(new Runnable() {
public void run() {
text.setText(meas.mResult);
}
}, 100);
}
}

class MeasView extends View {
String mResult = "";

public MeasView(Context context, AttributeSet attrs, int defStyle) { // 생성자 1
super(context, attrs, defStyle);
}

public MeasView(Context context, AttributeSet attrs) {               // 생성자 2

super(context, attrs);
}

public MeasView(Context context) {                                   // 생성자 3

super(context);
}

protected void onDraw(Canvas canvas) {    // onDraw 메서드. 캔버스를 RED로 채운다.

canvas.drawColor(Color.RED);
}

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 위젯크기 결정
int wMode, hMode;
int wSpec, hSpec;
int Width, Height;

Width = 150; // 폭(Width) = 150
Height = 80; // 높이(Height) = 80

wMode = MeasureSpec.getMode(widthMeasureSpec);
wSpec = MeasureSpec.getSize(widthMeasureSpec);
hMode = MeasureSpec.getMode(heightMeasureSpec);
hSpec = MeasureSpec.getSize(heightMeasureSpec);

switch (wMode) {
case MeasureSpec.AT_MOST: // 부모가 위젯에게 허용된 최대한의 크기를 알려준다.

// 차일드가 가질 수 있는 최대 크기.

// 차일드는 이 값 이하로 크기를 결정해야 한다.
Width = Math.min(wSpec, Width);
break;
case MeasureSpec.EXACTLY: // 부모가 제안한 폭을 사용하면 된다.

// 차일드가 가져야 하는 정확한 크기.
Width = wSpec;
break;
case MeasureSpec.UNSPECIFIED: // 별다를 제한없이 원하는 크기를 지정한다.
break;
}

switch (hMode) {
case MeasureSpec.AT_MOST:
Height = Math.min(hSpec, Height);
break;
case MeasureSpec.EXACTLY:
Height = hSpec;
break;
case MeasureSpec.UNSPECIFIED:
break;
}

setMeasuredDimension(Width, Height); // 최종 결정된 WIdth와 Height를 전달.

// 부모에게 자신이 원하는 크기를 알려줌.

mResult += (Spec2String(widthMeasureSpec) + ", "
+ Spec2String(heightMeasureSpec) +
" -> (" + Width + "," + Height + ")\n");
}

String Spec2String(int Spec) {
String str = "";

switch (MeasureSpec.getMode(Spec)) {
case MeasureSpec.AT_MOST:
str = "AT_MOST";
break;
case MeasureSpec.EXACTLY:
str = "EXACTLY";
break;
default:
str = "UNSPECIFIED";
break;
}

str += " " + MeasureSpec.getSize(Spec);
return str;
}
}


xml에서 왼쪽 버튼(첫 번째 차일드)의 폭을 100px로 설정했다.

두 번째 차일드 MeasView를 배치하는데 화면폭 480에서 왼쪽 버튼의 폭 100을 뺀 380의 폭이 전달된다.

높이는 리니어(Linear Layout)의 높이 100이 전달된다.


AT_MOST 380이 전달 되는 것을 볼 수 있다.

AT_MOST 100이 전달 되는 것을 볼 수 있다.


즉, 부모는 (380, 100)을 허락했지만, 위젯(MeasView)은 코드에서 상수로 정한 (150, 80) 만큼만 사용한다.


<Measuring 2 / xml에서 크기 지정>


그러면 이번에는 코드에서 (150, 80)이 아니라 XML에서 (100, 50)으로 설정해보자.

위와 같은 결과가 출력된다.



위젯이 계산한 (150, 80)은 무시당하고 레이아웃에서 한 (100, 50)이 강제 적용된다.


<Measuring 3 / xml에서 커스텀 위젯 양 옆에 배치된 버튼 제거>



버튼이 사라졌으므로 부모는 자신의 모든 영역인 480, 100을 허락하며 위젯은 이를 그대로 받아들인다.

따라서 리니어 전체가 MeasView로 가득 채워진다.



<Measuring 4 / 커스텀 위젯의 폭과 높이를 match_parent로 지정>

커스텀 위젯 MeasView의 폭과 높이를 match_parent로 지정하면 왼쪽 버튼이 먼저 자리를 차지하고 남은 역 전체를 다 사용한다.

왼쪽 버튼이 차지한 폭은 제외하고, 남은 공간에 대해서만 Spec을 전달한다.

그런데 MeasView의 레이아웃 크기가 match_parent이므로 스펙 모드는 최대값(AT_MOST)가 아니라

이만큼을 다 채우라는 EXACTLY가 전달되며, MeasView는 이 값을 받아들인다.


따라서 오른쪽 버튼이 배치될 공간이 없다.


<Measuring 5 / 종횡비 유지 코드 추가>


폭과 높이가 모두 AT_MOST일 때, 즉 일정한 범위의 영역이 허락되어 있을 떄

종횡비를 유지하기 위해 포과 높이를 강제로 작은 쪽으로 맞춘다.



< 5번 예제 : 무지개 프로그래스 >


                 


버튼과 에디트와는 모양이 완전히 다르므로 기존 클래스의 상속이나 조합을 통해서는 만들 수 없다.

표준 프로그래스(Progress Bar)는 수평으로만 진행되므로 수직으로 작업경과를 보여줄 수 없다.


레이아웃은 굉장히 간단하다.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<lkcompany.exercise.RainbowProgress
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="5px"
android:paddingTop="10px"
android:paddingRight="5px"
android:paddingBottom="10px"
/>
<Button
android:id="@+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start"
/>
</LinearLayout>



public class ExerciseExam extends AppCompatActivity {
RainbowProgress mProgress;
Handler mHandler;

public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_excercise_exam);

mProgress = (RainbowProgress)findViewById(R.id.progress);

Button btn = (Button)findViewById(R.id.start);
btn.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v) {
if (mProgress.getPos() == 0) {
mProgress.setPos(0);
mHandler.sendEmptyMessage(0);
}
}
});

mHandler = new Handler() {

        // (복습) 핸들러(Handler) : 다른 객체가 보낸 메시지를 수신, 처리하는 객체


public void handleMessage(Message msg) {
int Pos;
Pos = mProgress.getPos(); mProgress의 현재 위치를 "조사"함.


if (Pos < mProgress.getMax()) {
mProgress.setPos(Pos+1); // 프로그래스바의 맥스에 도달하지 않았으면

// (int) Pos값을 +1로 "설정함"


mHandler.sendEmptyMessageDelayed(0,100); 100이면 100 / 1000 = 0.1초

// sendEmptyMessageDelayed ( int what , long delayMillis )


} else {
Toast.makeText(ExerciseExam.this, "Completed", 0).show();
mProgress.setPos(0); // 프로그래스바의 맥스에 도달했으면

토스트메시지 "Completed"를 띄운다.
}
}
};
}
}

class RainbowProgress extends View {
int mMax;
int mPos;
int mProgHeight;
LinearGradient mShader;

public RainbowProgress(Context context, AttributeSet attrs, int defStyle) { // 생성자 1

super(context, attrs, defStyle);
init();
}

public RainbowProgress(Context context, AttributeSet attrs) {               // 생성자 2

super(context, attrs);
init();
}

public RainbowProgress(Context context) {                                   // 생성자 3

super(context);
init();
}

void init() {
mMax = 100;                    // 범위
mPos = 0;                     // 범위 내의 현재값을 기억하는 변수
}

void setMax(int aMax) {
if (aMax > 0) {
mMax = aMax;
invalidate();
}
}

int getMax() { return mMax; }

void setPos(int aPos) {
if (aPos < 0 || aPos > mMax) {
return;
}
mPos = aPos;
invalidate();
}

int getPos() { return mPos; }

protected void onDraw(Canvas canvas) { // 프로그래스 막대를 그리는 코드


// 뷰의 onDraw 메서드는 화면에 그리기를 수행할 때 호출되며

// 그리기 위해서는 종이에 해당하는 Canvas가 인수로 전달된다.


if (mShader == null) {
mProgHeight = getHeight() - getPaddingTop() - getPaddingBottom();
int[] colors = { Color.RED, Color.YELLOW, Color.GREEN, Color.BLUE };
mShader = new LinearGradient(0,0,0,mProgHeight,                                           colors, null, Shader.TileMode.CLAMP);
}

// LinearGradient (float x0, float y0, float x1, float y1,

int color0, int color1, Shader.TileMode tile) //

ㄴㅅ//



RectF rt = new RectF(); // 직사각형 객체 생성
rt.left = getPaddingLeft(); // rt의 왼쪽변
rt.right = getWidth() - getPaddingRight(); // rt의 오른쪽 변
rt.bottom = getPaddingTop() + mProgHeight; / rt의 밑변
rt.top = rt.bottom - mProgHeight * mPos / mMax; // rt의 윗변

Paint fillpnt = new Paint(); // Canvas가 '종이'라면, Paint 객체는 '붓'이다.
fillpnt.setShader(mShader);
canvas.drawRect(rt, fillpnt);

rt.top = getPaddingTop();


Paint outpnt = new Paint(); // 직사각형 바깥 부분을 흰색으로 그리는 담당
outpnt.setColor(Color.WHITE); 색깔 : WHITE
outpnt.setStyle(Paint.Style.STROKE); 그리기 스타일 : STOKE
canvas.drawRect(rt, outpnt); // rt 및 outpnt에서 설정한 값을 전달



// (복습) 모든 그리기(draw) 메서드의 마지막 인수는 색상, 글꼴, 스타일, 그리기 모드를 지정하는

Paint 객체이다. 위 코드에서는 outpnt의 이름으로 객체를 생성했다.

outpnt에서 색깔(setColor)와 스타일(setStyle)을 정해주었으니, 설정한 모양으로 그려질 것이다.



}

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int Width = 48, Height = 200; // 기본 폭(48), 높이(200)으로 지정

switch (MeasureSpec.getMode(widthMeasureSpec)) { // 폭(width)

case MeasureSpec.AT_MOST: // 레이아웃에서 wrap_content인 경우

부모가 AT_MOST로 상한값을 전달.

허가된 영역과 자신의 고유 크기 중 작은 값을 취한다.

Width = Math.min(MeasureSpec.getSize(widthMeasureSpec), Width);
break;
case MeasureSpec.EXACTLY: // EXACTLY인 경우 부모가 제안한 크기 사용

Width = MeasureSpec.getSize(widthMeasureSpec);
break;
}

switch (MeasureSpec.getMode(heightMeasureSpec)) { // 높이(height)

case MeasureSpec.AT_MOST:
Height = Math.min(MeasureSpec.getSize(heightMeasureSpec), Height);
break;
case MeasureSpec.EXACTLY:
Height = MeasureSpec.getSize(heightMeasureSpec);
break;
}

setMeasuredDimension(Width, Height);
}
}


복습 1. 마진과 패딩의 차이



Paint outpnt = new Paint();   // 직사각형 바깥 부분을 흰색으로 그리는 담당

        outpnt.setColor(Color.WHITE);                  //  색깔 : WHITE 

        outpnt.setStyle(Paint.Style.STROKE);           //  그리기 스타일 : STOKE

        canvas.drawRect(rt, outpnt);  // rt 및 outpnt에서 설정한 값을 전달