Try   HackMD

簡單筆記RecyclerView

其實一直不想做RecyclerView的筆記,因為網路上資源太多了,但每次要用的時候都還要花個幾分鐘回憶一下這東西到底在幹嘛,實在是浪費時間,所以這篇從程市結構上簡單講一下,讓下次要用到RecyclerView的時候可以快速恢復記憶。

程式結構

以下重點式地說一下哪些地方要寫東西:

  • Activity/Fragment的class attribute要新增

    • 一個RecyclerView:
      用來接收recyclerview的UI widget,並用它來設定好recyclerview背後的程式邏輯。

    • 一個RecyclerView.Adapter:
      這就是我們主要要寫的一個class,就是recyclerview背後的程式邏輯,主要負責接收資料、顯示item並處理點擊item後的反應。

    • 一個RecyclerView.LayoutManager:
      這是用來做recyclerview的layout管理的。

  • Activity/Fragment的onCreateView():
    裡面要設定inflat相關的東西,要設定recyclerview的layout xml file,以及把RecyclerView.Adapter和RecyclerView.LayoutManager放進RecyclerView。

  • 整個RecyclerView.Adapter要寫好,以下幾個重要部分:

    • 寫一個繼承自RecyclerView.ViewHolder(itemView)的class: 這裡會取得item中的UI widget們,以便待會使用。

    • onCreateViewHolder():
      這裡的重點就是要設定item的layout xml file。

    • onBindViewHolder():
      這裡負責資料的顯示,會把recyclerview的資料用在item的UI widget們身上,並設定item的UI widget們的onClick listener。

重點大概就是以上這些,至於用observer pattern來做Activity/Fragment跟recyclerview之間的溝通因為比較簡單我這邊就沒有特別寫,但那也是要有的,才可以把使用者對recyclerview的操作結果回傳。

程式範例

這個範例是一個掃描BLE並列出所有BLE裝置列表的fragment的.kt檔,透過ViewModel跟Activity拿到recyclerview需要的資料,並在item被click的時候顯示dialog。

import ...


class BleSettingFragment : Fragment(),
    BleDeviceRecyclerviewAdaptor.OnClickListener,
    ConnectDialog.OnClickListener {
    
    private val model: SharedViewModel by activityViewModels()
    private lateinit var recyclerView: RecyclerView
    private lateinit var viewAdapter: BleDeviceRecyclerviewAdaptor
    private lateinit var viewManager: RecyclerView.LayoutManager
    private var myDataset = mutableListOf<String>()
    private val connectDialog = ConnectDialog()


    override fun onStart() {
        super.onStart()
        btnBleCancel.setOnClickListener {
            model.msg.value = Uri.parse("msg://ble_setting/stop_scan")
            findNavController().navigate(
                BleSettingFragmentDirections
                    .actionBleSettingFragmentToMainPageFragment()
            )
        }
        model.msg.value = Uri.parse("msg://ble_setting/start_scan")
        model.msg.observe(viewLifecycleOwner, Observer {
            when (it.authority) {
                "ble_service" -> {
                    when (it.path) {
                        "/start_scan" -> {
                            txtBleScanStatus.text = "掃描中..."
                        }
                        "/stop_scan" -> {
                            txtBleScanStatus.text = "掃描完成"
                        }
                        "/scan_result" -> {
                            // it.fragment = "${name},${addr},${rssi}"
                            // myDataset[i] = "${name},${addr},${rssi}"
                            val newAddr = it.fragment!!.split(",")[1]
                            var alreadyExist = false
                            for (i in 0 until myDataset.size) {
                                val existsAddr = myDataset[i].split(",")[1]
                                if (newAddr == existsAddr) {
                                    alreadyExist = true
                                    myDataset[i] = it.fragment!!
                                }
                            }
                            if (!alreadyExist) {
                                myDataset.add(it.fragment!!)
                            }
                            viewAdapter.notifyDataSetChanged()
                        }
                    }
                }
            }
        })
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        viewManager = LinearLayoutManager(activity)
        viewAdapter = BleDeviceRecyclerviewAdaptor(myDataset)
        viewAdapter.setListener(this)


        var rootView = inflater
            .inflate(R.layout.fragment_ble_setting, container, false) as View
        recyclerView = rootView.findViewById<RecyclerView>(R.id.recyclerView_ble_device)
            .apply {
                // use this setting to improve performance if you know that changes
                // in content do not change the layout size of the RecyclerView
                setHasFixedSize(true)

                // use a linear layout manager
                layoutManager = viewManager

                // specify an viewAdapter (see also next example)
                adapter = viewAdapter
            }
        return rootView
    }

    override fun recyclerviewClick(data: String) {
        val nameAddrRssi = data.split(",")
        connectDialog.setListener(this)
        connectDialog.name = nameAddrRssi[0]
        connectDialog.addr = nameAddrRssi[1]
        connectDialog.signalPower = nameAddrRssi[2]
        connectDialog.width =
            (0.9 * Resources.getSystem().displayMetrics.widthPixels).roundToInt()
        connectDialog.height =
            (0.4 * Resources.getSystem().displayMetrics.heightPixels).roundToInt()
        connectDialog.show(activity?.supportFragmentManager!!, "connection dialog")
    }

    override fun connectDialogClick(data: String) {
        model.msg.value = Uri.parse("msg://ble_setting/connect#$data")
        connectDialog.dismiss()
        findNavController().navigate(
            BleSettingFragmentDirections
                .actionBleSettingFragmentToSystemSettingFragment()
        )
    }
}


class BleDeviceRecyclerviewAdaptor(private val myDataset: MutableList<String>) :
    RecyclerView.Adapter<BleDeviceRecyclerviewAdaptor.MyViewHolder>() {

    // onclick listener interface
    private var listener: OnClickListener? = null

    interface OnClickListener {
        fun recyclerviewClick(data: String)
    }

    fun setListener(parentFragment: OnClickListener) {
        listener = parentFragment
    }

    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder.
    // Each data item is just a string in this case that is shown in a TextView.
    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val device = itemView.btnDevice
    }


    // Create new views (invoked by the layout manager)
    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): BleDeviceRecyclerviewAdaptor.MyViewHolder {
        // create a new view
        val deviceItemLayout = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_ble_device, parent, false) as View
        // set the view's size, margins, paddings and layout parameters
        return MyViewHolder(deviceItemLayout)
    }

    // Replace the contents of a view (invoked by the layout manager)
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        // - get element from your dataset at this position
        // - replace the contents of the view with that element
        val name = myDataset[position].split(",")[0]
        val addr = myDataset[position].split(",")[1]
        if (name == "null") {
            holder.device.text = addr
        } else {
            holder.device.text = name
        }
        holder.device.setOnClickListener {
            listener?.recyclerviewClick(myDataset[position])
        }
    }

    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        super.onDetachedFromRecyclerView(recyclerView)
        listener = null
    }


    // Return the size of your dataset (invoked by the layout manager)
    override fun getItemCount() = myDataset.size
}


class ConnectDialog : DialogFragment() {
    var name = ""
    var addr = ""
    var signalPower = ""
    var width = 0
    var height = 0

    // onclick listener interface
    private var listener: OnClickListener? = null

    interface OnClickListener {
        fun connectDialogClick(data: String)
    }

    fun setListener(parentFragment: OnClickListener) {
        listener = parentFragment
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return LayoutInflater.from(context)
            .inflate(R.layout.dialog_connection, container, false)
    }

    override fun onStart() {
        super.onStart()
        txtDeviceName.text = name
        txtDeviceMac.text = addr
        txtDeviceSignalPower.text = signalPower
        btnConnect.setOnClickListener {
            listener?.connectDialogClick(addr)
        }
        dialog?.window?.setLayout(width, height)
        dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
    }
}