일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- ExoPlayer
- video caching
- doc2vec
- AWS EC2
- 시행착오
- ListAdapter DiffUtil
- 유튜브
- kotlin list
- build with ai
- Zsh
- 스피너
- map
- Python
- ktor api call
- llm
- ktor client
- FastAPI
- 독서
- ListAdapter
- android custom view
- exoplayer cache
- ChatGPT
- android exoplayer
- list map
- android
- getChangePayload
- 안드로이드
- DiffUtil.ItemCallback
- kotlin collection
- android ktor
- Today
- Total
버튼 수집상
[안드로이드] 스피너 커스텀UI 만들기 (두 줄 리스트 스피너) 본문
배경
리사이클러뷰 안에서 다른 뷰홀더를 덮으면서 펼쳐지는 두 줄짜리 리스트를 구현해야 했다.
그래서 드롭다운 UI가 액티비티 최상위에 그려지는 스피너 Spinner 를 선택했다. (참고 PopUpWindow)
과제1
드롭다운 리스트가 펼쳐지고 닫히는 시점을 알기 위해
AppCompatSpinner를 상속하는 커스텀뷰를 만들고 리스너 인터페이스를 달았다.
참고한 스택오버플로우 답변
Spinner: get state or get notified when opens
Is it possible to know whether a Spinner is open or closed? It would even be better if there was some sort of onOpenListener for Spinners. I've tried using an OnItemSelectedListener like this:
stackoverflow.com
CustomSpinner.kt
class CustomSpinner @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
def: Int = 0,
) : AppCompatSpinner(context, attrs, def) {
interface OnSpinnerEventListener {
fun onSpinnerOpened()
fun onSpinnerClosed()
fun onSpinnerItemClicked(index: Int) // 어댑터에서 사용
}
lateinit var spinnerListener: OnSpinnerEventListener
private var mOpenInitiated = false
fun setSpinner(listener: OnSpinnerEventListener, list: List<String>) {
spinnerListener = listener // 드롭다운 리스너 초기화
adapter = CustomSpinnerAdapter(list, listener) // 어댑터 초기화 (데이터리스트와 클릭리스너)
}
override fun performClick(): Boolean {
if (this::spinnerListener.isInitialized) {
mOpenInitiated = true
spinnerListener.onSpinnerOpened()
}
return super.performClick()
}
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
if (hasWindowFocus && hasBeenOpened()) {
if (this::spinnerListener.isInitialized) {
mOpenInitiated = false
spinnerListener.onSpinnerClosed()
}
}
}
private fun hasBeenOpened() = mOpenInitiated
}
스피너가 열리고 닫히는 지점을 잡을 때
클릭 trigger - 열림
열려있었으면서 windowFocus 획득 - 닫힘
xml 파일
<...>
<ImageView
android:id="@+id/spinner_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@color/white"
android:padding="12dp"
android:src="@drawable/selector_spinner_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#dddddd"
app:layout_constraintTop_toBottomOf="@id/tab_scroll" />
<com.example.flexibleapp.customview.CustomSpinner
android:id="@+id/spinner"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/white"
android:dropDownWidth="match_parent"
android:spinnerMode="dropdown"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider" />
<...>
스피너가 접혀있을 때 디폴트로 선택된 아이템을 보여주고 싶지 않았다.
그래서 스피너의 width, height를 0dp로 잡고
spinner_button 버튼을 클릭했을 때 spinner.performClick() 을 호출했다.
과제2
스피너에 리스트 아이템을 뿌려줄 어댑터를 구현하자.
그동안 써봤던 ArrayAdpater는 TextView가 최상위 뷰인 xml만 그릴 수 있었다.
두 줄짜리 리스트 아이템을 그리기 위해 BaseAdapter를 상속하는 커스텀 어댑터를 구현했다.
CustomSpinnerAdapter.kt
class CustomSpinnerAdapter(
private val items: List<String>,
private val listener: CustomSpinner.OnSpinnerEventListener? = null,
) : BaseAdapter() {
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
// viewHolder 패턴으로 그리라고 경고문이 뜬다
// 스크롤이 없는 단순한 UI이므로 이대로 구현
val binding = ItemTab001SpinnerBinding.inflate(LayoutInflater.from(parent?.context), parent, false)
// 1차원 문자열 리스트를 두 개씩 묶음
val list = items.mapIndexed{ index, item -> Pair(index, item) }.chunked(2)[position]
binding.tv1.apply {
text = list[0].second
setOnClickListener {
val index = list[0].first
listener?.onSpinnerItemClicked(index)
}
}
if (list.size == 2) {
binding.tv2.apply {
text = list[1].second
setOnClickListener {
val index = list[1].first
listener?.onSpinnerItemClicked(index)
}
}
}
if (list.size == 1 || position == count - 1) {
binding.bottomPadding.visibility = View.VISIBLE
}
if (position == 0) {
binding.topPadding.visibility = View.VISIBLE
}
return binding.root
}
override fun getItem(position: Int): Any {
return items[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getCount(): Int {
return if (items.size % 2 == 0) items.size / 2 else items.size / 2 + 1
}
}
item_tab001_spinner.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">
<View
android:id="@+id/top_padding"
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@color/white"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv1"
android:layout_width="0dp"
android:layout_height="40dp"
android:background="@color/white"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingHorizontal="16dp"
android:textColor="#505050"
android:textSize="13dp"
app:layout_constraintEnd_toStartOf="@id/tv2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_padding"
tools:text="ABOUT US" />
<TextView
android:id="@+id/tv2"
android:layout_width="0dp"
android:layout_height="40dp"
android:background="@color/white"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingHorizontal="16dp"
android:textColor="#505050"
android:textSize="13dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tv1"
app:layout_constraintTop_toBottomOf="@id/top_padding"
tools:text="ABOUT US" />
<View
android:id="@+id/bottom_padding"
android:layout_width="match_parent"
android:layout_height="20dp"
android:background="@color/white"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/tv1" />
</androidx.constraintlayout.widget.ConstraintLayout>
이제 뷰홀더 파일 안에서 스피너를 초기화해보자
//...
spinnerButton.setOnClickListener {
spinner.performClick()
}
spinner.setSpinner(
listener = object : CustomSpinner.OnSpinnerEventListener {
override fun onSpinnerOpened() {
spinnerButton.isSelected = true
}
override fun onSpinnerClosed() {
spinnerButton.isSelected = false
}
override fun onSpinnerItemClicked(index: Int) {
Log.v("TAG", "click! ${data[index].tabTitle}")
}
},
list = data.map { it.tabTitle ?: "" })
//...
이러면 잘 작동한다.
다만, 최상위 액티비티의 UI에 따라서
onWindowFocusChanged 안에서 구현한 커스텀 스피너 닫기 리스너가 제대로 동작하지 않는 경우가 있다.
'TIL - 안드로이드' 카테고리의 다른 글
[안드로이드] onBindViewHolder()가 호출될 때마다 뷰홀더 내부 가로 스크롤이 리셋되는 이슈 (0) | 2023.05.17 |
---|---|
[안드로이드] 스크롤 상단에 margin 적용한 sticky view 만들기 (0) | 2023.05.02 |
[안드로이드] 가변적인 json 키를 동일한 클래스로 파싱하기 (0) | 2023.04.18 |
[안드로이드] Invisible된 액티비티가 onStop을 타지 않는 이슈 (0) | 2023.02.22 |
[안드로이드] 액티비티 미리 로드하기 (0) | 2023.01.05 |