본문 바로가기
Android

[안드로이드] 스피너(Spinner)를 이용해 드롭다운 메뉴 구현 및 커스텀하는 방법

by JongSeok 2023. 1. 19.

어떤 뷰를 터치했을 때 해당하는 메뉴 항목들을 사용자가 선택할 수 있도록 목록으로 펼쳐지는 방식을 드롭다운 메뉴라고 합니다.

드롭다운 메뉴는 안드로이드에서는 스피너(Spinner)를 이용해 구현할 수 있는데 이번 포스팅에서는 스피너를 구현하는 방법과 스피너를 좀 더 보기 좋게 커스텀하는 방법에 대해 알아보겠습니다.

커스텀 스피너(Spinner)와 드롭다운 메뉴


먼저 메뉴의 목록이 될 레이아웃을 설정합니다.

item_spinner_buy_option.xml

<?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="wrap_content"
    android:padding="10dp">

    <TextView
        android:id="@+id/tv_spinner"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView"
        android:textColor="@color/main_text"
        android:textSize="18sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

저는 padding과 텍스트 속성만 변경했지만 자유롭게 뷰를 추가해도 무방합니다.

background_spinner_option.xml ( :drawable)

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <layer-list>
            <item>
                <shape android:shape="rectangle">
                    <solid android:color="@color/white"/>
                    <stroke android:color="@color/black" android:width="1dp"/>
                    <padding
                        android:top="6dp"
                        android:bottom="6dp"
                        android:right="10dp"/>
                    <corners android:radius="8dp" />
                </shape>
            </item>
            
            <item
                android:drawable="@drawable/ic_down"
                android:gravity="center|end"
                android:width="24dp"
                android:height="24dp" />
        </layer-list>
    </item>
</selector>

드롭다운 메뉴를 열기 전 보여지는 스피너 항목을 커스텀합니다.

해당 파일은 스피너의 background 속성과 연결되기 때문에 drawable 폴더에 위치합니다.

OptionSpinnerAdapter

class OptionSpinnerAdapter(context: Context, @LayoutRes private val resId: Int, private val menuList: List<String>)
    : ArrayAdapter<String>(context, resId, menuList) {

    // 드롭다운하지 않은 상태의 Spinner 항목의 뷰
    override fun getView(position: Int, converView: View?, parent: ViewGroup): View {
        val binding = ItemSpinnerBuyOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        binding.tvSpinner.text = menuList[position]

        return binding.root
    }

    // 드롭다운된 항목들 리스트의 뷰
    override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
        val binding = ItemSpinnerBuyOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        binding.tvSpinner.text = menuList[position]

        return binding.root
    }
    
    override fun getCount() = menuList.size
}

메뉴 항목과 Spinner를 연결할 Adapter를 생성합니다. 메뉴가 될 데이터를 배열로 관리하기 위해 ArrayAdapter를 상속받습니다.

저는 드롭다운 메뉴 터치 시 선택한 메뉴가 Spinner에 표시되도록 해당 position의 배열 값을 뷰에 할당해 주었습니다.

@LayoutRes는 레이아웃 리소스인데 드롭다운 메뉴의 xml 파일을 전달받습니다.

BuyFragment

class BuyFragment : Fragment() {
    private var _binding : FragmentBuyBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        _binding = FragmentBuyBinding.inflate(inflater, container,false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val list = listOf("옵션1 선택하기","블랙","카키","핑크","그린")

        binding.spinner.adapter = OptionSpinnerAdapter(requireContext(), R.layout.item_spinner_buy_option,list)
        binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
            override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
                val value = binding.spinner.getItemAtPosition(p2).toString()
                Toast.makeText(requireContext(), value, Toast.LENGTH_SHORT).show()
            }
            override fun onNothingSelected(p0: AdapterView<*>?) {
                // 선택되지 않은 경우
            }
        }
    }
}

Spinner가 있는 액티비티 혹은 프래그먼트에서 어댑터를 연결합니다.

그리고 Spinner의 onItemSelectedListener 속성을 통해 아이템이 선택되거나 선택되지 않은 경우 설정을 추가할 수 있습니다. 저는 아이템 선택 시 해당 메뉴를 토스트 메시지로 출력했습니다.

 

또한, Spinner 커스텀을 위해 Spinner가 있는 xml 파일에서 위에서 만들어둔 background_spinner_option.xml을 background 속성에 추가합니다.

android:theme="@style/SpinnerTheme" 속성은 드롭다운 메뉴와 메뉴 사이에 구분선을 추가하기 위해 style을 지정했습니다.

<style name="SpinnerDivideStyle" parent="android:style/Widget.ListView.DropDown">
    <item name="android:divider">@color/sub_text</item> <!-- divider 색 -->
    <item name="android:dividerHeight">1dp</item> <!-- divider 높이 -->
</style>
<style name="SpinnerTheme">
    <item name="android:dropDownListViewStyle">@style/SpinnerDivideStyle</item>
</style>

해당 style 태그는 values 폴더 안 themes.xml에 추가합니다.


실행결과

728x90
반응형