본문 바로가기
깡샘 코틀린

08-2 뷰 이벤트

by 농농씨 2023. 6. 23.

액티비티의 화면은 TextView, EditText, ImageView, Button 등의 뷰로 화면을 구성하고 구현한다. 이런 뷰를 사용자가 터치했을 때 이벤트 처리는 앞에서 살펴본 터치 이벤트를 이용하지 않는다. 각 뷰에서 이벤트를 별도로 제공한다

 

❓뷰의 이벤트도 결국 터치 아닌가요? 왜 터치 이벤트로 처리하지 않고 뷰에서 별도로 이벤트를 제공하죠?

❗️한 화면에 뷰가 많으면 뭘 터치했는지 좌표값을 알아야 한다(체크박스인지 버튼인지 등). 그래서 뷰가 많을 때는 프로그래밍이 복잡해진다. 그 대신 각 뷰가 제공하는 별도의 이벤트, 예를들어 버튼은 ClickEvent, 체크박스는 CheckdChangeEvent, 리스트는 ItemClickEvent로 처리하면 더 간단하고 명료해진다. 따라서 대부분의 뷰는 해당 뷰에 맞는 이벤트를 따로 제공하며 터치 이벤트로 처리하지 않는다. 

 

뷰 이벤트의 처리 구조

뷰 이벤트는 일정한 구조에 따라 처리된다. 앞에서 다룬 '터치' 이벤트는 이벤트 콜백 함수인 onTouchEvent()만 액티비티에 선언해 놓으면 처리할 수 있다.  또한 키 이벤트는 이벤트 콜백함수인 onKeyDown()만 액티비티에 선언해 놓아도 이벤트를 처리할 수 있다. 그런데 뷰 이벤트는 이벤트 콜백 함수만 선언해서는 처리할 수 없다

뷰 이벤트는 이벤트 소스(event source)와 이벤트 핸들러(event handler)로 역할이 나뉘며 이 둘을 리스너(listener)로 연결해야 이벤트를 처리할 수 있다.

  • 이벤트 소스: 이벤트가 발생한 객체
  • 이벤트 핸들러: 이벤트 발생 시 실행할 로직이 구현된 객체
  • 리스너: 이벤트 소스와 이벤트 핸들러를 연결해주는 함수

즉, 이벤트 소스에 리스너로 이벤트 핸들러를 등록해 놓으면 이벤트가 발생할 때 실행되는 구조이다.

다음코드는 체크박스의 체크상태가 변경될 때의 이벤트 처리 예시이다.

여기서 checkbox 객체가 이벤트가 발생하는 이벤트 소스이며, 이벤트 처리 내용이 담긴 이벤트 핸들러OnCheckedChangeListener 인터페이스를 구현한 객체이다.

// 체크박스 이벤트 처리
binding.checkbox.setOnCheckedChangeListener(object: CompoundButton.OnCheckedChangeListener {
// checkbox가 이벤트소스, setOnCheckedChangeListener가 리스너(이벤트 핸들러 등록 역할), object가 이벤트 핸들러
	override fun onCheckedChanged(p0: CompoundButton?, p1: Boolean) {
    	Log.d("kkang", "체크박스 클릭")
    }
})

대부분 이벤트 핸들러는 이름 형식이 OnXXXListener인 인터페이스를 구현해서 만든다. 대표적으로 OnClickListener, OnLongClickListener, OnItemClickListener 등의 인터페이스를 제공한다. 자세한 내용은 다음에.

위 코드에서는 이벤트 핸들러를 이벤트 소스에 등록하기 위해 리스너인 'setOnCheckedChangeListener()'라는 함수를 이용했다.

1️⃣인터페이스를 구현한 object 클래스를 이벤트 핸들러로 만들었지만,2️⃣ 액티비티 자체에서 인터페이스를 구현할 수도 있다. 또한 3️⃣이벤트 핸들러를 별도의 클래스로 만들어 처리할 수도 있으며 4️⃣ 코틀린의 SAM*기법을 이용할 수도 있다.

*SAM(single abstract method)이란 코틀린 코드에서 자바 인터페이스를 간단하게 사용하기 위해 제공하는 기법이다.

2,3,4의 세가지 예를 차례로 살펴보면 다음과 같다. 어떻게 작성하든 지정된 인터페이스를 구현한 객체를 이벤트 핸들러로 등록한다는 점은 같다.

// 액티비티에서 인터페이스를 구현한 예
class MainActivity3 : AppCompatActivity(), CompoundButton.OnCheckedChangeListener {
	//CompoundButton.OnCheckedChangeListener 주목
	override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
        val binding = ActivityMain3Binding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.checkbox.setOnCheckedChangeListener(this) // 주목
    }
    override fun onCheckedChanged(p0: CompoundButton?, p1: Boolean) {
    	Log.d("kkang", "체크박스 클릭")
    }
}

솔직히 이해안됨 뭐 그런갑다 해야지

binding이 뭐지

// 이벤트 핸들러를 별도의 클래스로 만든 예
class MyEventHandler : CompoundButton.OnCheckedChangeListener {
	override fun onCheckedChanged(p0: compoundButton?, p1: Boolean) {
    	Log.d("kkang", "체크박스 클릭")
    }
}
class MainActivity3 : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
        val binding = ActivityMain3Binding.inflate(layoutInflater)
        setOnContentView(binding.root)
        
        binding.checkbox.setOnCheckedChangeListener(MyEventHandler())
        // 클래스 호출함
    }
}
// SAM 기법으로 구현한 예
class MainActivity3 : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
        
        val binding = ActivityMain3Binding.inflate(layoutInflater)
        setContentView(binding.root)
        
        binding.checkbox.setOnCheckedChangeListener { // SAM 기법 사용됨
        	compoundButton, b ->
            Log.d("kkang", "체크박스 클릭")
        }
    }    
}

 

 

클릭과 롱클릭 이벤트 처리

안드로이드에서 뷰가 아무리 많아도 이벤트 처리 구조는 같으므로 이벤트 소스와 이벤트 핸들러를 리스너로 연결하는 구조만 이해한다면 어떤 뷰 이벤트라도 처리할 수 있다.

여기서는 대표적으로 뷰를 짧게 클릭할 때 발생하는 ClickEvent와 길게 클릭할 때 발생하는 LongClickEvent를 알아볼 것이다.

 

ClickEvent, LongClickEvent는 뷰의 최상위 클래스인 View에 정의된 이벤트이다. 즉, 가장 기초이면서 많이 사용하는 이벤트이다. 두 이벤트의 핸들러는 다음과 같다.

  • open fun setOnClickListener(l: View.OnClickListener?): Unit
  • open fun setOnLongClickListener(l: View.OnLongClickListener?): Unit

ClickEvent는 OnClickListener를 구현한 객체를 이벤트 핸들러로 등록해야 하고 LongClickListener는 OnLongClickListener를 구현한 객체를 이벤트 핸들러로 등록해야 한다ㅏ. 

// 버튼의 클릭, 롱클릭 이벤트 처리
binding.button.setOnClickListener {
	Log.d("kkang", "클릭 이벤트")
}
binding.button.setOnLongClickListener {
	Log.d("kkang", "롱클릭 이벤트")
    true // (반환값을 의미한다 LongClickEvent의 콜백 함수는 Boolean 타입의 반환값을 가지기 떄문이다.)
    // (람다함수라서 return 키워드 안쓰고 그냥 마지막줄 값을 반환한다.)
}

앞에서 설명한 뷰 이벤트의 처리 구조를 보면 이벤트 핸들러는 SetOnClickListener() 함수로 등록하고 여기에 setOnClickListener에 람다 함수를 매개변수로 지정한 것처럼 보인다.

예를 들어 이벤트 핸들러를 자바로 작성한다면 다음처럼 작성할 수 있다.

// 자바로 작성한 이벤트 핸들러
binding.btn.setOnClickListener(new View.OnClickListener() {
	@Override
    public boid onClick(View view) {
    }
});

이 자바 코드를 그대로 코틀린으로 전환한다면 다음처럼 작성할 수 있다

// 코틀린으로 작성한 이벤트 핸들러
binding.btn.setOnClickListener(object: View.OnClickListener {
	override fun onClick(p0: View?) {
    }
})

그런데 코틀린의 SAM 기법을 이용하면 이 코드를 조금 더 간단하게 작성할 수 있다. SAM은 자바 API를 코틀린에서 활용할 때 람다 표현식으로 쉽게 이용할 수 있게 해주는 기법이다. SAM은 단어 뜻(single abstract method) 그대로 하나의 추상함수를 포함하는 인터페이스를 활용하는 방법이다.

인터페이스가 자바에 작성되어 있고 그 인터페이스를 등록하는 세터(setter)함수도 자바에 작성되어 있으면 코틀린에서 세터 함수를 이용해 인터페이스를 구현한 객체를 등록할 때 람다 함수로 쉽게 등록할 수 있다. 예를 들어 자바에 다음처럼 선언된 인터페이스가 있다고 가정해 보자.

// 자바 인터페이스
public interface JavaInterface1 {
	void callback();
}

그리고 이 인터페이스 타입의 객체를 등록하는 함수도 다음처럼 자바에 선언되었다고 가정해보자.

// 자바 함수
public class SAMTest {
	JavaInterface1 callback;
    public void setInterface(JavaInterface1 callback) {
    	this.callback = callback;
    }
}

이처럼 자바함수인 setInterface()를 코틀린에서 이용하려면 인터페이스를 구현한 객체를 매개변수로 지정해야 한다. 따라서 다음처럼 작성할 수 있다.

// 코틀린에서 자바 함수 사용
obj.setinterface(object: JavaInterface1 {
	override fun callback() {
    	println("hello kotlin")
    }	
})

그런데 이 코드는 SAM 기법을 이용하면 다음처럼 더 간결하게 작성할 수 있다.

// SAM 기법을 이용한 자바 함수 호출
obj.setInterface { println("hello SAM") }

물론 자바 인터페이스를 구현한 객체를 모두 SAM 기법으로 이용할 수 있는 것은 아니다. 추상 함수 하나를 포함하는 인터페이스만 SAM 기법으로 이용할 수 있다. 코틀린에서는 이러한 SAM 기법을 이용해 많은 이벤트 핸들러 코드를 다음처럼 작성한다.

// SAM 기법을 이용한 이벤트 핸들러 구조
binding.btn.setOnClickListener {
	(...생략...)
}