마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

리니어레이아웃에서 자식뷰 클릭하기

0 추천

https://ibb.co/p4fJrFf

이러한 이미지의 다이얼로그 창을 구성했습니다.

코틀린으로 변경해서 자바로 작성된 코드를 코틀린 공부할겸 변경중인데

해당 사진은 기존에 리사이클러뷰로 구성했습니다 그런데 아이템 갯수가 적어

재활용될 일이 없기에 리사이클러뷰는 사용하지 않고 단순히

무식하게 LInearLayout에 뷰하나하나 다 넣었습니다.

이제 앱에서 이 뷰들을 선택을 해야하고 값을 가져와야하는데 어떻게 선택해야할지 모르겠습니다.

뷰들을 하나하나 아이디를 가져와서 클릭이벤틀르 설정하면 너무 지저분하고 비효율적인 것같고

리사이클러뷰는 사용안하기로 했으니..

다른 방법이 딱히 떠오르지 않는데, 리니어 레이아웃에서 자식 뷰들을 중복 선택가능하게끔 가져오는

방법이 있을까요?

아 그리고 클릭이벤트에 데이터바인딩도 적용가능해서 한번 해볼생각입니다..

이 데이터 바인딩 방법이면 텍스트뷰 하나하나에 클릭이벤트를 적용해도 findViewByID 하지 않아도 되니

코드도 줄어들고괜찮은 방법일까요

 

아래는 제가 시도했던 방법인데 이렇게하니 리니어 전체가 클릭되는이벤트가 발생하네요..

class BodyPartDialogFragment : DialogFragment(), View.OnClickListener{
    private lateinit var ll: LinearLayout
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view: View = inflater.inflate(R.layout.fragment_body_part_dialog, container, false)
        ll = view.findViewById(R.id.ll_body_part)
        ll.setOnClickListener {
            onClick(it)
        }
        return view
    }
    override fun onClick(view: View?) {
        when (view?.id) {
            R.id.back -> Toast.makeText(context, "back", Toast.LENGTH_LONG)
            R.id.chest -> Toast.makeText(context, "chest", Toast.LENGTH_SHORT)
            R.id.leg -> Toast.makeText(context, "leg", Toast.LENGTH_SHORT)
            R.id.shoulder -> Toast.makeText(context, "shoulder", Toast.LENGTH_SHORT)
            R.id.bieceps -> Toast.makeText(context, "biceps", Toast.LENGTH_SHORT)
            R.id.triceps -> Toast.makeText(context, "triceps", Toast.LENGTH_SHORT)
            R.id.abs -> Toast.makeText(context, "abs", Toast.LENGTH_SHORT)
        }
    }
}

 

XML

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white">
        <TextView
            android:id="@+id/back"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text=" 등 "
            android:textSize="20dp"
            android:background="?attr/selectableItemBackground"
            android:gravity="center"
            android:padding="15dp" />

        <ImageView
            android:id="@+id/selection_state1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_dumbbell"
            android:backgroundTint="@android:color/transparent"
            android:layout_gravity="center_vertical|end"
            android:layout_marginRight="20dp"
            android:visibility="invisible"/>
    </FrameLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white">
        <TextView
            android:id="@+id/chest"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text=" 가 슴 "
            android:textSize="20dp"
            android:background="?attr/selectableItemBackground"
            android:gravity="center"
            android:padding="15dp" />

        <ImageView
            android:id="@+id/selection_state2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_dumbbell"
            android:backgroundTint="@android:color/transparent"
            android:layout_gravity="center_vertical|end"
            android:layout_marginRight="20dp"
            android:visibility="invisible"/>
    </FrameLayout>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white">
        <TextView
            android:id="@+id/leg"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text=" 하 체 "
            android:textSize="20dp"
            android:background="?attr/selectableItemBackground"
            android:gravity="center"
            android:padding="15dp" />

        <ImageView
            android:id="@+id/selection_state3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_dumbbell"
            android:backgroundTint="@android:color/transparent"
            android:layout_gravity="center_vertical|end"
            android:layout_marginRight="20dp"
            android:visibility="invisible"/>
    </FrameLayout>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white">
        <TextView
            android:id="@+id/shoulder"
            android:layout_width="300dp"
            android:layout_height="wrap_content"
            android:text=" 어 깨 "
            android:textSize="20dp"
            android:background="?attr/selectableItemBackground"
            android:gravity="center"
            android:padding="15dp" />

        <ImageView
            android:id="@+id/selection_state4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_dumbbell"
            android:backgroundTint="@android:color/transparent"
            android:layout_gravity="center_vertical|end"
            android:layout_marginRight="20dp"
            android:visibility="invisible"/>
    </FrameLayout>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white">
        <TextView
            android:id="@+id/bieceps"
            android:layout_width="300dp"
            android:layout_height="wrap_content"
            android:text=" 이 두 "
            android:textSize="20dp"
            android:background="?attr/selectableItemBackground"
            android:gravity="center"
            android:padding="15dp" />

        <ImageView
            android:id="@+id/selection_state"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_dumbbell"
            android:backgroundTint="@android:color/transparent"
            android:layout_gravity="center_vertical|end"
            android:layout_marginRight="20dp"
            android:visibility="invisible"/>
    </FrameLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white">
        <TextView
            android:id="@+id/triceps"
            android:layout_width="300dp"
            android:layout_height="wrap_content"
            android:text=" 삼 두 "
            android:textSize="20dp"
            android:background="?attr/selectableItemBackground"
            android:gravity="center"
            android:padding="15dp" />

        <ImageView
            android:id="@+id/selection_state6"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_dumbbell"
            android:backgroundTint="@android:color/transparent"
            android:layout_gravity="center_vertical|end"
            android:layout_marginRight="20dp"
            android:visibility="invisible"/>
    </FrameLayout>
</LinearLayout>

 

codeslave (3,940 포인트) 님이 2021년 5월 14일 질문
codeslave님이 2021년 5월 14일 수정

1개의 답변

0 추천

Child View에 해당하는 부분을 CustomView로 만드세요.

<FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white">
        <TextView
            android:id="@+id/triceps"
            android:layout_width="300dp"
            android:layout_height="wrap_content"
            android:text=" 삼 두 "
            android:textSize="20dp"
            android:background="?attr/selectableItemBackground"
            android:gravity="center"
            android:padding="15dp" />
 
        <ImageView
            android:id="@+id/selection_state6"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_dumbbell"
            android:backgroundTint="@android:color/transparent"
            android:layout_gravity="center_vertical|end"
            android:layout_marginRight="20dp"
            android:visibility="invisible"/>
    </FrameLayout>

 

안드로이드 스튜디오의 새로 만들기 메뉴를 이용하시면 커스텀뷰의 골격에 해당하는 부분을 생성해 줍니다.

layout_my_custom_view.xml

<merge 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:id="@+id/containerFrl"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    tools:ignore="RtlSymmetry"
    tools:parentTag="android.widget.FrameLayout">

        <TextView
            android:id="@+id/triceps"
            android:layout_width="300dp"
            android:layout_height="wrap_content"
            android:text=" 삼 두 "
            android:textSize="20dp"
            android:background="?attr/selectableItemBackground"
            android:gravity="center"
            android:padding="15dp" />
 
        <ImageView
            android:id="@+id/selection_state6"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_dumbbell"
            android:backgroundTint="@android:color/transparent"
            android:layout_gravity="center_vertical|end"
            android:layout_marginRight="20dp"
            android:visibility="invisible"/>
    </FrameLayout>

//커스텀 속성이 필요하다면 values/attrs.xml에 필요한 속성을 추가해 주고 커스텀뷰에서 처리해 줍니다.
values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyCustomView">
        <attr name="message" format="string" />
    </declare-styleable>

</resources>


class MyCustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    private var backClickListener: View.OnClickListener? = null
    private var stateClickLisetner: View.OnClickListener? = null

    private val backText: TextView by lazy { findViewById(R.id.back) }
    private val stateImage: ImageView by lazy { findViewById(R.id.selection_state1) }

    
    var message: CharSequence
         get() = backText.text
         set(value) {
             backText.text = value
         }

    init {
        LayoutInflater.from(context).inflate(R.layout.my_custome_view, this)
          context.withStyledAttributes(attrs, R.styleable.MyCustomView) {
            message = getString(R.styleable.MyCustomView_message) ?: ""
        }
        initViews()
    } 

  
    private fun initViews() {
          backText.setOnClickListener { view ->
              backClickListener?.onClick(view)
          }

         stateImage.setOnClickListener { view ->
              stateClickLisetner?.onClick(view)
          }
    }

    fun setBackClickListener(listener: View.OnClickListener?) = apply {
           backClickListener = listener 
    }

    fun setStateClickListener(listener: View.OnClickListener?) = apply {
           stateClickLisetner = listener 
    }
}


 layout file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="wrap_content"
    android:orientation="vertical">
 
    <MyCustomView
        id="@+id/myCustomView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        app:message="테스트메세지"
       />

    <MyCustomView
        id="@+id/myCustomView2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white" />
 
   ...
</LinearLayout>


Activity:

val myCustomView1: MyCustomView = findViewById(R.id.myCustomView1)
myCustomView1.setBackClickLisetner {
     // do something
}.setStateClickLisetner {
      // do something
}

 

이처럼 하시고 커스텀뷰에 필요한 속성이 있다면 추가하시고, 원하신다면 View.OnClickListener대신에 lamda 를 써서 필요한 정보를 넘기실 수 있을 겁니다.

마지막으로, Device rotation같은 이벤트에도 커스텀뷰의 상태값을 유지하게 하려면 onSaveInstanceState와 onRestoreInstanceState 함수를 오버라이드해서 Bundle에 저장하고 복구해주셔야 합니다.

spark (227,830 포인트) 님이 2021년 5월 14일 답변
spark님이 2021년 5월 14일 수정
감사합니다 시도해보겠습니다.

커스텀뷰를 추천하신 이유가 제 기존 xml에
텍스트뷰, 이미지뷰 이 두개를 무식하게 반복하지말고 커스텀 뷰로 묶어서 하나로 처리해서 이벤트처리나 xml에서 코드를 줄이라는 이유때문이신건가요?
(+ merge xml에서 프레임레이아웃은 지우신건가요 사용하시는건가요? 끝 태그만 남아있어서 사용하시는걸로 작성하셨는지 잘 이해가 가지 않습니다)

그리고 마지막에 기기화면의 값을 유지하기 위해서 saveInstance같은 것을 사용하라하샸는데 이걸 편리하게 해주기위해 사용하는것이 뷰모댈아닌가요? MVVM 디자인패컨을 사용하기 위해 데이터바인딩, 라이브데이터, 뷰모델을 사용할거라서요.
레이아웃을 만들 때 일반적인 상위태그 외에도 include, merge 가 있습니다. 위와 같은 커스텀뷰를 만들  떼 특히 merge 태그를 사용을 많이 하게 되는데, 커스텀뷰 클래스가 FrameLayout이고 layout도 FrameLayout이면, 실제로는 FrameLayout안에 FrameLayout이 다시 들어가게 되기 때문에 이걸 방지하기 위해 사용한 겁니다.
그리고 tools:parentTag는 디자인 타임에 어떤 레이아웃이 사용되는지 안드로이드 스튜디오 레이아웃 에디터에게 알려주기 위해 사용하는 거구요.

커스텀뷰의 상태 복구는 테스트를 해보고 결정하시는게 더 나을 것 같네요. 대신 예를 하나 들게요. 안드로이드에서 제공하는 뷰들은 이미 이 함수들을 다 구현해 놓고 있습니다. 따라서 EditText같은 경우 사용자가 입력을 했을 경우 화면이 백그라운드로 갔다가 돌아와도 입력했던 값이 그대로 존재하잖아요. 이미 이걸 뷰에서 구현해 주었기 때문에 그렇습니다.  안드로이드 뷰시스템은 뷰상태를 복구할 때 커스텀뷰도 해당 함수가 구현되었는지 체크해서 호출을 해주도록 되어 있습니다. 물론 이걸 ViewModel을 통해서 할 수가 있고, 요즘 구글의 아키텍쳐도 이런 방향인데, 이게 생각보다 상당히 번거롭고 정확하지 않은 결과를 낳습니다. 예를 들면 스크롤 상태같은 걸 복구한다던가, 이미 선택했던 아이템의 위치를 복구한다던가 등등 뷰모델로 하기에 적합하지 않은 부분들이 분명히 존재하기 때문에 상황에 따라서 선택을 하도록 해야겠죠.

특히나 싱글액티비티 구조에 네비게이션 컴포넌트를 사용하면 문제가 아주 골치아파져요. 홈버튼을 누른 후 다시 돌아올 때는 액티비티의 상태를 복구하면서 프레그먼트의 상태도 복구해 주는데, 다른 프레그먼트로 갔다가 돌아오면 개발자가 다시 다 복구를 해주어야 하는 경우가 자주 생깁니다. 프레그먼트의 onStop - onStart는 액티비티의 라이프사이클과 같이 호출이 됩니다.
(개인적으로는 버그로 보고 있습니다.)

암튼 정답은 없습니다. 한가지 확실한 것은 중복코드의 제거는 소프트웨어 엔지니어링에서 항상 강조하는 부분입니다.
커스텀뷰 클래스에서 infate한 레이아웃파일만 처리하는게 아니라 커스텀뷰를 만들때 FrameLayout을 상속받아 정의했으니 FrameLayout을 기본 부모루트로 깔고 들어가고 이 부모레이아웃안에 레이아웃파일을 inflate하는 방식인가요?

그리고 싱글액티비티 + 네비게이션 컴포넌트로 도전해보고 있는데 벌써부터 걱정이네요

감사합니다..
선생님 추가로 궁금한점이, 지금 선생님 코드를보면 클릭이벤트를 막 나누어 놓고 액티비티에서 그 함수를 호출하고 리스너를 전달하는 방식을 사용하시던데 그렇게 하는 이유가 뭔가요?

그냥 mycustomView1.setOnClickListener 을 바로 호출해서 안에 TODO를 정의하면 안되나요?

왜 메소드를 따로 만들고 호출해서 리스너를 전달하는 방식을 사용하는건가여?
대부분의 경우는 님이 말씀하신 대로 해도 문제가 없을 것으로 보입니다만, 정확하게 설명드리기는 힘든데, 제 경험상 커스텀뷰가가 리사이클러뷰 같은데서 사용될 경우, 이벤트 리스너가 초기화되어서 동작하지 않을 때가 있습니다. 따라서 이런 모든 경우까지 감안하면, 로컬변수에 리스너를 담아놓고 사용하는 것이 좀 더 정확한 방법이 아닐까 생각합니다.
감사합니다! 시도해보겠습니다
...