Skip to content

Commit

Permalink
Request a new layout if previous smooth scroller was active during la…
Browse files Browse the repository at this point in the history
…yout
  • Loading branch information
rubensousa committed Jul 22, 2024
1 parent cc945e4 commit 3f2940f
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
this, configuration, layoutInfo, pivotSelector, scroller
)
private var hadFocusBeforeLayout = false
private var wasSmoothScrollingBeforeLayout = false
private var recyclerView: DpadRecyclerView? = null
private var isScrollingFromTouchEvent = false
internal var layoutCompletedListener: DpadRecyclerView.OnLayoutCompletedListener? = null
Expand All @@ -85,7 +86,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),

override fun generateLayoutParams(
context: Context,
attrs: AttributeSet
attrs: AttributeSet,
): RecyclerView.LayoutParams {
return DpadLayoutParams(context, attrs)
}
Expand Down Expand Up @@ -138,32 +139,38 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
// If we have focus, save it temporarily since the views will change and we might lose it
hadFocusBeforeLayout = hasFocus()
scroller.cancelSmoothScroller()
wasSmoothScrollingBeforeLayout =
layoutInfo.isScrollingToTarget || scroller.isSearchingPivot()
pivotLayout.onLayoutChildren(recycler, state)
layoutCompletedListener?.onLayoutCompleted(state)
}

override fun onLayoutCompleted(state: RecyclerView.State) {
pivotLayout.onLayoutCompleted(state)
if (hadFocusBeforeLayout) {
focusDispatcher.focusSelectedView(recyclerView)
focusDispatcher.focusSelectedView()
}
if (wasSmoothScrollingBeforeLayout) {
scroller.cancelSmoothScroller()
postOnAnimation { requestLayout() }
}
pivotSelector.onLayoutCompleted()
hadFocusBeforeLayout = false
wasSmoothScrollingBeforeLayout = false
}

override fun collectAdjacentPrefetchPositions(
dx: Int,
dy: Int,
state: RecyclerView.State,
layoutPrefetchRegistry: LayoutPrefetchRegistry
layoutPrefetchRegistry: LayoutPrefetchRegistry,
) {
prefetchCollector.collectAdjacentPrefetchPositions(dx, dy, state, layoutPrefetchRegistry)
}

override fun collectInitialPrefetchPositions(
adapterItemCount: Int,
layoutPrefetchRegistry: LayoutPrefetchRegistry
layoutPrefetchRegistry: LayoutPrefetchRegistry,
) {
prefetchCollector.collectInitialPrefetchPositions(
adapterItemCount = adapterItemCount,
Expand All @@ -176,13 +183,13 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
override fun scrollHorizontallyBy(
dx: Int,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
): Int = pivotLayout.scrollHorizontallyBy(dx, recycler, state)

override fun scrollVerticallyBy(
dy: Int,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
): Int = pivotLayout.scrollVerticallyBy(dy, recycler, state)

override fun computeHorizontalScrollOffset(state: RecyclerView.State): Int {
Expand Down Expand Up @@ -277,7 +284,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
override fun smoothScrollToPosition(
recyclerView: RecyclerView,
state: RecyclerView.State,
position: Int
position: Int,
) {
scroller.scrollToPosition(position, subPosition = 0, smooth = true)
}
Expand Down Expand Up @@ -310,7 +317,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),

override fun onAdapterChanged(
oldAdapter: RecyclerView.Adapter<*>?,
newAdapter: RecyclerView.Adapter<*>?
newAdapter: RecyclerView.Adapter<*>?,
) {
if (oldAdapter != null) {
pivotLayout.reset()
Expand All @@ -333,14 +340,14 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
recyclerView: RecyclerView,
views: ArrayList<View>,
direction: Int,
focusableMode: Int
focusableMode: Int,
): Boolean {
return focusDispatcher.onAddFocusables(recyclerView, views, direction, focusableMode)
}

fun onRequestFocusInDescendants(
direction: Int,
previouslyFocusedRect: Rect?
previouslyFocusedRect: Rect?,
): Boolean {
return focusDispatcher.onRequestFocusInDescendants(direction, previouslyFocusedRect)
}
Expand All @@ -349,7 +356,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
parent: RecyclerView,
state: RecyclerView.State,
child: View,
focused: View?
focused: View?,
): Boolean {
focusDispatcher.onRequestChildFocus(parent, child, focused)
return true
Expand All @@ -360,7 +367,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
parent: RecyclerView,
child: View,
rect: Rect,
immediate: Boolean
immediate: Boolean,
): Boolean = false

override fun onAttachedToWindow(view: RecyclerView) {
Expand All @@ -382,22 +389,22 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),

override fun getRowCountForAccessibility(
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
): Int {
return accessibilityHelper.getRowCountForAccessibility(state)
}

override fun getColumnCountForAccessibility(
recycler: RecyclerView.Recycler,
state: RecyclerView.State
state: RecyclerView.State,
): Int {
return accessibilityHelper.getColumnCountForAccessibility(state)
}

override fun onInitializeAccessibilityNodeInfo(
recycler: RecyclerView.Recycler,
state: RecyclerView.State,
info: AccessibilityNodeInfoCompat
info: AccessibilityNodeInfoCompat,
) {
accessibilityHelper.onInitializeAccessibilityNodeInfo(recycler, state, info)
}
Expand All @@ -406,7 +413,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
recycler: RecyclerView.Recycler,
state: RecyclerView.State,
host: View,
info: AccessibilityNodeInfoCompat
info: AccessibilityNodeInfoCompat,
) {
accessibilityHelper.onInitializeAccessibilityNodeInfoForItem(host, info)
}
Expand All @@ -415,7 +422,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
recycler: RecyclerView.Recycler,
state: RecyclerView.State,
action: Int,
args: Bundle?
args: Bundle?,
): Boolean = accessibilityHelper.performAccessibilityAction(recyclerView, state, action)

override fun onSaveInstanceState(): Parcelable {
Expand Down Expand Up @@ -691,13 +698,13 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(),
}

fun addOnLayoutCompletedListener(
listener: DpadRecyclerView.OnLayoutCompletedListener
listener: DpadRecyclerView.OnLayoutCompletedListener,
) {
pivotLayout.addOnLayoutCompletedListener(listener)
}

fun removeOnLayoutCompletedListener(
listener: DpadRecyclerView.OnLayoutCompletedListener
listener: DpadRecyclerView.OnLayoutCompletedListener,
) {
pivotLayout.removeOnLayoutCompletedListener(listener)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import kotlin.math.min
*/
internal class PivotSelector(
private val layoutManager: LayoutManager,
private val layoutInfo: LayoutInfo
private val layoutInfo: LayoutInfo,
) {

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.forEach
import androidx.recyclerview.widget.OrientationHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
Expand All @@ -30,7 +31,7 @@ import com.rubensousa.dpadrecyclerview.layoutmanager.LayoutConfiguration

internal class LayoutInfo(
private val layout: LayoutManager,
private val configuration: LayoutConfiguration
private val configuration: LayoutConfiguration,
) {

val orientation: Int
Expand Down Expand Up @@ -313,7 +314,7 @@ internal class LayoutInfo(
private fun findFirstChildWithinParentBounds(
startIndex: Int,
endIndex: Int,
onlyCompletelyVisible: Boolean
onlyCompletelyVisible: Boolean,
): Int {
val increment = if (startIndex < endIndex) 1 else -1
var currentIndex = startIndex
Expand Down Expand Up @@ -392,7 +393,7 @@ internal class LayoutInfo(
pivotPosition: Int,
startOldPosition: Int,
endOldPosition: Int,
reverseLayout: Boolean
reverseLayout: Boolean,
): Boolean {
val view = viewHolder.itemView
val layoutParams = view.layoutParams as RecyclerView.LayoutParams
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.rubensousa.dpadrecyclerview.sample.ui.screen.animation

import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.RecyclerView
import com.rubensousa.dpadrecyclerview.OnViewHolderSelectedListener
import com.rubensousa.dpadrecyclerview.compose.DpadComposeFocusViewHolder
import com.rubensousa.dpadrecyclerview.sample.R
import com.rubensousa.dpadrecyclerview.sample.databinding.ScreenRecyclerviewBinding
import com.rubensousa.dpadrecyclerview.sample.ui.model.ListTypes
import com.rubensousa.dpadrecyclerview.sample.ui.viewBinding
import com.rubensousa.dpadrecyclerview.sample.ui.widgets.common.MutableListAdapter
import com.rubensousa.dpadrecyclerview.sample.ui.widgets.common.PlaceholderComposable
import com.rubensousa.dpadrecyclerview.sample.ui.widgets.item.ItemComposable
import com.rubensousa.dpadrecyclerview.sample.ui.widgets.item.MutableGridAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class PredictiveAnimationFragment : Fragment(R.layout.screen_recyclerview) {

private val binding by viewBinding(ScreenRecyclerviewBinding::bind)
private val viewModel by viewModels<PredictiveAnimationViewModel>()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val itemAdapter = PredictiveItemAdapter()
binding.recyclerView.apply {
adapter = itemAdapter
addOnViewHolderSelectedListener(object : OnViewHolderSelectedListener {
override fun onViewHolderSelected(
parent: RecyclerView,
child: RecyclerView.ViewHolder?,
position: Int,
subPosition: Int,
) {
viewModel.loadMore(position)
}
})
requestFocus()
}
viewModel.getItems().observe(viewLifecycleOwner) { items ->
itemAdapter.submitList(items)
}
}

class PredictiveItemAdapter(
) : MutableListAdapter<Int, DpadComposeFocusViewHolder<Int>>(MutableGridAdapter.DIFF_CALLBACK) {

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): DpadComposeFocusViewHolder<Int> {
return when (viewType) {
ListTypes.ITEM -> {
DpadComposeFocusViewHolder(parent) { item ->
ItemComposable(
modifier = Modifier.fillMaxWidth().height(200.dp),
item = item,
)
}
}

else -> {
DpadComposeFocusViewHolder(
parent,
isFocusable = false
) {
PlaceholderComposable(
Modifier.fillMaxWidth().height(300.dp),
)
}
}
}
}

override fun onBindViewHolder(holder: DpadComposeFocusViewHolder<Int>, position: Int) {
val item = getItem(position)
holder.setItemState(item)
holder.itemView.contentDescription = item.toString()
}

override fun getItemViewType(position: Int): Int {
val item = getItem(position)
return if (item >= 0) {
ListTypes.ITEM
} else {
ListTypes.LOADING
}
}

}


class PredictiveAnimationViewModel : ViewModel() {

private val totalItems = 10
private var offset = 0
private var isLoadingMore = false
private val liveData = MutableLiveData<MutableList<Int>>()
private val dispatcher = Dispatchers.Default

init {
loadFirstPage()
}

fun getItems(): LiveData<MutableList<Int>> = liveData

private fun loadFirstPage() {
viewModelScope.launch(dispatcher) {
val newList = mutableListOf<Int>()
repeat(totalItems) {
newList.add(it)
}
liveData.postValue(newList.toMutableList())
offset = totalItems
}
}

fun loadMore(selectedPosition: Int) {
if (isLoadingMore) {
return
}
if (selectedPosition < offset - 2) {
return
}
isLoadingMore = true
viewModelScope.launch(dispatcher) {
val currentList = liveData.value!!
val loadingList = currentList + mutableListOf(-1)
liveData.postValue(loadingList.toMutableList())
delay(5000L)

val newList = MutableList(totalItems + offset) { it }
liveData.postValue(newList)
offset = newList.size
isLoadingMore = false
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ class MainViewModel : ViewModel() {
return FeatureList(
title = "Item animations",
destinations = listOf(
ScreenDestination(
direction = MainFragmentDirections.openPredictiveAnimations(),
title = "Predictive animations"
),
ScreenDestination(
direction = MainFragmentDirections.openItemAnimations(),
title = "Random updates"
Expand Down
Loading

0 comments on commit 3f2940f

Please sign in to comment.