demand
There is a need:
- 1 requires a page that displays multiple entries
- 2 Each entry has an independent countdown, and the entry will be deleted after the countdown
- 3 There is a delete button on each item, click to delete the item
- 4 The types of items on the list are varied
Feasibility analysis
The first thing that can definitely be done:
- 1 use a RecyclerView to achieve
- 2 add a countdown control to each item. Note that the countdown is in the corresponding data of the item, not in the UI
- 3 Add and delete button, click to delete the corresponding data, and stop the countdown corresponding to the data, while updating the adapter
- 4 Use getViewType() to implement multiple item types
Third-rate programmers see this and are already writing code…
Second – rate programmers continue to read.
Demand analysis
First of all, no problem with number one.
2, we need to add a countdown component to the data corresponding to the item, which sounds wrong. The countdown component is clearly used to update the UI, should be held by the UI, now let the data hold, that is equivalent to the data indirectly hold the UI, long life cycle hold short life cycle, no. And, when there is a lot of data, such as 10W pieces of data, there are 10W countdown components, the CPU does not eat or drink also busy (CPU: WQNMLGB)! This obviously belongs to the problem of qualitative change caused by quantitative change. In order to avoid this problem, we need to quantify the countdown components constantly, that is, only constant countdown, so that the number of countdown components is not affected by the number of data.
So, how do we define this constant?
Considering that the countdown is used to update the UI, we can create as many countdown components as there are visible items on the screen, since the off-screen items are invisible to others anyway, so we can ask ViewHolder to hold the countdown components. Moreover, the reuse mechanism of RecyclerView to ViewHolder can be used.
But if we hold the ViewHolder, when the ViewHolder slides out of the screen, it gets retrieved, and the countdown stops, and then we can’t trigger the delete operation that ends the countdown, because even out of the screen, as soon as the delete data of the countdown is triggered, our data in the screen will slide up one position, and it’s perceptible, So, if we slide off the screen and the countdown stops and we can’t trigger the delete, we might wait too long without seeing the data slide up on the screen, which is clearly wrong.
The program serves users. Based on the above analysis, we only consider from the perspective of users:
- In case1, if the countdown is placed in the data, the user can feel the deletion operation, because there is sliding, but more data will obviously feel the lag, because there is a lot of countdown.
- In case2, if the countdown is placed inside the ViewHolder, the user will not be aware of the delete operation because the countdown stops when the screen slides out, but will not be aware of the delay if more data is added.
This is a deadlock and cannot be resolved! Then you need to take a step back and change the requirements. Since the user’s problem cannot be solved perfectly, we will change the user’s habit. Let’s make: After the countdown ends, item will not be deleted, but will be left in grey.
Why did you change that? Because for case1, we can’t solve it, so we have to start with Case2, and the problem with CasE2 is that the user can’t sense the deletion, so I don’t delete it, so you don’t have to either, just put the ash.
All right, number two.
Article 3, do not have what problem, remove directly (index), and then call adapter. NotifyItemRemoved () finished.
If-else /switch-case can return different ViewHolder for different types. But a better way to write it is to use factory mode.
design
After the feasibility analysis and requirements analysis, we started the outline design.
- 1 We need to create a RecyclerView.
- 2. We need to add a countdown component to the ViewHolder. It is enough for us to use Handler here, and we need to start the countdown when entering the screen and stop the countdown when sliding out of the screen to save CPU.
- 3 delete the data is not nonsense, this can not be, back to rebuild it.
- Use factory mode to create different ViewHolder for different viewtypes.
Here are a few things to note:
- 1 ViewHolder entering the screen will trigger onBindViewHolder(), and sliding off the screen will trigger onViewRecycled().
- 2 factory mode to use multiple factories, this can reduce the coupling, when a new ViewType, just add, can follow the OCP principle.
- 3 we can pre-load the factory, using the map cache, just like the last source code in the implementation idea of factory mode, Android source code also pre-load the factory.
All right, analysis done. Let’s start pumping.
coding
First, we define the data entity:
// Notice that terminalTime refers to the end time, not the time difference, is a time value. // Data class BaseItemBean(val id: Long, var terminalTime: Long, val type: Int)Copy the code
A simple line of code, a Bean object, directly on the data class.
Then, we define two Viewholders, and since they have the same layout, we can use inheritance directly:
ViewHolder open inner class BaseVH(itemView: View) : Recyclerview.viewholder (itemView) {private val tvTimer = ItemView.findViewById <TextView>(R.idv_time) // Delete button private val btnDelete = itemView.findViewById<TextView>(R.id.btn_delete) init { btnDelete.setOnClickListener { onItemDeleteClick? .invoke(adapterPosition)}} // private var delay = 0L private val timerRunnable = Runnable { Log.d(TAG, "run: ${hashCode()}") delay -= 1000 updateTimerState() private fun startTimer() { timerHandler.postDelayed(timerRunnable, 1000)} / / end of the countdown private fun endTimer () {timerHandler. RemoveCallbacks (timerRunnable)} / / testing the countdown And update the status, private fun UpdateTimerState () {if (delay < = 0) {/ / the end of the countdown tvTimer. Text = "end" itemView. SetBackgroundColor (Color, GRAY) endTimer () } else {/ / continue countdown tvTimer... text = "${1000} delay/S" itemView. SetBackgroundColor (Color. ParseColor (" # FFBB86FC ")) */ open fun display(bean: BaseItemBean) {log. d(TAG, "display: $adapterPosition") Delay = bean.terminaltime-system.currenttimemillis () // check and updateTimerState updateTimerState()} D (TAG, "onRecycled: $adapterPosition") // Terminate timer endTimer()}}Copy the code
In the basic ViewHolder, we added the countdown kit and counted and started the countdown when entering the screen, terminated the countdown when sliding out of the screen, recalculated the delay time difference when sliding into the screen next time, and counted down again.
Then look at another ViewHolder:
// Inherits from BaseViewHolder because there is a common countdown kit inner class OnSaleVH(itemView: View) : BaseVH(itemView) {// Add a name private val tvName = ItemView. findViewById<TextView>(r.id_name) Override fun Display (bean: BaseItemBean) {super.display(bean) // add name tvname.text = "${bean.id} in sale "}}Copy the code
Let’s look at the factory where the ViewHolder is created:
/** * define abstract factory */ abstract class VHFactory {abstract fun createVH(context: context, parent: ViewGroup): Inner class BaseVHFactory: VHFactory() {override fun createVH(context: Context, parent: ViewGroup): BaseVH { return BaseVH(LayoutInflater.from(context).inflate(R.layout.item_base, parent, False))}} / inner class OnSaleVHFactory: VHFactory() {override fun createVH(context: override fun createVH) Context, parent: ViewGroup): BaseVH { return OnSaleVH(LayoutInflater.from(context).inflate(R.layout.item_on_sale, parent, false)) } }Copy the code
Very simple. Next, let’s look at the Adapter:
class Adapter(private val datas: List<BaseItemBean>) : Recyclerview.adapter < adapter.basevh >() {private val TAG = "Adapter" /** */ var onItemDeleteClick: ((position: Int) -> Unit)? = null /** * ViewHolder factory */ private val VHS = SparseArray<VHFactory>()/private val timerHandler = Handler(looper.getMainLooper ()) /** * init {VHS. Put (itemType.item_base, BaseVHFactory()) vhs.put(ItemType.ITEM_ON_SALE, Override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseVH = vhs.get(viewType).createvh (parent.context, parent) Override Fun onBindViewHolder(holder: BaseVH, position: Int) = holder.display(datas[position]) override fun getItemCount(): Int = datas.size // ViewHolder is called to slide out of the screen. BaseVH) = holder.onrecycled () /** * Return ViewType */ Override fun getItemViewType(position: Int): Int = datas[position].type }Copy the code
The code is also easy, using factory mode to return different ViewHolder.
The mental journey of writing code:
- 1 Since there are multiple viewTypes, there must be multiple Viewholders, and viewTypes are mapped to viewholders
- You can use if-else, you can use switch-case, but it doesn’t scale well
- 3 So use multiple factories to achieve
- Do you need to create a factory each time onCreateViewHolder()? No, then cache it.
- The cache needs to know which factory creates which ViewHolder, and the ViewHolder corresponds to the ViewType, so we can make the factory correspond to the ViewType, and create a Map.
- 6 ViewType is of type Integer, so SparseArray() can be used more sparingly.
- 7 Thus, we have the above code.
We define viewTypes (both of int type because int matches quickly):
object ItemType {
const val ITEM_BASE = 0x001
const val ITEM_ON_SALE = 0x002
}
Copy the code
Now we can use it in our Activity:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityMainBinding.inflate(layoutInflater) The setContentView (binding. Root) binding. RecyclerView. LayoutManager = LinearLayoutManager (this) / / add test data val beans = ArrayList<BaseItemBean>() for (i in 0.. 100) {// Calculate the end time, Val terminalTime = system.currentTimemillis () + I * 10_000 // ViewType (I %2)+1 beans.add(BaseItemBean(i.toLong(), terminalTime, (I % 2) + 1))} val adapter = adapter (beans) adapter. OnItemDeleteClick = {position - > / / at the click of a button to delete beans. RemoveAt (position) adapter.notifyItemRemoved(position) } binding.recyclerView.adapter = adapter } }Copy the code
The effect is as follows: