From 89a5516b42a7867091bef2c1dfb2ed34c0a22ac2 Mon Sep 17 00:00:00 2001 From: Ruben Sousa Date: Fri, 12 Jul 2024 02:59:20 +0200 Subject: [PATCH 01/12] Always trigger layout instead of trying to skip it based on item changes --- .../layoutmanager/PivotLayoutManager.kt | 3 -- .../layoutmanager/layout/PivotLayout.kt | 20 +--------- .../layoutmanager/layout/StructureEngineer.kt | 40 ------------------- .../LinearLayoutEngineerVerticalTest.kt | 1 - .../sample/ui/screen/list/ListFragment.kt | 7 ---- .../sample/ui/screen/list/ListViewModel.kt | 2 +- 6 files changed, 2 insertions(+), 71 deletions(-) diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt index f6353f62..4bdf64e0 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt @@ -290,7 +290,6 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(), override fun onItemsAdded(recyclerView: RecyclerView, positionStart: Int, itemCount: Int) { layoutInfo.invalidateSpanCache() - pivotLayout.onItemsAdded(positionStart, itemCount) pivotSelector.onItemsAdded(positionStart, itemCount) } @@ -301,13 +300,11 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(), override fun onItemsRemoved(recyclerView: RecyclerView, positionStart: Int, itemCount: Int) { layoutInfo.invalidateSpanCache() - pivotLayout.onItemsRemoved(positionStart, itemCount) pivotSelector.onItemsRemoved(positionStart, itemCount) } override fun onItemsMoved(recyclerView: RecyclerView, from: Int, to: Int, itemCount: Int) { layoutInfo.invalidateSpanCache() - pivotLayout.onItemsMoved(from, to, itemCount) pivotSelector.onItemsMoved(from, to, itemCount) } diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt index a8b9ee1c..ea10d3f0 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/PivotLayout.kt @@ -49,7 +49,6 @@ internal class PivotLayout( private var layoutListener: OnChildLaidOutListener? = null private var structureEngineer = createStructureEngineer() private val layoutCompleteListeners = ArrayList() - private val itemChanges = ItemChanges() private var anchor: Int? = null private var initialSelectionPending = false @@ -137,7 +136,7 @@ internal class PivotLayout( saveAnchorState() } - structureEngineer.layoutChildren(pivotSelector.position, itemChanges, recycler, state) + structureEngineer.layoutChildren(pivotSelector.position, recycler, state) if (configuration.keepLayoutAnchor) { restoreAnchorState(recycler, state) @@ -191,7 +190,6 @@ internal class PivotLayout( initialSelectionPending = false updateInitialSelection() } - itemChanges.reset() layoutInfo.onLayoutCompleted() layoutCompleteListeners.forEach { listener -> listener.onLayoutCompleted(state) @@ -228,22 +226,6 @@ internal class PivotLayout( structureEngineer.clear() } - fun onItemsAdded(positionStart: Int, itemCount: Int) { - itemChanges.insertionPosition = positionStart - itemChanges.insertionItemCount = itemCount - } - - fun onItemsRemoved(positionStart: Int, itemCount: Int) { - itemChanges.removalPosition = positionStart - itemChanges.removalItemCount = itemCount - } - - fun onItemsMoved(from: Int, to: Int, itemCount: Int) { - itemChanges.moveFromPosition = from - itemChanges.moveToPosition = to - itemChanges.moveItemCount = itemCount - } - fun setOnChildLaidOutListener(listener: OnChildLaidOutListener?) { layoutListener = listener } diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/StructureEngineer.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/StructureEngineer.kt index 9275c781..668fafc2 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/StructureEngineer.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/StructureEngineer.kt @@ -184,17 +184,9 @@ internal abstract class StructureEngineer( fun layoutChildren( pivotPosition: Int, - itemChanges: ItemChanges, recycler: RecyclerView.Recycler, state: RecyclerView.State ) { - if (!isNewLayoutRequired(state, itemChanges)) { - if (DpadRecyclerView.DEBUG) { - Log.i(TAG, "layout changes are out of bounds, so skip full layout: $itemChanges") - } - finishLayout() - return - } recyclerViewProvider.updateRecycler(recycler) // Start by detaching all existing views. @@ -295,38 +287,6 @@ internal abstract class StructureEngineer( } } - /** - * We only need to do a full layout in the following scenarios: - * - * 1. There's a structural change in the adapter - * 2. There are no items in the current layout - * 3. Pivot is no longer aligned - * 4. Item changes affect the current visible window - */ - private fun isNewLayoutRequired( - state: RecyclerView.State, - itemChanges: ItemChanges - ): Boolean { - if (state.didStructureChange() - || !itemChanges.isValid() - || preLayoutRequest.extraLayoutSpace > 0 - || layoutRequest.loopDirection != DpadLoopDirection.NONE - ) { - return true - } - val firstPos = layoutInfo.findFirstAddedPosition() - val lastPos = layoutInfo.findLastAddedPosition() - if (firstPos == RecyclerView.NO_POSITION || lastPos == RecyclerView.NO_POSITION) { - return true - } - val changesOutOfBounds = if (!layoutRequest.reverseLayout) { - itemChanges.isOutOfBounds(firstPos, lastPos) - } else { - itemChanges.isOutOfBounds(lastPos, firstPos) - } - return !changesOutOfBounds - } - fun scrollBy( offset: Int, recycler: RecyclerView.Recycler, diff --git a/dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/test/layoutmanager/linear/LinearLayoutEngineerVerticalTest.kt b/dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/test/layoutmanager/linear/LinearLayoutEngineerVerticalTest.kt index 7cc3a55f..bb584f41 100644 --- a/dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/test/layoutmanager/linear/LinearLayoutEngineerVerticalTest.kt +++ b/dpadrecyclerview/src/test/java/com/rubensousa/dpadrecyclerview/test/layoutmanager/linear/LinearLayoutEngineerVerticalTest.kt @@ -126,7 +126,6 @@ class LinearLayoutEngineerVerticalTest { engineer.onLayoutStarted(recyclerViewStateMock.get()) engineer.layoutChildren( pivotPosition, - itemChanges, recyclerMock.get(), recyclerViewStateMock.get() ) diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListFragment.kt index 248cc3b1..a9d58e25 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListFragment.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListFragment.kt @@ -24,9 +24,7 @@ import androidx.fragment.app.viewModels import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.RecyclerView -import com.rubensousa.dpadrecyclerview.BuildConfig import com.rubensousa.dpadrecyclerview.DpadRecyclerView -import com.rubensousa.dpadrecyclerview.DpadSelectionSnapHelper import com.rubensousa.dpadrecyclerview.OnViewHolderSelectedListener import com.rubensousa.dpadrecyclerview.ParentAlignment import com.rubensousa.dpadrecyclerview.sample.R @@ -89,11 +87,6 @@ class ListFragment : Fragment(R.layout.screen_recyclerview) { binding.selectionOverlayView.isActivated = true binding.recyclerView.apply { adapter = concatAdapter - // Include this for debug builds if you want to use touch events on the emulator - // or if you need to support touch events on the device (automotive or mobile) - if (BuildConfig.DEBUG) { - DpadSelectionSnapHelper().attachToRecyclerView(this) - } requestFocus() } } diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListViewModel.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListViewModel.kt index c0149fce..90ce093f 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListViewModel.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListViewModel.kt @@ -37,7 +37,7 @@ class ListViewModel : ViewModel() { loadingStateLiveData.postValue(true) viewModelScope.launch(Dispatchers.Default) { list.addAll(createPage()) - delay(2000L) + delay(7500L) listLiveData.postValue(ArrayList(list)) }.invokeOnCompletion { loadingStateLiveData.postValue(false) } From 06ef3f1c75b13681a40f75ad00fc7c91127f73b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Mon, 22 Jul 2024 18:51:36 +0100 Subject: [PATCH 02/12] Do not check for animation state after layout completes --- .../dpadrecyclerview/layoutmanager/focus/FocusDispatcher.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/focus/FocusDispatcher.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/focus/FocusDispatcher.kt index ca87751d..147ce9ac 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/focus/FocusDispatcher.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/focus/FocusDispatcher.kt @@ -111,9 +111,8 @@ internal class FocusDispatcher( } } - fun focusSelectedView(recyclerView: RecyclerView?) { - val currentRecyclerView = recyclerView ?: return - if (!configuration.isFocusSearchDisabled(currentRecyclerView)) { + fun focusSelectedView() { + if (configuration.isFocusSearchDisabled) { return } val view = layoutInfo.findViewByAdapterPosition(pivotSelector.position) ?: return From cc945e4176c7b6675e580e71f95d6b87f77b024a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Mon, 22 Jul 2024 18:52:58 +0100 Subject: [PATCH 03/12] Mark compose view as not focusable when appropriate --- .../dpadrecyclerview/compose/DpadComposeFocusViewHolder.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolder.kt b/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolder.kt index 1b850d32..179edfc8 100644 --- a/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolder.kt +++ b/dpadrecyclerview-compose/src/main/java/com/rubensousa/dpadrecyclerview/compose/DpadComposeFocusViewHolder.kt @@ -47,7 +47,7 @@ class DpadComposeFocusViewHolder( parent: ViewGroup, compositionStrategy: ViewCompositionStrategy = RecyclerViewCompositionStrategy.DisposeOnRecycled, isFocusable: Boolean = true, - private val content: @Composable (item: T) -> Unit = {} + private val content: @Composable (item: T) -> Unit = {}, ) : RecyclerView.ViewHolder(ComposeView(parent.context)) { private val itemState = mutableStateOf(null) @@ -71,6 +71,10 @@ class DpadComposeFocusViewHolder( fun setFocusable(focusable: Boolean) { composeView.apply { + if (!isFocusable) { + isFocusable = false + isFocusableInTouchMode = false + } descendantFocusability = if (focusable) { ViewGroup.FOCUS_AFTER_DESCENDANTS } else { From 3f2940f3db4e40eb8b570a058b28a91d53619806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Mon, 22 Jul 2024 19:03:09 +0100 Subject: [PATCH 04/12] Request a new layout if previous smooth scroller was active during layout --- .../layoutmanager/PivotLayoutManager.kt | 47 +++--- .../layoutmanager/PivotSelector.kt | 2 +- .../layoutmanager/layout/LayoutInfo.kt | 7 +- .../animation/PredictiveAnimationFragment.kt | 153 ++++++++++++++++++ .../sample/ui/screen/main/MainViewModel.kt | 4 + .../sample/ui/widgets/item/ItemComposable.kt | 9 -- sample/src/main/res/navigation/nav_graph.xml | 8 + 7 files changed, 197 insertions(+), 33 deletions(-) create mode 100644 sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/animation/PredictiveAnimationFragment.kt diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt index 4bdf64e0..05cb6743 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt @@ -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 @@ -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) } @@ -138,7 +139,8 @@ 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) } @@ -146,24 +148,29 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(), 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, @@ -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 { @@ -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) } @@ -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() @@ -333,14 +340,14 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(), recyclerView: RecyclerView, views: ArrayList, 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) } @@ -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 @@ -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) { @@ -382,14 +389,14 @@ 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) } @@ -397,7 +404,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(), override fun onInitializeAccessibilityNodeInfo( recycler: RecyclerView.Recycler, state: RecyclerView.State, - info: AccessibilityNodeInfoCompat + info: AccessibilityNodeInfoCompat, ) { accessibilityHelper.onInitializeAccessibilityNodeInfo(recycler, state, info) } @@ -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) } @@ -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 { @@ -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) } diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotSelector.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotSelector.kt index c7926f77..f6def3a6 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotSelector.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotSelector.kt @@ -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 { diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt index 261a7b83..db98fd64 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/layout/LayoutInfo.kt @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/animation/PredictiveAnimationFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/animation/PredictiveAnimationFragment.kt new file mode 100644 index 00000000..03413c52 --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/animation/PredictiveAnimationFragment.kt @@ -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() + + 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>(MutableGridAdapter.DIFF_CALLBACK) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): DpadComposeFocusViewHolder { + 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, 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>() + private val dispatcher = Dispatchers.Default + + init { + loadFirstPage() + } + + fun getItems(): LiveData> = liveData + + private fun loadFirstPage() { + viewModelScope.launch(dispatcher) { + val newList = mutableListOf() + 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 + } + } + } + +} diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt index bb7bb76a..f26d212b 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt @@ -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" diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemComposable.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemComposable.kt index 5fb406fe..6788b705 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemComposable.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemComposable.kt @@ -59,14 +59,6 @@ fun ItemComposable( onClick: () -> Unit = {}, ) { var isFocused by remember { mutableStateOf(false) } - val scaleState = animateFloatAsState( - targetValue = if (isFocused) 1.1f else 1.0f, - label = "scale", - animationSpec = tween( - durationMillis = if (isFocused) 350 else 0, - easing = FastOutSlowInEasing - ) - ) val backgroundColor = if (isFocused) { Color.White } else { @@ -79,7 +71,6 @@ fun ItemComposable( } Box( modifier = modifier - .scale(scaleState.value) .clip(RoundedCornerShape(8.dp)) .background(backgroundColor) .onFocusChanged { focusState -> diff --git a/sample/src/main/res/navigation/nav_graph.xml b/sample/src/main/res/navigation/nav_graph.xml index ab321229..022b4195 100644 --- a/sample/src/main/res/navigation/nav_graph.xml +++ b/sample/src/main/res/navigation/nav_graph.xml @@ -72,6 +72,10 @@ android:id="@+id/open_item_animations" app:destination="@id/item_animations_fragment" /> + + @@ -206,6 +210,10 @@ android:id="@+id/item_animations_fragment" android:name="com.rubensousa.dpadrecyclerview.sample.ui.screen.animation.ItemAnimationsFragment" /> + + From e0af99815249127a60d7c29d190f185850073612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Mon, 22 Jul 2024 19:28:05 +0100 Subject: [PATCH 05/12] Schedule view focus in the next frame since it can be called during a remove --- .../layoutmanager/focus/FocusDispatcher.kt | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/focus/FocusDispatcher.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/focus/FocusDispatcher.kt index 147ce9ac..045473e4 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/focus/FocusDispatcher.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/focus/FocusDispatcher.kt @@ -40,7 +40,7 @@ internal class FocusDispatcher( private val scroller: LayoutScroller, private val layoutInfo: LayoutInfo, private val pivotSelector: PivotSelector, - private val spanFocusFinder: SpanFocusFinder + private val spanFocusFinder: SpanFocusFinder, ) { private val addFocusableChildrenRequest = AddFocusableChildrenRequest(layoutInfo) @@ -103,7 +103,9 @@ internal class FocusDispatcher( val view = layout.findViewByPosition(index) ?: break if (layoutInfo.isViewFocusable(view)) { if (!view.hasFocus()) { - pivotSelector.focus(view) + view.postOnAnimation { + pivotSelector.focus(view) + } } break } @@ -124,7 +126,7 @@ internal class FocusDispatcher( fun onInterceptFocusSearch( recyclerView: DpadRecyclerView?, focused: View, - direction: Int + direction: Int, ): View? { val currentRecyclerView = recyclerView ?: return focused @@ -241,7 +243,7 @@ internal class FocusDispatcher( recyclerView: RecyclerView, views: ArrayList, direction: Int, - focusableMode: Int + focusableMode: Int, ): Boolean { if (configuration.isFocusSearchDisabled(recyclerView)) { if (recyclerView.isFocusable) { @@ -280,7 +282,7 @@ internal class FocusDispatcher( */ fun onRequestFocusInDescendants( direction: Int, - previouslyFocusedRect: Rect? + previouslyFocusedRect: Rect?, ): Boolean { if (configuration.isFocusSearchDisabled) return false val view = layout.findViewByPosition(pivotSelector.position) ?: return false @@ -310,7 +312,7 @@ internal class FocusDispatcher( recyclerView: RecyclerView, views: ArrayList, direction: Int, - focusableMode: Int + focusableMode: Int, ) { if (layout.childCount == 0) { // No need to continue since there's no children @@ -378,7 +380,7 @@ internal class FocusDispatcher( request: AddFocusableChildrenRequest, views: ArrayList, direction: Int, - focusableMode: Int + focusableMode: Int, ) { var index = request.start val increment = request.increment @@ -428,7 +430,7 @@ internal class FocusDispatcher( movement: FocusDirection, views: ArrayList, direction: Int, - focusableMode: Int + focusableMode: Int, ): Boolean { if (configuration.spanCount == 1 || focusedPosition == RecyclerView.NO_POSITION) { return false @@ -482,7 +484,7 @@ internal class FocusDispatcher( next: Boolean, views: ArrayList, direction: Int, - focusableMode: Int + focusableMode: Int, ): Boolean { val positionIncrement = layoutInfo.getPositionIncrement(next) val nextPosition = focusedPosition + positionIncrement @@ -530,7 +532,7 @@ internal class FocusDispatcher( focusedChild: View?, focusedChildIndex: Int, focusedAdapterPosition: Int, - focusDirection: FocusDirection + focusDirection: FocusDirection, ) { this.focused = focusedChild this.focusedAdapterPosition = focusedAdapterPosition From 4772a42bae69491279eb9cb6713294b630804817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Mon, 22 Jul 2024 19:44:59 +0100 Subject: [PATCH 06/12] Only cancel explicit pivot selections and not new pivot searches --- .../dpadrecyclerview/layoutmanager/PivotLayoutManager.kt | 9 ++------- .../layoutmanager/scroll/LayoutScroller.kt | 6 ++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt index 05cb6743..a118d830 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt @@ -75,7 +75,6 @@ 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 @@ -139,8 +138,6 @@ 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() - wasSmoothScrollingBeforeLayout = - layoutInfo.isScrollingToTarget || scroller.isSearchingPivot() pivotLayout.onLayoutChildren(recycler, state) layoutCompletedListener?.onLayoutCompleted(state) } @@ -150,13 +147,11 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(), if (hadFocusBeforeLayout) { focusDispatcher.focusSelectedView() } - if (wasSmoothScrollingBeforeLayout) { - scroller.cancelSmoothScroller() - postOnAnimation { requestLayout() } + if (layoutInfo.isScrollingToTarget) { + scroller.cancelScrollToTarget() } pivotSelector.onLayoutCompleted() hadFocusBeforeLayout = false - wasSmoothScrollingBeforeLayout = false } override fun collectAdjacentPrefetchPositions( diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/scroll/LayoutScroller.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/scroll/LayoutScroller.kt index d464d3c4..26f94e9e 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/scroll/LayoutScroller.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/scroll/LayoutScroller.kt @@ -238,6 +238,12 @@ internal class LayoutScroller( fun isSearchingPivot() = searchPivotScroller != null + fun cancelScrollToTarget() { + layoutInfo.setIsScrollingToTarget(false) + pivotSelectionScroller?.cancel() + pivotSelectionScroller = null + } + fun cancelSmoothScroller() { layoutInfo.setIsScrollingToTarget(false) searchPivotScroller?.cancel() From 6a136e1d93871e9b3f7402b7461f1733ddaf05fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Mon, 22 Jul 2024 19:45:18 +0100 Subject: [PATCH 07/12] Decrease delay --- .../dpadrecyclerview/sample/ui/screen/list/ListViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListViewModel.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListViewModel.kt index 90ce093f..9ee954cc 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListViewModel.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/list/ListViewModel.kt @@ -37,7 +37,7 @@ class ListViewModel : ViewModel() { loadingStateLiveData.postValue(true) viewModelScope.launch(Dispatchers.Default) { list.addAll(createPage()) - delay(7500L) + delay(3000L) listLiveData.postValue(ArrayList(list)) }.invokeOnCompletion { loadingStateLiveData.postValue(false) } From 4cfcb2b72a87ee3d109b588970c140bb2bc4e54b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Tue, 23 Jul 2024 11:59:31 +0100 Subject: [PATCH 08/12] Wait for layout before continuing --- .../test/tests/scrolling/PendingAlignmentTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/PendingAlignmentTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/PendingAlignmentTest.kt index 0f34b587..dd586a65 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/PendingAlignmentTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/PendingAlignmentTest.kt @@ -11,6 +11,7 @@ import com.rubensousa.dpadrecyclerview.test.TestLayoutConfiguration import com.rubensousa.dpadrecyclerview.test.helpers.assertFocusAndSelection import com.rubensousa.dpadrecyclerview.test.helpers.onRecyclerView import com.rubensousa.dpadrecyclerview.test.helpers.waitForIdleScrollState +import com.rubensousa.dpadrecyclerview.test.helpers.waitForLayout import com.rubensousa.dpadrecyclerview.test.tests.DpadRecyclerViewTest import com.rubensousa.dpadrecyclerview.testing.KeyEvents import com.rubensousa.dpadrecyclerview.testing.R @@ -109,5 +110,6 @@ class PendingAlignmentTest : DpadRecyclerViewTest() { } }) } + waitForLayout() } } From a8335436703e1bb3c59a93a78559b95474faaa30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Tue, 23 Jul 2024 12:28:19 +0100 Subject: [PATCH 09/12] Move scroll control logic to onLayoutChildren since it can be called multiple times for the same layout pass --- .../layoutmanager/PivotLayoutManager.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt index a118d830..81c79618 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt @@ -139,21 +139,21 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(), // If we have focus, save it temporarily since the views will change and we might lose it hadFocusBeforeLayout = hasFocus() pivotLayout.onLayoutChildren(recycler, state) - layoutCompletedListener?.onLayoutCompleted(state) - } - - override fun onLayoutCompleted(state: RecyclerView.State) { - pivotLayout.onLayoutCompleted(state) if (hadFocusBeforeLayout) { focusDispatcher.focusSelectedView() } if (layoutInfo.isScrollingToTarget) { scroller.cancelScrollToTarget() } - pivotSelector.onLayoutCompleted() hadFocusBeforeLayout = false } + override fun onLayoutCompleted(state: RecyclerView.State) { + layoutCompletedListener?.onLayoutCompleted(state) + pivotLayout.onLayoutCompleted(state) + pivotSelector.onLayoutCompleted() + } + override fun collectAdjacentPrefetchPositions( dx: Int, dy: Int, From bd4729d8854330c394aae518fac032ade0fdc81d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Tue, 23 Jul 2024 12:35:53 +0100 Subject: [PATCH 10/12] Improve ViewHolder focus detection in assertion --- .../assertions/DpadRecyclerViewAssertions.kt | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/assertions/DpadRecyclerViewAssertions.kt b/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/assertions/DpadRecyclerViewAssertions.kt index c146c501..2add0f16 100644 --- a/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/assertions/DpadRecyclerViewAssertions.kt +++ b/dpadrecyclerview-testing/src/main/java/com/rubensousa/dpadrecyclerview/testing/assertions/DpadRecyclerViewAssertions.kt @@ -41,7 +41,7 @@ object DpadRecyclerViewAssertions { private class SelectionAssertion( private val position: Int, - private val subPosition: Int = 0 + private val subPosition: Int = 0, ) : DpadRecyclerViewAssertion() { override fun check(view: DpadRecyclerView) { @@ -52,21 +52,12 @@ object DpadRecyclerViewAssertions { private class FocusAssertion( private val focusedPosition: Int, - private val focusedSubPosition: Int = 0 + private val focusedSubPosition: Int = 0, ) : DpadRecyclerViewAssertion() { override fun check(view: DpadRecyclerView) { - val focusedView = view.findFocus() ?: throw AssertionFailedError( - "DpadRecyclerView didn't have focus: ${HumanReadables.describe(view)}" - ) - - val viewHolder = view.findContainingViewHolder(focusedView) - ?: throw AssertionFailedError( - "ViewHolder not found for position " + - "$focusedPosition and sub position $focusedSubPosition" - ) - - assertThat(viewHolder.absoluteAdapterPosition).isEqualTo(focusedPosition) + val viewHolder = view.findViewHolderForAdapterPosition(focusedPosition) + ?: throw AssertionFailedError("ViewHolder not found for position $focusedPosition") val alignments = getAlignments(viewHolder) if (alignments.isEmpty() && focusedSubPosition > 0) { @@ -75,11 +66,23 @@ object DpadRecyclerViewAssertions { "View: ${HumanReadables.describe(view)}" ) } else if (alignments.isNotEmpty()) { + val focusedView = viewHolder.itemView.findFocus() + ?: throw AssertionFailedError( + "ViewHolder at $focusedPosition didn't have focus: " + + HumanReadables.describe(viewHolder.itemView) + ) val alignment = alignments[focusedSubPosition] val expectedView = viewHolder.itemView.findViewById( alignment.getFocusViewId() ) assertThat(focusedView).isEqualTo(expectedView) + } else { + if (!viewHolder.itemView.hasFocus()) { + throw AssertionFailedError( + "ViewHolder at $focusedPosition didn't have focus: " + + HumanReadables.describe(viewHolder.itemView) + ) + } } } From 4526a1d3e631ae243bc860b6fb16c5f243704d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Tue, 23 Jul 2024 12:40:49 +0100 Subject: [PATCH 11/12] Add .kotlin to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b3ff17c4..fba55646 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ .cxx local.properties /test_coverage -/logs \ No newline at end of file +/logs +/.kotlin \ No newline at end of file From 145b087d816dde9b4abf04cefafb60cbb73ce6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Sousa?= Date: Tue, 23 Jul 2024 13:13:45 +0100 Subject: [PATCH 12/12] Align all items to center since the first one is recycled after scrolling --- .../test/tests/scrolling/PendingAlignmentTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/PendingAlignmentTest.kt b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/PendingAlignmentTest.kt index dd586a65..12de9b68 100644 --- a/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/PendingAlignmentTest.kt +++ b/dpadrecyclerview/src/androidTest/kotlin/com/rubensousa/dpadrecyclerview/test/tests/scrolling/PendingAlignmentTest.kt @@ -29,10 +29,10 @@ class PendingAlignmentTest : DpadRecyclerViewTest() { spans = 1, orientation = RecyclerView.HORIZONTAL, parentAlignment = ParentAlignment( - edge = ParentAlignment.Edge.MIN_MAX, - fraction = 0.0f + edge = ParentAlignment.Edge.NONE, + fraction = 0.5f ), - childAlignment = ChildAlignment(offset = 0, fraction = 0.0f) + childAlignment = ChildAlignment(offset = 0, fraction = 0.5f) ) }