버튼 클릭 이벤트를 처리하는 몇 가지 방법. (Android Button Click Event)

2016. 4. 6. 10:46


1. Button의 클릭 이벤트 처리

안드로이드에서 Button 클릭 이벤트를 처리하기 위해서는 리스너(listener)의 개념과 구현 방식을 이해하고 있어야 합니다. 만약 리스너에 대한 내용을 처음 접하신다면, 이전 글 [안드로이드 버튼 기본 사용법 - Button 클릭에 대한 이벤트 처리]에서 리스너의 의미와 View 클래스의 리스너 구현 내용, 리스너 객체 생성과 이벤트 처리 함수 작성 내용을 설명하였으니 살펴보시기 바랍니다.


그런데 버튼 클릭 이벤트를 처리하기 위해 반드시 [안드로이드 버튼 기본 사용법 - Button 클릭에 대한 이벤트 처리]에서 설명한 방법만 사용해야 하는 것은 아닙니다. Java 코드에서 리스너를 생성하지 않고 처리하는 방법도 있고, 리스너를 사용하지만 리스너 객체를 다루는 방식을 달리 할 수도 있습니다. 아니면 인터페이스로 만들어져 있는 OnClickListener를 Activity에서 implements하여 이벤트를 처리할 수도 있죠.


어떤 방법을 사용할지는 코드를 작성하는 개발자의 취향(?)에 따라 결정되겠지만 상황에 따라 이벤트를 처리하는 방식을 달리 선택해야 하는 경우도 생기므로, 각 이벤트 처리 방식의 구현 절차와 장단점을 알아보겠습니다.

1.1 설명을 위한 공통 예제 화면 구성도

각 이벤트 처리 방법에 대한 설명을 위해 공통적으로 사용할 예제 화면 구성도는 아래와 같습니다.


안드로이드 버튼 클릭 이벤트 예제


세 개의 Button은 각각 Red, Green, Blue라는 텍스트를 가집니다. 각 Button이 클릭되면 자신의 텍스트에 따라 TextView의 배경 색상을 변경합니다.


이벤트 처리를 위한 대부분의 작업들은 Java 코드에서 이루어지므로 아래의 Layout 리소스 XML을 공통적으로 사용하겠습니다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.recipes4dev.examples.buttoneventexample1.MainActivity"
    tools:showIn="@layout/activity_main">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/textView1"
        android:text="Color"
        android:textSize="30sp"
        android:textAlignment="center" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_below="@id/textView1">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/buttonRed"
            android:text="RED"
            android:layout_weight="1" />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/buttonGreen"
            android:text="GREEN"
            android:layout_weight="1" />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/buttonBlue"
            android:text="BLUE"
            android:layout_weight="1" />
    </LinearLayout>
</RelativeLayout>

2. 이벤트 처리를 위한 몇 가지 방법

2.1 첫 번째 방법 : 익명(Anonymous) 클래스를 생성하여 이벤트 리스너로 사용하기.

[안드로이드 버튼 기본 사용법 - Button 클릭에 대한 이벤트 처리]에서 Button의 setOnClickListener() 함수를 사용하여 이벤트 리스너를 지정할 때 익명 클래스를 사용하는 방법을 설명하였습니다.

익명 클래스를 사용한 Button 클릭 이벤트 처리.
    Button button1 = (Button) findViewById(R.id.button1) ;
    button1.setOnClickListener(new Button.OnClickListener() {
        @Override
        public void onClick(View view) {
            // TODO : click event
        }
    });

익명 클래스를 사용하는 방법은 특정 Button의 클릭 이벤트가 어디서 처리되는지 직관적으로 확인할 수 있고 코드 작성이 간결하기 때문에 가장 자주 사용되는 방법입니다. 익명 클래스를 사용하여 예제 버튼들에 대한 이벤트를 처리하는 코드는 다음과 같습니다.

[방법-1] 익명(Anonymous) 클래스를 생성하여 이벤트 리스너로 사용.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ... 코드 계속

        final TextView textView1 = (TextView) findViewById(R.id.textView1);

        Button buttonRed = (Button) findViewById(R.id.buttonRed) ;
        buttonRed.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                textView1.setText("Red") ;
                textView1.setBackgroundColor(Color.rgb(255, 0, 0));
            }
        }) ;

        Button buttonGreen = (Button) findViewById(R.id.buttonGreen) ;
        buttonGreen.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                textView1.setText("Green") ;
                textView1.setBackgroundColor(Color.rgb(0, 255, 0));
            }
        }) ;

        Button buttonBlue = (Button) findViewById(R.id.buttonBlue) ;
        buttonBlue.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                textView1.setText("Blue") ;
                textView1.setBackgroundColor(Color.rgb(0, 0, 255));
            }
        }) ;
    }
}

하지만 익명 클래스를 사용하는 방법은 익명 클래스가 가지는 특징으로 인해 몇 가지 단점이 존재합니다.


먼저 Button을 하나만 사용할 때는 간편하게 사용할 수 있지만 Button의 개수가 늘어나면 Button의 개수만큼 익명 클래스 객체를 생성해야 하는 문제가 생깁니다. 늘어나는 코드의 양은 감수한다 하더라도 연관된 기능을 수행하는 Button들의 이벤트 핸들러 함수가 각각의 익명 클래스 객체에 정의됨으로 인해 전혀 상관없는 별개의 Button들로 인식될 소지가 있는 문제가 있죠.


또 다른 문제는 익명 클래스에 정의된 이벤트 핸들러에서 익명 클래스 외부에 있는 변수를 사용하려면 변수를 선언할 때 반드시 final 키워드를 사용해야 한다는 것입니다. 즉, Button 이벤트 리스너로 정의된 익명 클래스의 onClick() 함수에서 onCreate() 내에서 선언된 변수를 사용하는 경우를 말하는 것이죠.


위에서 작성한 코드에서 TextView 변수를 만들 때 "final" 키워드를 사용한 것을 볼 수 있습니다.

    final TextView textView1 = (TextView) findViewById(R.id.textView1);

만약 "final"을 붙이지 않으면 다음과 같은 에러가 발생합니다.

    error: local variable textView1 is accessed from within inner class; needs to be declared final

내부(inner) 클래스의 한 종류인 익명 클래스에서 클래스 외부의 변수를 사용하기 위해서는 외부 변수 앞에 "final"을 사용해야 합니다. 그에 따른 에러 메시지를 출력하는 것입니다.


"final" 키워드를 사용하여 변수를 선언하는 것이 마음에 들지 않는다면, 익명 클래스 외부에서 TextView의 참조를 가져오지 않고, onClick() 함수에서 TextView에 대한 참조를 가져오도록 만들면 됩니다.

    buttonRed.setOnClickListener(new Button.OnClickListener() {
        @Override
        public void onClick(View view) {
            TextView textView1 = (TextView) findViewById(R.id.textView1);

            textView1.setText("Red") ;
            textView1.setBackgroundColor(Color.rgb(255, 0, 0));
        }
    }) ;

정리하자면 "익명 클래스를 생성하여 이벤트 리스너로 사용하는 방법"은 다음과 같은 상황에서 사용할 것을 추천합니다.


  • Button의 개수가 적거나, Button들 간의 연관성이 적은 경우.
  • 이벤트 핸들러 함수 내에서 익명 클래스 외부의 변수를 참조하지 않는 경우.
  • 간단한 Button 클릭 이벤트 테스트 코드를 작성하는 경우.

2.2 두 번째 방법 : 생성해 놓은 익명(Anonymous) 클래스의 참조를 이벤트 리스너로 사용하기.

이벤트를 처리하는 두 번째 방법은 익명 클래스 객체를 먼저 만들어 놓고 그 객체를 모든 Button의 이벤트 리스너로 사용하는 것입니다. 기본적으로 익명 클래스를 사용한다는 점에서 첫 번째 방법과 유사하지만 익명 클래스 객체를 매번 생성하지 않는다는 차이가 있습니다. 또한 각 Button들에 대한 이벤트 처리 코드가 이벤트 리스너 객체 별로 구분되지 않고 이벤트 리스너 안의 핸들러 함수(onClick)에서 구분되는 점도 다릅니다.

[방법-2] 생성해 놓은 익명(Anonymous) 클래스의 참조를 이벤트 리스너로 사용.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ... 코드 계속

        Button.OnClickListener onClickListener = new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                TextView textView1 = (TextView) findViewById(R.id.textView1);
                switch (view.getId()) {
                    case R.id.buttonRed :
                        textView1.setText("Red") ;
                        textView1.setBackgroundColor(Color.rgb(255, 0, 0));
                        break ;
                    case R.id.buttonGreen :
                        textView1.setText("Green") ;
                        textView1.setBackgroundColor(Color.rgb(0, 255, 0));
                        break ;
                    case R.id.buttonBlue :
                        textView1.setText("Blue") ;
                        textView1.setBackgroundColor(Color.rgb(0, 0, 255));
                        break ;
                }
            }
        } ;
        Button buttonRed = (Button) findViewById(R.id.buttonRed) ;
        buttonRed.setOnClickListener(onClickListener) ;
        Button buttonGreen = (Button) findViewById(R.id.buttonGreen) ;
        buttonGreen.setOnClickListener(onClickListener) ;
        Button buttonBlue = (Button) findViewById(R.id.buttonBlue) ;
        buttonBlue.setOnClickListener(onClickListener) ;
    }
}

이렇게 미리 만들어놓은 익명 클래스 객체를 이벤트 리스너로 사용하는 방법은 매번 객체를 새로 만들지 않아도 되는 장점이 있지만 익명 클래스가 가지는 단점을 여전히 가진다는 것과, 경우에 따라서 이벤트가 어디서 처리되는지 첫 번째 방법에 비해 직관적이지 않은 단점이 있죠.


그래서 "생성해 놓은 익명 클래스의 참조를 이벤트 리스너로 사용하기"는 다음과 같은 경우에 사용할 것을 추천합니다.


  • Button이 여러 개 존재하고 Button들 간의 연관성이 많은 경우.
  • 이벤트 핸들러 함수 내에서 익명 클래스 외부의 변수를 참조하지 않는 경우.
  • 추후 또 다른 Button을 추가하여 사용할 가능성이 높은 경우.

2.3 세 번째 방법 : 이벤트 리스너 인터페이스를 implements하는 이벤트 리스너 클래스 생성하기.

위에서 익명 클래스를 사용하는 방법을 살펴보았는데, 익명 클래스와 관련하여 종종 잘 못 이해되는 내용에 대해 짚고 넘어가겠습니다. 익명 클래스를 생성할 때 작성한 코드를 다시 한번 살펴보겠습니다.

    Button button1 = (Button) findViewById(R.id.button1) ;
    button1.setOnClickListener(new Button.OnClickListener() {
        @Override
        public void onClick(View view) {
            // TODO : click event
        }
    });

코드를 보면 new 키워드 다음에 Button의 이벤트 리스너 인터페이스의 이름이 적혀 있습니다. 이로 인해 마치 Button.OnClickListener의 객체를 만드는 것으로 잘 못 이해되는 경우가 종종 있습니다. 하지만 저 문장은 Button.OnClickListener의 객체를 만드는 것이 아닙니다.

정확히는 Button.OnClickListener를 상속받는 새로운 클래스의 객체가 생성되는 것입니다. 클래스와 객체에 이름이 지정되지 않은 익명(anonymous) 상태로 말이죠.


그런데 만약 익명 클래스를 사용함으로 인해 코드 상에서 생략되었던, "Button.OnClickListener를 상속받는 새로운 클래스의 정의를 명시적으로 수행"해주면 어떨까요? 그것이 바로 버튼 클릭 이벤트를 처리하는 세 번째 방법입니다.

[방법-3] 이벤트 리스너 인터페이스를  implements하는 이벤트 리스너 클래스 생성하기.
public class MainActivity extends AppCompatActivity {

    class BtnOnClickListener implements Button.OnClickListener {
        @Override
        public void onClick(View view) {
            TextView textView1 = (TextView) findViewById(R.id.textView1);
            switch (view.getId()) {
                case R.id.buttonRed :
                    textView1.setText("Red") ;
                    textView1.setBackgroundColor(Color.rgb(255, 0, 0));
                    break ;
                case R.id.buttonGreen :
                    textView1.setText("Green") ;
                    textView1.setBackgroundColor(Color.rgb(0, 255, 0));
                    break ;
                case R.id.buttonBlue :
                    textView1.setText("Blue") ;
                    textView1.setBackgroundColor(Color.rgb(0, 0, 255));
                    break ;
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ... 코드 계속

        // BtnOnClickListener의 객체 생성.
        BtnOnClickListener onClickListener = new BtnOnClickListener() ;

        // 각 Button의 이벤트 리스너로 onClickListener 지정.
        Button buttonRed = (Button) findViewById(R.id.buttonRed) ;
        buttonRed.setOnClickListener(onClickListener) ;
        Button buttonGreen = (Button) findViewById(R.id.buttonGreen) ;
        buttonGreen.setOnClickListener(onClickListener) ;
        Button buttonBlue = (Button) findViewById(R.id.buttonBlue) ;
        buttonBlue.setOnClickListener(onClickListener) ;
    }
}

이름이 부여된 이벤트 리스너 클래스가 만들어짐으로써 버튼 이벤트가 어디서 처리되는지 조금 더 명확하게 구분됨을 확인할 수 있습니다.

"이벤트 리스너 인터페이스를 implements하는 이벤트 리스너 클래스 생성하기"를 사용하려 한다면 아래의 내용을 참고하세요.


  • 명시적으로 이벤트 리스너 인터페이스를 상속하여 만듦으로 코드의 가독성을 높이고 싶은 경우.
  • 추후 또 다른 Button을 추가하여 사용할 가능성이 높은 경우.


2.4 네 번째 방법 : Activity에서 이벤트 리스너 implements해서 사용하기.

위에서 설명한 세 가지 방법은 Button.OnClickListener의 익명 클래스를 사용하거나 명시적인 상속 클래스를 정의하는 등의 이벤트 처리 과정에 딱히 Activity가 관여할 일이 없었습니다. 즉, Button.OnClickListener를 implements한 새로운 클래스를 정의하고 객체를 생성하기 위해 Activity에 상속 관계를 정의하거나 멤버 함수를 추가할 필요는 없다는 거죠.


그런데.. 여기서 잠깐! 이벤트 리스너를 만들기 위해 무조건 새로운 클래스를 정의(익명클래스 포함)하고 그 객체를 생성해야 하는 것일까요? 이미 만들어진 Activity에서 그 역할을 대신 수행하면 안되는 걸까요?


아주 기초적인 내용으로 돌아가서, 어떤 클래스가 Button의 클릭 이벤트를 처리하는 이벤트 리스너가 되기 위한 필요 조건은 새로운 클래스를 정의하거나 객체를 생성하는 것이 아니라, Button.OnClickListener 인터페이스를 implements하면 된다는 것입니다. Activity가 그 조건을 만족하면 Button의 클릭 이벤트에 대한 리스너 역할을 수행할 수 있는 거죠.


다음과 같이 Activity에 이벤트 리스너의 자격을 부여할 수 있습니다.

[방법-4] Activity에서 이벤트 리스너 인터페이스를 implements하기
public class MainActivity extends AppCompatActivity implements Button.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ... 코드 계속.

        // 각 Button의 이벤트 리스너로 this(MainActivity) 지정.
        Button buttonRed = (Button) findViewById(R.id.buttonRed) ;
        buttonRed.setOnClickListener(this) ;
        Button buttonGreen = (Button) findViewById(R.id.buttonGreen) ;
        buttonGreen.setOnClickListener(this) ;
        Button buttonBlue = (Button) findViewById(R.id.buttonBlue) ;
        buttonBlue.setOnClickListener(this) ;
    }

    // Button.OnclickListener를 implements하므로 onClick() 함수를 오버라이딩.
    @Override
    public void onClick(View view) {
        TextView textView1 = (TextView) findViewById(R.id.textView1);
        switch (view.getId()) {
            case R.id.buttonRed :
                textView1.setText("Red") ;
                textView1.setBackgroundColor(Color.rgb(255, 0, 0));
                break ;
            case R.id.buttonGreen :
                textView1.setText("Green") ;
                textView1.setBackgroundColor(Color.rgb(0, 255, 0));
                break ;
            case R.id.buttonBlue :
                textView1.setText("Blue") ;
                textView1.setBackgroundColor(Color.rgb(0, 0, 255));
                break ;
        }
    }
}

Button의 setOnClickListener() 함수 호출 시, this 키워드가 사용된 것을 확인하시기 바랍니다. Activity 자신이 Button.OnClickListener를 implements했으며 onClick() 함수를 오버라이딩 하였기에 이벤트 리스너 역할을 수행할 수 있다는 것을 this로 알려주는 것이죠.


"Activity에서 이벤트 리스너 implements해서 사용하기" 방법은 다음과 같은 경우에 사용할 것을 추천합니다.

  • 이벤트 핸들러 함수에서 많은 수의 Activity 멤버를 액세스해야 하는 경우.
  • Activity내부의 Button들에 대한 클릭 이벤트를 한 곳에서 처리하고 싶은 경우.
  • 익명클래스 또는 별도의 이벤트 리스너를 만들고 싶지 않은 경우.

2.5 다섯 번째 방법 : Layout 리소스 XML에서 Button의 onClick 속성을 이용하는 방법.

마지막으로 설명할 방법은 모든 이벤트 리스너 관련 처리를 Java 코드에서 작성하는 것이 아니라, Layout 리소스 XML과 Java 코드를 같이 사용하는 방법입니다.


사용하는 절차는 아주 간단합니다. 먼저 Activity의 Layout 리소스 XML에 Button을 추가할 때 onClick 속성을 사용하여 이벤트 핸들러 함수를 지정합니다.

[방법-5-1] Layout 리소스 XML의 Button에 onClick 속성 사용.
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/buttonRed"
        android:text="RED"
        android:layout_weight="1"
        android:onClick="onButtonClick" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/buttonGreen"
        android:text="GREEN"
        android:layout_weight="1"
        android:onClick="onButtonClick" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/buttonBlue"
        android:text="BLUE"
        android:layout_weight="1"
        android:onClick="onButtonClick" />

그 다음 onClick 속성에 지정한 핸들러 함수와 똑같은 이름의 함수를 Activity의 멤버 함수로 추가합니다. 단, 함수의 형식은 반드시 "public void XXX(View v)"이어야 합니다.

[방법-5-2] Button의 onClick 속성에 지정한 함수 정의.
public class MainActivity extends AppCompatActivity {
    // ... 코드 계속

    public void onButtonClick(View view) {
        TextView textView1 = (TextView) findViewById(R.id.textView1);
        switch (view.getId()) {
            case R.id.buttonRed :
                textView1.setText("Red") ;
                textView1.setBackgroundColor(Color.rgb(255, 0, 0));
                break ;
            case R.id.buttonGreen :
                textView1.setText("Green") ;
                textView1.setBackgroundColor(Color.rgb(0, 255, 0));
                break ;
            case R.id.buttonBlue :
                textView1.setText("Blue") ;
                textView1.setBackgroundColor(Color.rgb(0, 0, 255));
                break ;
        }
    }
}

앞서 설명한 방법들보다 좀 더 간단하고 직관적인 사용법이지만 별로 추천하고 싶지는 않습니다. 이유는 앱을 개발할 때 개발자가 작성하는 코드의 역할 이슈 때문입니다.

일반적으로 화면에 보여지는 UI 구성은 Layout 리소스 XML에서 담당하고, 각 구성 요소들의 이벤트 처리 및 제어는 Java 코드에 구현합니다. 그런데 Button의 onClick속성을 사용하게되면 Layout 리소스 XML에서 이벤트 처리에 관여하게 됨으로 역할 구분이 모호해지게 됩니다. 또한 코드 양이 많아지고 복잡해지면 Button과 이벤트 핸들러의 관계를 파악하기가 용이하지 않다는 단점도 있죠.


하지만 역시 아주 간단하게 이벤트를 처리할 수 있다는 것은 큰 장점입니다. 그래서 아래와 같은 상황에서 이 방법을 사용할 것을 추천합니다.

  • Button의 개수가 적은 경우.
  • 간단한 Button 클릭 이벤트 테스트 코드를 작성하는 경우.
  • 이벤트 리스너를 별도로 작성하는 게 번거롭게 느껴지는 경우.
  • 가장 간단하고 직관적인 방법을 선호하는 경우.

3. Button 예제 실행 화면

앞 서 살펴본 다섯 가지 방법은 모두 동일한 화면을 표시하며 버튼을 눌렀을 때 수행하는 기능도 같습니다. 어떤 방법을 적용할 것인지는 본인의 코딩 스타일에 따라 적절하게 결정하면 되겠죠.


예제는 아래와 같이 동작합니다.

4. 참고

.END.


ANDROID 프로그래밍/BUTTON