0101011001010111

1-3. CustomView 본문

Kotlin/안드로이드_[숙련]앱개발

1-3. CustomView

[진주] 2023. 8. 25. 20:52
728x90
반응형

커스텀뷰 CustomView

말그대로 내가 직접 커스터마이징 해서 만드는 뷰이다!!

 

 

 


 

먼저, 커스텀뷰를 만들기 위해 할일!! 목록!! 

 

1.커스텀 항목을 위한 XML 레이아웃 정의

 

2. 항목 관련 데이터 클래스 정의

예를들면 위와 같은 커스텀 뷰를 만들 것이다 하면, 이 커스텀 뷰에 들어가는 데이터들을 하나의 클래스로 정의할 겁니다! 

 

 

 

3. 어댑터 클래스 정의

 

4. 메인화면 레이아웃에 ListView 위젯 정의

 

5. 어댑터를 생성하고 어댑터뷰 객체에 연결

 

순으로 이루어 진다. 

 

상세 세부 설명을 시작하겠다.▼

 


 

1.커스텀 항목을 위한 XML 레이아웃 정의

먼저, 커스텀 항목을 위한 xml을 만들건데, 다음과 같이 만들어보도록 하겠다! 

이런 형식으로 만들 것이다.

Constraintlayout을 써도 되지만, 이번엔 LinearLayout으로 만들어보자 ! 

 

 

먼저 실습을 위해 이미지가 필요하다! 

drawable 폴더에 사진을 붙여넣자.

 

 

item이라는 레이아웃 파일을 하나 만들자.(커스텀 레이아웃을 위해서 기본틀을 만드는거임! )

 

 

그리고, values 에, dimens(이름자유) 로 하나 만들자

왜 만드냐면, 그림이라든지, 텍스트 옵션값같은것을 style값을 저장해서 그 스타일을 쓰고자 할 때 빼서쓸 수 있다.

 

파일이름은 xml 파일로 만들어주자

만들기 완성

 

<?xml version="1.0" encoding="utf-8"?>

<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
    <dimen name="list_item_text_size1">20dp</dimen>
    <dimen name="list_item_text_size2">16dp</dimen>
    <dimen name="list_item_padding">4dp</dimen>
    <dimen name="icon_size">60dp</dimen>
    <dimen name="icon_padding">8dp</dimen>
</resources>

 

그안에는 이렇게 넣어줬는데,

마진값, 텍스트값

패딩값, 아이콘사이즈와 패딩값 등을 설정해두었다.

 

( 본인 원하는대로 자주 쓸 값을 넣어주자)

 

 

item.xml

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    >
    <ImageView
        android:id="@+id/iconItem"
        android:layout_width="@dimen/icon_size"
        android:layout_height="@dimen/icon_size"
        android:scaleType="centerCrop"
        android:padding="@dimen/icon_padding"
        android:layout_gravity="center_vertical"
        android:layout_weight="1"
        android:src="@drawable/jinju1"
        />
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="2">
        <TextView
            android:id="@+id/textItem1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/black"
            android:textSize="@dimen/list_item_text_size1"
            android:padding="@dimen/list_item_padding"
            android:hint="Name"
            />
        <TextView
            android:id="@+id/textItem2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/black"
            android:textSize="@dimen/list_item_text_size2"
            android:padding="@dimen/list_item_padding"
            android:hint="Age"
            />
    </LinearLayout>

</LinearLayout>

그럼 리스트뷰에 항목이 될 애를 이렇게 레이아웃으로 하나 만들었다 .

 

 

 


2. 항목 관련 데이터 클래스 정의

 

그 다음 내가 만든 레이아웃을 MyItem이라고 데이터 클래스를 정의해주자 . 

 

 

data class MyItem(val aIcon:Int, val aName:String, val aAge:String) {}

 

* 잠깐, 데이터 클래스는 뭐냐면 , 

그냥 클래스와의 차이는 코틀린에서 데이터 클래스를 제공하는데,  데이터를 다루는 [최적화]된 클래스로 

equals(), hashCode(), toString(), copy(), componentN() 5가지 유용한 함수들을 내부적으로 자동으로 생성

 ㄴ 이런게 데이터 클래스를 안쓴다면, 직접 다 구현을 해줘야 한다.

 

그래서 데이터 클래스를 쓰면 더 편하게 다룰 수 있다.

 

 

이것도 그냥 new해서 클래스를 만들어주도록 하자.

 

 

위 코드를 여기 넣어주면 끝 

 

 

data class MyItem(val aIcon:Int, val aName:String, val aAge:String) {}

자 , aIcon 이게 Int인 이유는, 리소스ID(이미지들)가 Int값으로 들어가기 때문에 , 리소스를 쓸거라서 Int이고

 

aname은 문자열을 쓸 것이므로 스트링,

나이 > 스트링 (문자열 쓸것인가봄)

 


 

 

3. 어댑터 클래스 정의

 

이제 어댑터 클래스를 만들어보자 

자 어댑터는 MyAdapter 라는 클래스로 만들어준다 .

 

  • 앞서 정의한 MyItem 타입의 객체들을 ArrayList로 관리하는 MyAdapter 클래스 BaseAdapter를 파생하여 정의
  • MyAdapter 클래스는 앞서 예시한 그리드 뷰의 예제에서 처럼, getCount(), getItem(), getItemID(), getView() method를 재 정의해야 합니다.

 

package com.example.android_1_2_customviewrecyclerview

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.TextView

class MyAdapter(val mContext: Context, val mItems: MutableList<MyItem>) : BaseAdapter() {

    // MyAdapter 클래스가 관리하는 항목의 총 개수를 반환
    override fun getCount(): Int {
        return mItems.size
    }

    // MyAdapter 클래스가 관리하는 항목의 중에서 position 위치의 항목을 반환
    override fun getItem(position: Int): Any {
        return mItems[position]
    }

    // 항목 id를 항목의 위치로 간주함
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    // position 위치의 항목에 해당되는 항목뷰를 반환하는 것이 목적임
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {

        var convertView = convertView
        if (convertView == null) convertView = LayoutInflater.from(parent?.context).inflate(R.layout.item, parent, false)

        val item : MyItem = mItems[position]

        // convertView 변수로 참조되는 항목 뷰 객체내에 포함된 객체를 id를 통해 얻어옴
        val iconImageView = convertView?.findViewById<View>(R.id.iconItem) as ImageView
        val tv_name = convertView.findViewById<View>(R.id.textItem1) as TextView
        val tv_age = convertView.findViewById<View>(R.id.textItem2) as TextView

        // 어댑터가 관리하는 항목 데이터 중에서 position 위치의 항목의 객체를 헤딩 힝목에 설정
        iconImageView.setImageResource(item.aIcon)
        tv_name.text = item.aName
        tv_age.text = item.aAge

        return convertView
    }
}

 

class MyAdapter(val mContext: Context, val mItems: MutableList<MyItem>) : BaseAdapter() {

 

MyItem 타입의 객체들을 ArrayList로 관리하는 MyAdapter 클래스 BaseAdapter를 파생하여 정의

할거고,

 

안에 들어가있는 파라미터들을 보겠습니다.

 

MyAdapter를 호출할 때 Context 가 넘어오고 그다음 MutavleList로 MyItem 리스트를 같이 넣어줄 거에요.

그러니까, 어댑터를 생성할 때, 데이터들을 넣어줘야 하는 것이다.

 

+추가 궁금증 : 파라미터 안의 Context는 무엇일까?

먼저, Context는 Android에서 제공하는 것이다.

Context는 애플리케이션 환경에 대한 전역 정보의 인터페이스 입니다.

여기서는 MyAdapter클래스에서 Android 시스템 리소스나 애플리케이션 수준의 작업을 수행하기 위해 필요합니다.


좀 더 쉬운 설명 ▼

 

Context를 일상적인 상황에 비유해보면 이해하기 쉬워질 것입니다.

 

Context의 비유

Android의 Context는 사람의 사회적·문화적 배경과 같습니다. 우리는 다양한 사회적·문화적 배경 아래에서 생활하며 그 배경을 기반으로 정보를 해석하고, 의사소통을 하며, 일상적인 활동을 수행합니다.

예를 들어, 우리는 우리 나라의 언어를 사용하여 의사소통을 하고, 우리 나라의 문화에 맞는 행동을 합니다. 이런 배경 정보 없이는 우리 주변의 사람들과 소통하기 어렵습니다.

 

Android에서의 Context

마찬가지로, Android 앱 내에서의 Context는 앱이나 컴포넌트가 "살아가는" 환경이나 배경을 나타냅니다. Context를 통해 앱은 자신의 리소스에 접근하거나 시스템 서비스를 사용하거나 다른 컴포넌트와 소통할 수 있습니다.

간단히 말하면, Context는 앱이나 그 내부의 컴포넌트가 Android 시스템과 소통하기 위한 "브릿지"나 "통신수단" 역할을 합니다.

 

예를 들면:

  • 앱이 특정 문자열 리소스를 필요로 할 때, Context를 사용하여 해당 리소스에 접근합니다. (사람이 자신의 언어로 의사소통하는 것과 유사)
  • 앱이 진동이나 알림을 발생시키려면, Context를 통해 시스템 서비스에 액세스합니다. (사람이 자신의 문화에 따라 특정 행동을 하는 것과 유사)

이렇게 보면, Context는 앱이나 컴포넌트가 Android 시스템 내에서 "살아가기" 위한 필수적인 도구나 환경이라고 볼 수 있습니다.


BaseAdapter를 반환받기 위해 필요한 파라미터는 어떤 것이 있을까?

BaseAdapter를 상속받아서 커스텀 어댑터를 구현할 때, 꼭 필요한 파라미터는 없습니다. 그러나, 대부분의 어댑터 구현에서 아래와 같은 파라미터들이 자주 전달됩니다:

  1. Context: 앞서 언급했듯이, Context는 Android 시스템 리소스나 서비스에 액세스하기 위한 참조입니다. Context를 통해 리소스에 접근하거나 레이아웃을 인플레이션할 수 있습니다.
  2. 데이터 리스트: 어댑터는 주로 데이터의 리스트를 화면에 표시하는 데 사용됩니다. 이 데이터 리스트는 보통 List, Array 등의 형태로 전달됩니다. 이 파라미터는 실제로 어댑터가 화면에 표시할 데이터를 나타냅니다.

예를 들어, 위에서 제공된 코드에서 MyAdapter의 생성자는 Context와 MutableList<MyItem>을 파라미터로 받습니다. 여기서 MutableList<MyItem>은 어댑터가 화면에 표시할 데이터의 목록입니다.

그렇지만, 어댑터의 목적과 디자인에 따라 다른 추가 파라미터들이 필요할 수도 있습니다. 이는 개발자의 필요에 따라 달라집니다.

 

 


 

 

여기서 MyItem은 우리가 아까 데이터 클래스 MyItem만든 이거다.

 

 

override fun getCount(): Int {
    return mItems.size
}

getConut()가 들어가게되면 

들어온 나의 아이템 (val mItems)의 총 사이즈를 반환하게 되고 ( 총 몇갠지.)

 

override fun getItemId(position: Int): Long {
    return position.toLong()
}

getItem은

포지션에 해당하는 position에들어가는 위치를 반환하게 되는거고

 

 

 

override fun getItemId(position: Int): Long {
    return position.toLong()
}

getItemId는 position의 아이디를 리턴하게 됩니다.

 

(getItemId는 지금 이미지에 Id를 부여 ! //getItem은 얘가 어딧니? 할때 위치를 찾아주는거)

 

 

 

 

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {

    var convertView = convertView
    if (convertView == null) convertView = LayoutInflater.from(parent?.context).inflate(R.layout.item, parent, false)

    val item : MyItem = mItems[position]

그리고 중요한게 getView를 하게 되면, 

우리가 만들었던 이 커스텀 뷰 잇졍 ? (item.xml)

이친구를 리턴하게 될건데, 어떤식으로 리턴하냐면 

override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View

 

getView는 ! (파라미터를 보자)

position의 위치와, 

converView 라고, 리스트뷰에 항목을 띄울 뷰를 파라미터로 받게 되고,

Viewgroup까지 받게 된다.

 

 

 

 

 

그래서 리턴할 뷰에다가 뭘 넣게 되냐면,

.inflate(R.layout.item, parent, false)

inflate해서 우리가 만들었던 item(item.xml) 이 ▲ 이렇게 들어올거고

 

 

 

 

val item : MyItem = mItems[position]

 

이 코드의 의미는 다음과 같습니다:

  1. val 키워드를 사용하여 item이라는 이름의 상수(변수의 불변형)를 선언합니다. 이 item 상수의 타입은 MyItem입니다.
  2. mItems[position]는 mItems라는 MutableList<MyItem>에서 position 인덱스에 해당하는 항목을 가져옵니다. 이 항목은 MyItem 타입의 객체일 것입니다.
  3. = 연산자를 사용하여 mItems[position]에서 반환된 MyItem 객체를 item 상수에 할당합니다.

따라서 이 코드는 mItems 리스트에서 position 위치에 있는 MyItem 객체를 item이라는 상수에 연결(할당)합니다.

 

-MyItem이라는 걸 하나 선언해서 position이 들어왔잖아요

그 포지션에 해당하는...

여기서 리스트를 쭉 받게 되잖아요?

 

그러면 그 중에 포지션에 해당하는 아이를 아이템으로 하나 만들어서 (클래스를 만들어서)

그 아이템에 해당하는 아이들을 넣어줄건데, 

넣어주려면 어디다 넣을 건지를 지정해야겠죠 ?

 

 

// convertView 변수로 참조되는 항목 뷰 객체내에 포함된 객체를 id를 통해 얻어옴
val iconImageView = convertView?.findViewById<View>(R.id.iconItem) as ImageView
val tv_name = convertView.findViewById<View>(R.id.textItem1) as TextView
val tv_age = convertView.findViewById<View>(R.id.textItem2) as TextView

 

여기서 이미지 뷰랑 텍스트 뷰 2개 ( 이건 아까 item.xml에서 설정한 아이디 값을 넣어준다 ) 

item.xml

이 애들을 findViewById를 해서 연결 해줄거에요. 

 

이렇게 선언을 해두고

 

 

// 어댑터가 관리하는 항목 데이터 중에서 position 위치의 항목의 객체를 헤딩 힝목에 설정
iconImageView.setImageResource(item.aIcon)
tv_name.text = item.aName
tv_age.text = item.aAge

 

여기다가 포지션에 해당하는 아이템의 사진과

이름과 나이를 넣어줘야 되겠죠.

 

그리고

 

return convertView

 

convertView를 리턴해주면 

 

MyAdapter는 이렇게 끝나게 됩니다.

 


그 다음 해줘야 할게 activity_main.xml은 간단해요.

 

만들어준 ListView만 꽉차게 뿌리면 되니까 

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

  <ListView
      android:id="@+id/listView"
      android:layout_width="match_parent"
      android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>


5. 어댑터를 생성하고 어댑터 뷰 객체에 연결

8분 

 

어댑터를 생성 후, 어댑터 뷰를 객체 연결 하는 작업을 할거에요. 

데이터 원본 (이미지 ) 를 준비하고 

 

// 어댑터 생성 및 연결
binding.listView.adapter = MyAdapter(this, dataList)

 

// 항목 클릭 이벤트 처리
binding.listView.setOnItemClickListener{ parent, view, position, id ->
    val name: String = (binding.listView.adapter.getItem(position) as MyItem).aName
    Toast.makeText(this," $name 선택!", Toast.LENGTH_SHORT).show()

9분 22초 

코드 다시한번 설명 : 

가장 먼저 이제 MyItem이라는 걸 하나 데이터 클래스로 만들고, 

 

그 다음에 어댑터 만들고 

package com.example.android_1_2_customviewrecyclerview

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.TextView

class MyAdapter(val mContext: Context, val mItems: MutableList<MyItem>) : BaseAdapter() {

    // MyAdapter 클래스가 관리하는 항목의 총 개수를 반환
    override fun getCount(): Int {
        return mItems.size
    }


    // MyAdapter 클래스가 관리하는 항목의 중에서 position 위치의 항목을 반환
    override fun getItem(position: Int): Any {
        return mItems[position]
    }

    // 항목 id를 항목의 위치로 간주함
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    // position 위치의 항목에 해당되는 항목뷰를 반환하는 것이 목적임
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {

        var convertView = convertView
        if (convertView == null) convertView = LayoutInflater.from(parent?.context).inflate(R.layout.item, parent, false)

        val item : MyItem = mItems[position]

        // convertView 변수로 참조되는 항목 뷰 객체내에 포함된 객체를 id를 통해 얻어옴
        val iconImageView = convertView?.findViewById<View>(R.id.iconItem) as ImageView
        val tv_name = convertView.findViewById<View>(R.id.textItem1) as TextView
        val tv_age = convertView.findViewById<View>(R.id.textItem2) as TextView

        // 어댑터가 관리하는 항목 데이터 중에서 position 위치의 항목의 객체를 헤딩 힝목에 설정
        iconImageView.setImageResource(item.aIcon)
        tv_name.text = item.aName
        tv_age.text = item.aAge

        return convertView
    }
}

 

 

어댑터에다가 데이터 원본을 집어넣어서 

MainActivity.kt

 

// 어댑터 생성 및 연결
binding.listView.adapter = MyAdapter(this, dataList)

 

리스트뷰에 있는 어댑터랑 연결 시키고 

 

 

// 항목 클릭 이벤트 처리
binding.listView.setOnItemClickListener{ parent, view, position, id ->
    val name: String = (binding.listView.adapter.getItem(position) as MyItem).aName
    Toast.makeText(this," $name 선택!", Toast.LENGTH_SHORT).show()

 

 

그 다음에 onClick 처리.

 

 

 

실행화면

 

 

 

 

 

 

 

ㅇ ㅏ......ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ

혼자 못할거같은데 좀 더 연습이 필요하다 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90
반응형