07
20

FAB๋ฅผ ์ด์šฉํ•œ ํด๋ฆญ์ด๋ฒคํŠธ๋กœ ๋ฉ”๋‰ด๋ฅผ ์ฃผ๋ฅด๋ฅต ๋œจ๊ฒŒ ๋งŒ๋“ค์–ด์•ผํ–ˆ๋‹ค. 

์ด๋Ÿฐ ์‹์œผ๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด์—ˆ๋Š”๋ฐ, ์ด๊ฑด github์— ๊ฒ€์ƒ‰ํ•˜๋ฉด ์ตœ์ƒ๋‹จ์— ๋‚˜์˜ค๋Š” ์œ ์ € ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค. ๊ทธ๋ƒฅ ์จ๋„ ๋˜์ง€๋งŒ, 3๋…„ ์ „์ด ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ์ธ ๊ฒƒ๋„ ์žˆ๊ณ , ์ด๋ฒˆ์— ํ•œ๋ฒˆ ๋„์ „ํ•ด๋ณด์ž๋Š” ์˜๋ฏธ์—์„œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ฐ์ง€ ์•Š๊ณ  ๊ตฌํ˜„ํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.

# ์ฒซ ์‹œ๋„ - popup๋ฉ”๋‰ด๋ฅผ ๋‹ฌ์•„๋ณด๊ธฐ

`/res/menu`์— ์‚ฌ์šฉํ•  ๋ฉ”๋‰ด๋“ค์„ ์„ ์–ธํ•˜๊ณ , ๊ตฌํ˜„ํ•ด๋ดค๋‹ค. ๋‹จ์ˆœํžˆ popup๋ฉ”๋‰ด ์ด๋ฒคํŠธ์˜ show ํŠธ๋ฆฌ๊ฑฐ๋ฅผ fab ํด๋ฆญ์œผ๋กœ ๋‹ฌ์•„๋‘” ํ˜•ํƒœ์˜€๋‹ค...

 

์ด๋ž˜์„œ๋Š” ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ํ˜•ํƒœ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์—†์—ˆ๋‹ค.

ํ”ผ๊ทธ๋งˆ ๋Œ€๋กœ๋ฉด ์ด๋Ÿฐ ๋ชจ์–‘์„ ๋งŒ๋“ค์–ด์•ผํ–ˆ๋Š”๋ฐ, ์ข€ ์ฐพ์•„๋ณด๋‹ค ๋ณด๋‹ˆ `ListPopupWindow`๋ผ๋Š” ๊ฑธ ์ฐพ์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค. spinner์˜ ์ผ์ข…์œผ๋กœ ํ˜ธ์ŠคํŠธ๋กœ ์–ด๋–ค ๋Œ€์ƒ์„ ์ •ํ•ด์„œ, ๊ทธ๊ฑธ ๊ธฐ์ค€์œผ๋กœ ๋ชฉ๋ก์„ ๋ณด์—ฌ์ฃผ๋Š” ์œ„์ ฏ์ด๋‹ค.

# `ListPopupWindow`๋กœ ๊ตฌํ˜„ 

๋ชฉ๋ก์„ adapter๋กœ ๋ฐ›๋Š”๋ฐ, ListAdapter๋ฅผ ๋ฐ›๋Š”๋‹ค. ์ฒ˜์Œ์— ์ด๊ฑฐ๋ณด๊ณ  "์–ด? RecyclerView์˜ ListAdapter์ธ๊ฐ€?" ํ–ˆ๋Š”๋ฐ widget์˜ ListAdpater์˜€๋‹ค. ์ด๋Ÿฌ๋ฉด override ํ•ด์•ผํ•˜๋Š” ํ•จ์ˆ˜๊ฐ€ ๋งŽ์•„์„œ ๊ทธ๋ƒฅ ListAdapter๋ฅผ ์ƒ์†ํ•œ ํด๋ž˜์Šค์ธ BaseAdapter๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

๋ฌธ์„œ์—๋„ ์„ค๋ช…๋˜์–ด์žˆ๋‹ค

class CustomPopupAdapter(private val items: List<String>) : BaseAdapter() {
    override fun getCount(): Int = items.size
    override fun getItem(position: Int): Any = items[position]
    override fun getItemId(position: Int): Long = position.toLong()

    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val holder: PopUpViewHolder = if (convertView == null) {
            PopUpViewHolder(
                ItemPopUpBinding.inflate(
                    LayoutInflater.from(parent.context),
                    parent,
                    false
                )
            )
        } else {
            convertView.tag as PopUpViewHolder
        }

        return holder.bind(items[position], position)
    }

getView๋Š” view๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ค˜์•ผํ•˜๋Š”๋ฐ, ViewHolderํŒจํ„ด์„ ํ™œ์šฉํ–ˆ๋‹ค. ๋‹ค๋งŒ getView์—ฌ์„œ View๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ๋˜๋Š”๋ฐ, ์ด๊ฑธ ViewHolder ํด๋ž˜์Šค์—์„œ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์—ˆ์ง€๋งŒ ๋ฐฉ๋ฒ•์„ ๋”ฑํžˆ ๋– ์˜ฌ๋ฆฌ์ง€ ๋ชปํ•ด holder์˜ `itemView`๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ๋‹ค.

 

convertView๋กœ ๋ทฐ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ๋งค๋ฒˆ ์ƒˆ๋กœ ๋งŒ๋“คํ•„์š”๊ฐ€ ์—†๋„๋ก null์ผ๋•Œ๋งŒ inflateํ–ˆ๋‹ค. ์ด๋ฏธ ๋ทฐ๊ฐ€ ๋งŒ๋“ค์–ด์กŒ๋‹ค๋ฉด tag์— ๋‹ด๊ฒจ์žˆ๋Š” ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. tag๋Š” ์•ˆ์จ๋ณธ ์‚ฌ๋žŒ๋„ ์žˆ์„ ๊ฑฐ๋ผ ์ƒ๊ฐ๋˜๋Š”๋ฐ, ์ผ์ข…์˜ Any ๊ฐ™์€ ๋Š๋‚Œ์ด๋‹ค. 

/**
 * Returns this view's tag.
 *
 * @return the Object stored in this view as a tag, or {@code null} if not
 *         set
 *
 * @see #setTag(Object)
 * @see #getTag(int)
 */
@ViewDebug.ExportedProperty
@InspectableProperty
public Object getTag() {
    return mTag;
}

๋ชจ๋“  ๊ฐ์ฒด๋ฅผ ๋‹ด์„ ์ˆ˜ ์žˆ๊ณ , ๋”ฐ๋ผ์„œ ViewHolder ๊ฐ์ฒด ์—ญ์‹œ ๋‹ด์„ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ด๋ฏธ ๋‹ด๊ฒจ์žˆ๋Š” ๊ฐ์ฒด๋ฅผ ๊บผ๋‚ด์“ฐ๋ฉด ๋˜์„œ ์„ฑ๋Šฅ์ด ์˜ฌ๋ผ๊ฐ„๋‹ค. holder ๊ฐ์ฒด์—์„œ ์‚ฌ์šฉํ•  ๊ฐ’๋„ ๋„ฃ์–ด์ฃผ๋ฉด ๋๋‚œ๋‹ค. 

 

์ด์ œ ViewHolder๋ฅผ ๋ณด์ž.

class PopUpViewHolder(private val binding: ItemPopUpBinding) {
    private val itemView: View = binding.root

    init {
        itemView.tag = this
    }

    fun bind(item: String, position: Int): View {
        binding.tvTitle.text = item
        when (position) {
            Ticket.ISSUED.state -> {
                binding.ivIcon.setImageResource(R.drawable.ic_issued_ticket)
            }

            Ticket.UNISSUED.state -> {
                binding.ivIcon.setImageResource(R.drawable.ic_unissued_ticket)
            }
        }
        binding.executePendingBindings()
        return itemView
    }
}

์šฐ์„  binding.root๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์•ˆ๋œ๋‹ค. tag๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ทฐ๋ฅผ ์žฌํ™œ์šฉํ•ด์•ผ๋˜๊ณ , ๊ทธ tag์— ๋‹ด๊ธธ ์š”์†Œ๊ฐ€ ViewHolder์ธ๋ฐ tag๋ฅผ ์ง€์ •ํ•˜์ง€์•Š๊ณ  ๊ทธ๋ƒฅ binding.root๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด tag๋ฅผ ์ฐพ์ง€๋ชปํ•ด์„œ ์•ฑ์ด ์ฃฝ์–ด๋ฒ„๋ฆฐ๋‹ค.

 

๊ทธ๋ž˜์„œ itemView๋ฅผ binding.root๋กœ ํ•ด์„œ,  tag๋ฅผ PopUpViewHolder๋กœ ์ง€์ •ํ•ด์ฃผ๋Š” ๊ฒƒ์ด๋‹ค. executePendingBindings๋Š” ๋ฐ”์ธ๋”ฉ ๊ณ„์˜ lazy๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

 

์ด์ œ ํ˜ธ์ถœ๋ถ€๋ฅผ ๋ณด๊ฒ ๋‹ค.

private val popUpAdapter by lazy {
    CustomPopupAdapter(
        listOf(
            getString(R.string.schedule_ticket_issued),
            getString(R.string.schedule_ticket_unissued)
        )
    )
}


private fun initFAB() {
    val listPopupWindow = ListPopupWindow(binding.root.context)
    listPopupWindow.apply {
        setBackgroundDrawable(
            ContextCompat.getDrawable(
                binding.root.context,
                android.R.color.transparent
            )
        )
        setAdapter(popUpAdapter)
        anchorView = binding.fabAddTicket
        width =
            binding.root.context.resources.getDimensionPixelSize(R.dimen.default_popup_width)
        height = ListPopupWindow.WRAP_CONTENT
        isModal = true
        setOnItemClickListener { _, _, position, _ ->
            when (position) {
                Ticket.ISSUED.state -> {
                    Timber.tag("fab").d("issued")
                }

                Ticket.UNISSUED.state -> {
                    Timber.tag("fab").d("unissued")
                }
            }
            listPopupWindow.dismiss()
        }
    }

    binding.fabAddTicket.setOnClickListener {
        listPopupWindow.show()
    }
}

ListPopupWindow๊ฐ€ ๊ธฐ๋ณธํ…Œ๋งˆ๋ฅผ ๊ฐ–๊ณ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐฐ๊ฒฝ๋ถ€ํ„ฐ ๋‚ ๋ ค์คฌ๋‹ค. ์–ด๋Œ‘ํ„ฐ๋ฅผ ์„ค์ •ํ•ด์ฃผ๊ณ , ํ˜ธ์ŠคํŠธ๋กœ ์‚ฌ์šฉํ•  fab๋ฅผ anchor๋กœ ๋‹ฌ์•„์คฌ๋‹ค.

width๋Š” ์—ฌ๊ธฐ์„œ๋Š” ๋ณด์ด์ง€ ์•Š์ง€๋งŒ 200dp๋กœ ์„ค์ •ํ–ˆ๊ณ , ๋ชฉ๋ก์ด ์•„๋‹Œ ๋‹ค๋ฅธ ๋ถ€๋ถ„์„ ๋ˆ„๋ฅด๋ฉด ๋‹ซํžˆ๋„๋ก modal ๊ฐ’์„ true๋กœ ์ง€์ •ํ–ˆ๋‹ค. 

 

์•„๋ž˜๋Š” ๊ฒฐ๊ณผ๋ฌผ์ด๋‹ค..! ๋‚˜๋ฆ„ ์„ฑ๊ณต์ ์ธ ๋„์ „์ด๋ผ๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

 

 

๋„์›€์ด ๋๋‹ค๋ฉด ๋Œ“๊ธ€์ด๋‚˜ ๊ณต๊ฐ ๋ฒ„ํŠผ ํ•œ ๋ฒˆ์”ฉ ๋ˆ„๋ฅด๊ณ  ๊ฐ€์ฃผ์„ธ์š”!

 

๋ฐ˜์‘ํ˜•
COMMENT