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 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 { 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) + ) + } } } 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..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 @@ -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 @@ -28,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) ) } @@ -109,5 +110,6 @@ class PendingAlignmentTest : DpadRecyclerViewTest() { } }) } + waitForLayout() } } 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..81c79618 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/PivotLayoutManager.kt @@ -85,7 +85,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(), override fun generateLayoutParams( context: Context, - attrs: AttributeSet + attrs: AttributeSet, ): RecyclerView.LayoutParams { return DpadLayoutParams(context, attrs) } @@ -138,32 +138,34 @@ 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() pivotLayout.onLayoutChildren(recycler, state) - layoutCompletedListener?.onLayoutCompleted(state) + if (hadFocusBeforeLayout) { + focusDispatcher.focusSelectedView() + } + if (layoutInfo.isScrollingToTarget) { + scroller.cancelScrollToTarget() + } + hadFocusBeforeLayout = false } override fun onLayoutCompleted(state: RecyclerView.State) { + layoutCompletedListener?.onLayoutCompleted(state) pivotLayout.onLayoutCompleted(state) - if (hadFocusBeforeLayout) { - focusDispatcher.focusSelectedView(recyclerView) - } pivotSelector.onLayoutCompleted() - hadFocusBeforeLayout = 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 +178,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 +279,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) } @@ -290,7 +292,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,19 +302,17 @@ 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) } override fun onAdapterChanged( oldAdapter: RecyclerView.Adapter<*>?, - newAdapter: RecyclerView.Adapter<*>? + newAdapter: RecyclerView.Adapter<*>?, ) { if (oldAdapter != null) { pivotLayout.reset() @@ -336,14 +335,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) } @@ -352,7 +351,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 @@ -363,7 +362,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(), parent: RecyclerView, child: View, rect: Rect, - immediate: Boolean + immediate: Boolean, ): Boolean = false override fun onAttachedToWindow(view: RecyclerView) { @@ -385,14 +384,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) } @@ -400,7 +399,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) } @@ -409,7 +408,7 @@ class PivotLayoutManager(properties: Properties) : RecyclerView.LayoutManager(), recycler: RecyclerView.Recycler, state: RecyclerView.State, host: View, - info: AccessibilityNodeInfoCompat + info: AccessibilityNodeInfoCompat, ) { accessibilityHelper.onInitializeAccessibilityNodeInfoForItem(host, info) } @@ -418,7 +417,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 { @@ -694,13 +693,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/focus/FocusDispatcher.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/layoutmanager/focus/FocusDispatcher.kt index ca87751d..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 } @@ -111,9 +113,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 @@ -125,7 +126,7 @@ internal class FocusDispatcher( fun onInterceptFocusSearch( recyclerView: DpadRecyclerView?, focused: View, - direction: Int + direction: Int, ): View? { val currentRecyclerView = recyclerView ?: return focused @@ -242,7 +243,7 @@ internal class FocusDispatcher( recyclerView: RecyclerView, views: ArrayList, direction: Int, - focusableMode: Int + focusableMode: Int, ): Boolean { if (configuration.isFocusSearchDisabled(recyclerView)) { if (recyclerView.isFocusable) { @@ -281,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 @@ -311,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 @@ -379,7 +380,7 @@ internal class FocusDispatcher( request: AddFocusableChildrenRequest, views: ArrayList, direction: Int, - focusableMode: Int + focusableMode: Int, ) { var index = request.start val increment = request.increment @@ -429,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 @@ -483,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 @@ -531,7 +532,7 @@ internal class FocusDispatcher( focusedChild: View?, focusedChildIndex: Int, focusedAdapterPosition: Int, - focusDirection: FocusDirection + focusDirection: FocusDirection, ) { this.focused = focusedChild this.focusedAdapterPosition = focusedAdapterPosition 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/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/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() 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/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/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..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(2000L) + delay(3000L) listLiveData.postValue(ArrayList(list)) }.invokeOnCompletion { loadingStateLiveData.postValue(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" /> + +