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" /> + +