-
Notifications
You must be signed in to change notification settings - Fork 45
/
InternalAdapter.kt
380 lines (319 loc) · 16.5 KB
/
InternalAdapter.kt
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
package com.idanatz.oneadapter.internal
import android.graphics.Canvas
import android.os.Handler
import android.os.Looper
import android.view.ViewGroup
import androidx.recyclerview.widget.*
import com.idanatz.oneadapter.external.event_hooks.SwipeEventHook
import com.idanatz.oneadapter.external.holders.EmptyIndicator
import com.idanatz.oneadapter.external.holders.LoadingIndicator
import com.idanatz.oneadapter.external.interfaces.Diffable
import com.idanatz.oneadapter.external.modules.*
import com.idanatz.oneadapter.internal.diffing.OneDiffUtil
import com.idanatz.oneadapter.internal.holders.*
import com.idanatz.oneadapter.internal.holders.Metadata as HolderMetadata
import com.idanatz.oneadapter.internal.holders.OneViewHolder
import com.idanatz.oneadapter.internal.holders_creators.ViewHolderCreator
import com.idanatz.oneadapter.internal.holders_creators.ViewHolderCreatorsStore
import com.idanatz.oneadapter.internal.paging.OneScrollListener
import com.idanatz.oneadapter.internal.paging.LoadMoreObserver
import com.idanatz.oneadapter.internal.selection.*
import com.idanatz.oneadapter.internal.swiping.OneItemTouchHelper
import com.idanatz.oneadapter.internal.utils.Logger
import com.idanatz.oneadapter.internal.utils.extensions.createMutableCopyAndApply
import com.idanatz.oneadapter.internal.utils.extensions.isClassExists
import com.idanatz.oneadapter.internal.utils.extensions.isScrolling
import com.idanatz.oneadapter.internal.utils.extensions.removeAllItems
import com.idanatz.oneadapter.internal.utils.extractGenericClass
import com.idanatz.oneadapter.internal.validator.Validator
import java.lang.IllegalStateException
private const val UPDATE_DATA_DELAY_MILLIS = 100L
@Suppress("UNCHECKED_CAST", "NAME_SHADOWING")
internal class InternalAdapter(val recyclerView: RecyclerView) : RecyclerView.Adapter<OneViewHolder<Diffable>>(),
LoadMoreObserver, SelectionObserver, ItemSelectionActions {
internal val modules = Modules()
internal val data: List<Diffable>
get() = differ.currentList
internal val holderVisibilityResolver: HolderVisibilityResolver = HolderVisibilityResolver(this)
private val context
get() = recyclerView.context
private val viewHolderCreatorsStore = ViewHolderCreatorsStore()
private val holderPositionHandler = HolderPositionHandler()
private val logger = Logger(this)
// Paging
private var oneScrollListener: OneScrollListener? = null
// Item Selection
private var oneSelectionHandler: OneSelectionHandler? = null
// Threads Executors
private val uiHandler = Handler(Looper.getMainLooper())
// Diffing
private val listUpdateCallback = object : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) {
logger.logd { "onInserted -> position: $position, count: $count" }
notifyItemRangeInserted(position, count)
}
override fun onRemoved(position: Int, count: Int) {
logger.logd { "onRemoved -> position: $position, count: $count" }
notifyItemRangeRemoved(position, count)
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
logger.logd { "onRemoved -> fromPosition: $fromPosition, toPosition: $toPosition" }
notifyItemMoved(fromPosition, toPosition)
}
override fun onChanged(position: Int, count: Int, payload: Any?) {
logger.logd { "onChanged -> position: $position, count: $count, payload: $payload" }
notifyItemRangeChanged(position, count, payload)
}
}
private val differ = AsyncListDiffer(
listUpdateCallback,
AsyncDifferConfig.Builder(OneDiffUtil()).build()
).apply {
addListListener { _, _ ->
// verify manually if paging should be triggered
// relevant when the new data matches the paging conditions and should not wait for a scroll event
if (modules.pagingModule != null) {
uiHandler.postDelayed({ oneScrollListener?.handleLoadingEvent() }, UPDATE_DATA_DELAY_MILLIS)
}
}
}
init {
setHasStableIds(true)
recyclerView.apply {
adapter = this@InternalAdapter
OneItemTouchHelper().attachToRecyclerView(this)
}
}
//region Traditional Adapter Overrides
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OneViewHolder<Diffable> {
val oneViewHolder = viewHolderCreatorsStore.getCreator(viewType)?.create(parent)
oneViewHolder?.onCreateViewHolder() ?: throw RuntimeException("OneViewHolder creation failed")
logger.logd { "onCreateViewHolder -> classDataType: ${viewHolderCreatorsStore.getClassDataType(viewType)}" }
return oneViewHolder
}
override fun onBindViewHolder(holder: OneViewHolder<Diffable>, position: Int) {
val model = data[position]
val isFirstBind = holderPositionHandler.isFirstBind(holder.itemViewType, position)
val metadata = HolderMetadata(
position = holder.adapterPosition, // don't use position variable for future use, caused bugs with swiping
isRebinding = !isFirstBind && !recyclerView.isScrolling,
isFirst = position == 0,
isLast = position == itemCount - 1,
animationMetadata = object : AnimationMetadata {
override val isAnimatingFirstBind: Boolean = if (holder.firstBindAnimation != null) isFirstBind else false
},
selectionMetadata = object : SelectionMetadata {
override val isSelected: Boolean = isPositionSelected(position)
}
)
logger.logd { "onBindViewHolder -> holder: $holder, model: $model, metadata: $metadata" }
holder.onBindViewHolder(model, metadata)
}
private fun onBindSelection(holder: OneViewHolder<Diffable>, position: Int, selected: Boolean) {
val model = data[position]
logger.logd { "onBindSelection -> holder: $holder, position: $position, model: $model, selected: $selected" }
holder.onBindSelection(model, selected)
}
override fun getItemCount() = data.size
override fun getItemId(position: Int): Long {
val item = data[position]
// javaClass is used for lettings different Diffable models share the same unique identifier
return item.javaClass.name.hashCode() + item.uniqueIdentifier
}
override fun getItemViewType(position: Int) = viewHolderCreatorsStore.getCreatorUniqueIndex(data[position].javaClass)
override fun onViewRecycled(holder: OneViewHolder<Diffable>) {
super.onViewRecycled(holder)
holder.onUnbind(holder.model)
}
//endregion
fun updateData(incomingData: MutableList<Diffable>) {
logger.logd { "updateData -> diffing request with incomingData: $incomingData" }
// handle the fast and simple cases where no diffing is required
when {
incomingData.isEmpty() && data.contains(EmptyIndicator) -> {
uiHandler.post {
logger.logd { "updateData -> no diffing required, refreshing EmptyModule" }
listUpdateCallback.onChanged(0, 1, null)
}
return
}
}
Validator.validateItemsAgainstRegisteredModules(modules.itemModules, incomingData)
// modify the incomingData if needed
when (incomingData.size) {
0 -> {
holderPositionHandler.resetState()
if (modules.emptinessModule != null) { incomingData.add(EmptyIndicator) }
if (modules.pagingModule != null) { oneScrollListener?.resetState() }
}
else -> {
if (modules.emptinessModule != null) incomingData.remove(EmptyIndicator)
if (modules.pagingModule != null) incomingData.remove(LoadingIndicator)
}
}
// handle the diffing
logger.logd { "updateData -> dispatching update with incomingData: $incomingData" }
uiHandler.post {
differ.submitList(incomingData)
}
}
//region Item Module
fun <M : Diffable> register(itemModule: ItemModule<M>) {
val moduleConfig = itemModule.config
val modelClass = extractGenericClass(itemModule.javaClass) as? Class<Diffable> ?: throw IllegalStateException("Unable to extract generic class from ItemModule")
Validator.validateLayoutExists(context, itemModule.javaClass, moduleConfig.layoutResource)
Validator.validateItemModuleAgainstRegisteredModules(modules.itemModules, modelClass)
val layoutResourceId = moduleConfig.layoutResource!!
modules.itemModules[modelClass] = itemModule
viewHolderCreatorsStore.addCreator(modelClass, object : ViewHolderCreator<M> {
override fun create(parent: ViewGroup): OneViewHolder<M> {
return object : OneViewHolder<M>(
parent = parent,
layoutResourceId = layoutResourceId,
firstBindAnimation = moduleConfig.firstBindAnimation,
statesHooksMap = itemModule.states,
eventsHooksMap = itemModule.eventHooks
) {
override fun onCreated() = itemModule.onCreate?.invoke(viewBinder) ?: Unit
override fun onBind(model: M) = itemModule.onBind?.invoke(model, viewBinder, metadata) ?: Unit
override fun onUnbind(model: M) = itemModule.onUnbind?.invoke(model, viewBinder, metadata) ?: Unit
override fun onClicked(model: M) = itemModule.eventHooks.getClickEventHook()?.onClick?.invoke(model, viewBinder, metadata) ?: Unit
override fun onSwipe(canvas: Canvas, xAxisOffset: Float) = itemModule.eventHooks.getSwipeEventHook()?.onSwipe?.invoke(canvas, xAxisOffset, viewBinder) ?: Unit
override fun onSwipeComplete(model: M, swipeDirection: SwipeEventHook.SwipeDirection) = itemModule.eventHooks.getSwipeEventHook()?.onSwipeComplete?.invoke(model, viewBinder, metadata) ?: Unit
override fun onSelected(model: M, selected: Boolean) = itemModule.states.getSelectionState()?.onSelected?.invoke(model, selected) ?: Unit
}
}
} as ViewHolderCreator<Diffable>)
}
//endregion
//region Emptiness Module
fun enableEmptiness(emptinessModule: EmptinessModule) {
val moduleConfig = emptinessModule.config
val modelClass = EmptyIndicator.javaClass as Class<Diffable>
Validator.validateLayoutExists(context, emptinessModule.javaClass, moduleConfig.layoutResource)
val layoutResourceId = moduleConfig.layoutResource!!
modules.emptinessModule = emptinessModule
viewHolderCreatorsStore.addCreator(modelClass, object : ViewHolderCreator<Diffable> {
override fun create(parent: ViewGroup): OneViewHolder<Diffable> {
return object : OneViewHolder<Diffable>(
parent = parent,
layoutResourceId = layoutResourceId,
firstBindAnimation = moduleConfig.firstBindAnimation
) {
override fun onCreated() = emptinessModule.onCreate?.invoke(viewBinder) ?: Unit
override fun onBind(model: Diffable) = emptinessModule.onBind?.invoke(viewBinder, metadata) ?: Unit
override fun onUnbind(model: Diffable) = emptinessModule.onUnbind?.invoke(viewBinder, metadata) ?: Unit
override fun onClicked(model: Diffable) {}
override fun onSwipe(canvas: Canvas, xAxisOffset: Float) {}
override fun onSwipeComplete(model: Diffable, swipeDirection: SwipeEventHook.SwipeDirection) {}
override fun onSelected(model: Diffable, selected: Boolean) {}
}
}
})
configureEmptinessModule()
}
private fun configureEmptinessModule() {
// in case emptiness module is configured, add empty indicator item
if (data.isEmpty() && modules.emptinessModule != null) {
differ.submitList(listOf(EmptyIndicator, *data.toTypedArray()))
}
}
//endregion
//region Paging Module
fun enablePaging(pagingModule: PagingModule) {
val moduleConfig = pagingModule.config
val modelClass = LoadingIndicator.javaClass as Class<Diffable>
Validator.validateLayoutExists(context, pagingModule.javaClass, moduleConfig.layoutResource)
val layoutResourceId = moduleConfig.layoutResource!!
modules.pagingModule = pagingModule
viewHolderCreatorsStore.addCreator(modelClass, object : ViewHolderCreator<Diffable> {
override fun create(parent: ViewGroup): OneViewHolder<Diffable> {
return object : OneViewHolder<Diffable>(
parent = parent,
layoutResourceId = layoutResourceId,
firstBindAnimation = moduleConfig.firstBindAnimation
) {
override fun onCreated() = pagingModule.onCreate?.invoke(viewBinder) ?: Unit
override fun onBind(model: Diffable) = pagingModule.onBind?.invoke(viewBinder, metadata) ?: Unit
override fun onUnbind(model: Diffable) = pagingModule.onUnbind?.invoke(viewBinder, metadata) ?: Unit
override fun onClicked(model: Diffable) {}
override fun onSwipe(canvas: Canvas, xAxisOffset: Float) {}
override fun onSwipeComplete(model: Diffable, swipeDirection: SwipeEventHook.SwipeDirection) {}
override fun onSelected(model: Diffable, selected: Boolean) {}
}
}
})
recyclerView.layoutManager?.let { layoutManager ->
oneScrollListener = OneScrollListener(
layoutManager = layoutManager,
visibleThreshold = moduleConfig.visibleThreshold,
loadMoreObserver = this@InternalAdapter,
logger = logger
).also { recyclerView.addOnScrollListener(it) }
}
}
override fun shouldHandleLoadingEvent(): Boolean = when {
data.contains(EmptyIndicator) -> false // paging should be disabled when EmptyIndicator is present
else -> true
}
override fun onLoadingStateChanged(loading: Boolean) {
if (loading && !data.isClassExists(LoadingIndicator.javaClass)) {
differ.submitList(listOf(*data.toTypedArray(), LoadingIndicator))
// post it to the UI handler because the recycler crashes when calling notify from an onScroll callback
uiHandler.post { notifyItemInserted(data.size) }
}
}
override fun onLoadMore(currentPage: Int) {
modules.pagingModule?.onLoadMore?.invoke(currentPage)
}
//endregion
//region Selection Module
fun enableSelection(itemSelectionModule: ItemSelectionModule) {
itemSelectionModule.actions = this
modules.itemSelectionModule = itemSelectionModule
oneSelectionHandler = OneSelectionHandler(itemSelectionModule, recyclerView).also { it.observer = this }
}
override fun onItemStateChanged(holder: OneViewHolder<Diffable>, position: Int, selected: Boolean) {
onBindSelection(holder, position, selected)
}
override fun onSelectionStarted() {
modules.itemSelectionModule?.onStartSelection?.invoke()
}
override fun onSelectionEnded() {
modules.itemSelectionModule?.onEndSelection?.invoke()
}
override fun onSelectionUpdated(selectedCount: Int) {
modules.itemSelectionModule?.onUpdateSelection?.invoke(selectedCount)
}
override fun startSelection() {
oneSelectionHandler?.startSelection()
}
override fun clearSelection(): Boolean {
return oneSelectionHandler?.clearSelection() ?: false
}
override fun getSelectedPositions(): List<Int> {
return oneSelectionHandler?.getSelectedPositions() ?: emptyList()
}
override fun getSelectedItems(): List<Diffable> {
return oneSelectionHandler?.getSelectedPositions()?.map { position -> data[position] } ?: emptyList()
}
override fun isSelectionActive(): Boolean {
return oneSelectionHandler?.inSelectionActive() ?: false
}
override fun isPositionSelected(position: Int): Boolean {
return oneSelectionHandler?.isPositionSelected(position) ?: false
}
override fun removeSelectedItems() {
val modifiedData = data.createMutableCopyAndApply { removeAllItems(getSelectedItems()) }
updateData(modifiedData)
uiHandler.postDelayed({ clearSelection() }, UPDATE_DATA_DELAY_MILLIS)
}
//endregion
//region RecyclerView
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
oneScrollListener?.let { recyclerView.removeOnScrollListener(it) }
}
//endregion
fun <M : Diffable> getItemViewTypeFromClass(clazz: Class<M>): Int = viewHolderCreatorsStore.getCreatorUniqueIndex(clazz as Class<Diffable>)
fun getItemPosition(item: Diffable) = data.indexOf(item)
}