본문 바로가기
코딩일기/android studio

Android 독학 10일차 : 커스텀 뷰(Custom View) 만들기

by 욱파이어니어 2021. 3. 18.
728x90
반응형

우리가 위젯을 사용하다 보면 안드로이드에서 제공해주는 위젯을 사용할 때도 있지만 원하는 위젯이 없는 경우에는

직접 위젯을 만들어야 하는 경우가 생긴다. 

그리고 이미 존재하는 위젯에서 뭔가 다른 이벤트를 주고 싶을때도 우리는 커스텀해서 만들어야 한다.

 

커스텀 뷰는 그럼 어떻게 만드냐 그 방법은 기존의 View를 extends(상속) 받아서 사용해야 한다.

 

내가 지금 현재 독학하고 있는 

'그림으로 쉽게 설명하는 안드로이드 프로그래밍' 이라는 책에 나오는 예제를 통해서 커스텀 뷰에 대해서 설명을 하겠다.

 

해당 예제는 볼륨 컨트롤러 같은것으로 별점을 매기는 형태이다.

 

일단 xml 로 어떤 화면이 구성될 것인지를 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:context=".MainActivity">
 
    <wook.co.kr.VolumeControll
        android:id="@+id/vc"
        android:layout_width="300px"
        android:layout_height="300px"
        android:layout_gravity="center"/>
    <RatingBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/vc"
        android:layout_alignParentLeft ="true"
        android:layout_alignParentStart ="true"
        android:layout_marginLeft="13dp"
        android:layout_marginStart="13dp"
        android:layout_marginTop="36dp"
        android:id="@+id/ratingBar"/>
</LinearLayout>
cs

일단 xml부터 커스텀 뷰라서 그런지 생전 처음 보는 View가 보이는 것을 알 수가 있다.

 

일단 <wook.co.kr.VolumeControll>이라는 것은 내가 기존의 ImageView를 상속받아서 커스텀한 클래스가

위치한 곳이다.

 

xml의 화면은 대충 이러하다.

xml 화면

xml 소스에는 이미지의 src도 존재하지 않는데도 사진이 나오는 이유는 위에서도 언급했듯 내가 

기존의 ImageView 클래스를 상속받아 커스텀한 클래스 파일에 정의되어 있기 때문이다.

 

그럼 이제 커스텀한 VolumeControll이라는 클래스에 대해서 알아보자.

 

@SuppressLint("AppCompatCustomView")
public class VolumeControll extends ImageView implements View.OnTouchListener {

 	
    private double angle = 0.0; //각도를 저장할 변수
    private KnobListener listener; //해당 뷰에서 이벤트를 감지할 리스너 객체 생성
    float x,y; //터치한 지점의 x와 y의 좌표
    float mx, my; //회전 각도를 위한 값

    public VolumeControll(Context context) {
        super(context);
        this.setImageResource(R.drawable.knob);
        this.setOnTouchListener(this);
    }

    public VolumeControll(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.setImageResource(R.drawable.knob);
        this.setOnTouchListener(this);
    }

    public interface KnobListener { //커스텀 뷰에 있는 이벤트 리스너 인터페이스를 만듬
        public void onChanged(double angle);
    }

    public void setKnobListener(KnobListener lis){ //리스너를 설정하는 부분
        listener = lis;
    }


    private double getAngle(float x, float y){ //현재 위치의 각도를 구해서 반환하는 함수
        mx = x - (getWidth() / 2.0f); //해당 뷰의 width가져와서 2.0으로 나눔
        my = (getHeight() / 2.0f) - y; //해당 뷰의 height가져와서 2.0으로 나눔
        double degree = Math.atan2(mx, my) * 180.0 / 3.141592; //두 정 사이의 각도 구하는 부분
        return degree;
    }
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) { //터치됐을때 실행하는 콜백 함수
        x = motionEvent.getX();
        y = motionEvent.getY(); //터치 이벤트의 x축고 y축 값을 받는다
        angle = getAngle(x,y); //x,y 값으로 각도를 받아서 angle로 받는다
        Log.v("각도",String.valueOf(angle));
        invalidate(); //onDraw 해서 이미지 뷰를 angle 만큼 돌린다
        listener.onChanged(angle); //angle 값을 보내 오버라이딩 하는 onChanged() 함수에서 ratingBar에 변화를 주게 한다.
        return true;
    }
    protected void onDraw(Canvas c){ // View의 내용을 돌리는 부분
        Paint paint = new Paint();
        //c.save();
        c.rotate((float)angle, getWidth() / 2, getHeight() / 2); //해당 이미지 뷰의 width와 height를 받아서 angle 만큼 뷰를 rotate한다.
        super.onDraw(c);
        //c.restore();
    }
}

해당 VolumeControll 클래스에 대한 설명을 하기 위해서 위에서부터 해당 코드가 어떤 건지 설명을 하겠다.

 

일단 자바 VolumeControll이라는 클래스의 초입부에 보면 

extends ImageView implements View.OnTouchListener 로 해서 기존의 ImageView를 상속받아

기존의 ImageView를 커스텀할 수 있게 했고 View.OntouchListener라는 리스너를 implements(구현)

해서 해당 이벤트가 발생했을 때 나타날 효과들을 정의할 수 있게 했다.

 

그리고 해당 변수들을 선언하였다.

 

    private double angle = 0.0; //각도를 저장할 변수
    private KnobListener listener; //해당 뷰에서 이벤트를 감지할 리스너 객체 생성
    float x, y; //터치한 지점의 x와 y의 좌표
    float mx, my; //회전 각도를 위한 값

 

이전 View의 생명주기에 관해 언급했듯 View가 생성되려면 생성자가 필요한데 

그 생성자 부분이 바로 

 

public VolumeControll(Context context) {
super(context);
this.setImageResource(R.drawable.knob);
this.setOnTouchListener(this);
}

public VolumeControll(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.setImageResource(R.drawable.knob);
this.setOnTouchListener(this);
}

 

이 부분이다.

 

해당 생성자가 뭘 뜻하는지 모르겠으면 내가 이전에 View의 생명주기에 관해 설명한 부분은 보면 될 것 같다.

wpioneer.tistory.com/26

 

Android 독학 9일차 : View 생명주기(View를 그리는 과정)

View의 생명주기에 대해서 공부를 하는 이유는 내가 이제 커스텀 View에 대해서 설명할 것이기 때문이다. 커스텀 View는 우리가 직접 View 클래스를 상속받아서 나만의 View를 만들거나 기존의 View에

wpioneer.tistory.com

 

생성자 부분에 대해 간략히 설명하자면 상속받은 ImageView 클래스에

해당 context(커스텀한 것)를 ImageView 생성자에 넘겨 생성할 수 있게 했고

this.setImageResource(R.drawable.knob); 해서 새로 커스텀한 VolumeControll 클래스의 이미지를

drawable 파일 안에 있는 knob으로 설정했다.

그리고 해당 뷰가 터치됐을 때 this.setOnTouchListener(this);로 해서 해당 뷰를 매개변수로 넘겨

터치 이벤트를 실행하도록 했다.

 

그다음에는 커스텀한 VolumeControll이라는 View에서 이벤트 리스너를 만들었다.

 

public interface KnobListener { //커스텀 뷰에 있는 이벤트 리스너 인터페이스를 만듦
public void onChanged(double angle);
}

 

해당 인터페이스는 onChanged(double angle); 만들도록 명시해놨다.

 

그다음은

public void setKnobListener(KnobListener lis){ //리스너를 설정하는 부분
listener = lis;
}

를 만들었는데 해당 메소드는

매개변수로 만든 KnobListener를 인자로 받아 해당 클래스에 있는 

private KnobListener listener; 변수에다가 집어넣어줬다.

(이 부분이 자바에서 getter/setter 부분에 해당되는 부분이 아닌가 싶다.)

 

 

그 밑에 있는 getAngle(), onTouch() 메소드는 나중에 설명하기로 하고

일단은 onDraw() 메소드에 대한 설명으로 넘어가겠다.

 

onDraw() 메소드는 내가 9일 차 때 생명주기에서 배웠듯 가장 마지막 단계에서 View를 그리는 단계이다.

그래서 우리는 해당 View를 그리는 것이기 때문에 이 부분에서 우리는 VolumeControll이 회전할 수 있게 만들어야 한다.

 

하지만 앱을 처음 만들었을 때 VolumeControll이 회전이 되지 않는 이유는

private double angle = 0.0; 로 설정했기 때문이다

(여기서 해당 angle의 접근 지정자가 private인 이유는 자바의 속성인 캡슐화 때문이다.)

 

그럼 여기까지 하면 이제 우리가 ImageView를 커스텀한 VolumeControll의 외형의 모습(사진 지정)은 다 그려졌다고 보면 된다.

 

그럼 이제 MainActivity 클래스에서 프로그래밍의 진행 순서대로 가면서 설명할 예정이다.

 

그럼 MainActivity로 넘어가 소스를 한번 확인해보자.

 

public class MainActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final RatingBar rb = (RatingBar)findViewById(R.id.ratingBar); //레이팅 바를 받아온다.
        VolumeControll view = (VolumeControll)findViewById(R.id.vc); //이부분에 널이 들어간거 같은데
        view.setKnobListener(new VolumeControll.KnobListener() {
            @Override
            public void onChanged(double angle) {
                float rating = rb.getRating();
                if(angle > 0 && rating < 7.0){
                    //오른쪽으로 회전
                    rb.setRating(rating+0.5f);
                    Log.v("현재 별점 ", String.valueOf(rb.getRating()));
                }else if(rating > 0.0){
                    //왼쪽으로 회전
                    rb.setRating(rating-0.5f);
                    Log.v("현재 별점 ", String.valueOf(rb.getRating()));
                }
            }
        });
    }
}

일단 activity_main.xml에서 

RatingBar를 가져왔다.

그리고 우리가 위에서 만든 커스텀 뷰인 VolumeControll의 객체를 생성해주었고 해당 객체는 

activity_main.xml에서 id가 vc인 애로 가져와서 넣어 주었다.

 

그다음에는 

view.setKnobListener()를 해서 매개 변수로 KnobListener를 무명 객체로 넘겨주어서

VolumeControll view의 private KnobListener listener 변수에 listener를 설정해주었다.

 

KnobListener를 무명 객체로 넘겨주려면 interface였던 KnobListner안에 있는 onChange() 메소드를 구체화시켜 주어야 한다.

그래서 무명 객체 안에 

            @Override
            public void onChanged(double angle) {
                float rating = rb.getRating();
                if(angle > 0 && rating < 7.0){
                    //오른쪽으로 회전
                    rb.setRating(rating+0.5f);
                    Log.v("현재 별점 ", String.valueOf(rb.getRating()));
                }else if(rating > 0.0){
                    //왼쪽으로 회전
                    rb.setRating(rating-0.5f);
                    Log.v("현재 별점 ", String.valueOf(rb.getRating()));
                }
            }

를 만들어서 구체화한 것이다.

 

그럼 이제 구체화된 onChange()메소드를 보자

 

해당 메소드는 angle이라는 매개변수를 받아 

angle과 RatingBar에서 가져온 별점의 수에 따라 RatingBar에 별점을 매기는 부분이다.

 

이로써 MainActivity에서는 VolumeControll view의 설정을 끝마치고 앱의 설정은 끝난 것이다.

 

그럼 getAngle(), onTouch() 메소드들은 언제 호출되는 건지 궁금할 것이다.

 

해당 메소드들은 우리가 이미 VolumeControll View를 생성했기 때문에 해당 뷰가 onTouch()되 있을 때 호출되는 메소드들이다.

 

그래서 우리가 MainActivity 클래스에서 만든 VolumeControll view가 터치됐을 때 어떻게 되는지 한번 보자.

 

해당 뷰가 터치되면 

곧바로 VolumeControll 클래스에 있던 

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) { //터치됐을때 실행하는 콜백 함수
        x = motionEvent.getX();
        y = motionEvent.getY(); //터치 이벤트의 x축고 y축 값을 받는다
        angle = getAngle(x,y); //x,y 값으로 각도를 받아서 angle로 받는다
        Log.v("각도",String.valueOf(angle));
        invalidate(); //onDraw 해서 이미지 뷰를 angle 만큼 돌린다
        listener.onChanged(angle); //angle 값을 보내 오버라이딩 하는 onChanged() 함수에서 ratingBar에 변화를 주게 한다.
        return true;
    }

가 실행이 된다.

 

왜냐하면 우리는 해당 클래스에서 View.OnTouchListener를 implements 했고 생성자에서 만들 때 this.setOnTouchListener(this);를했기때문이다.

 

그럼 이제 onTouch()메소드를 살펴보자.

onTouch()는 매개변수로 View와 MotionEvent를 받는다. MotionEvent은 사용자의 모션 이벤트에 대한 정보를 받기 위해서 이고 View는 해당 View에 대한 정보를 받기 위해서이다(View부분은 확실하지 않음)

 

그래서 사용자의 모션 이벤트의 x와 y의 값을 받아 getAngle() 함수를 호출한다.

    private double getAngle(float x, float y){ //현재 위치의 각도를 구해서 반환하는 함수
        mx = x - (getWidth() / 2.0f); //해당 뷰의 width가져와서 2.0으로 나눔
        my = (getHeight() / 2.0f) - y; //해당 뷰의 height가져와서 2.0으로 나눔
        double degree = Math.atan2(mx, my) * 180.0 / 3.141592; //두 정 사이의 각도 구하는 부분
        return degree;
    }

 

getAngle 함수는 해당 터치가 원래 있던 자리로부터의 각도가 얼마나 변했는지를 찾아 해당 각도를 반환해주는

부분이다.

(이 부분은 사실 어떻게 돌아가는지는 잘 모르겠다. 깊게 파보려고 했으나 시간낭비인 거 같아 일단은 넘긴다)

 

그럼 이제 다시 onTouch()로 넘어와서 

private double angle에 반환받은 값을 저장해서 invalidate()를 호출한다.

invalidate()가 뭔지 궁금하다면 내 독학 9일 차를 확인해보길 바란다.

 

wpioneer.tistory.com/26

 

Android 독학 9일차 : View 생명주기(View를 그리는 과정)

View의 생명주기에 대해서 공부를 하는 이유는 내가 이제 커스텀 View에 대해서 설명할 것이기 때문이다. 커스텀 View는 우리가 직접 View 클래스를 상속받아서 나만의 View를 만들거나 기존의 View에

wpioneer.tistory.com

그러고 나서는 MainActivity에서 세팅된 

view.setKnobListener(new VolumeControll.KnobListener() {
            @Override
            public void onChanged(double angle) {
                float rating = rb.getRating();
                if(angle > 0 && rating < 7.0){
                    //오른쪽으로 회전
                    rb.setRating(rating+0.5f);
                    Log.v("현재 별점 ", String.valueOf(rb.getRating()));
                }else if(rating > 0.0){
                    //왼쪽으로 회전
                    rb.setRating(rating-0.5f);
                    Log.v("현재 별점 ", String.valueOf(rb.getRating()));
                }
            }
        });

무명 객체의 onChanged()를 호출해서 RatingBar를 수정해준다.

 

그럼 이제 회전되는 VolumeControll이 보이고 그에 따라 별점도 수정이 될 것이다.

 

참고 서적

'그림으로 쉽게 설명하는 안드로이드 프로그래밍'

반응형