diff --git a/dpadrecyclerview/api/dpadrecyclerview.api b/dpadrecyclerview/api/dpadrecyclerview.api index 5d675765..f5c16e5b 100644 --- a/dpadrecyclerview/api/dpadrecyclerview.api +++ b/dpadrecyclerview/api/dpadrecyclerview.api @@ -40,9 +40,12 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle public final fun addRecyclerListener (Landroidx/recyclerview/widget/RecyclerView$RecyclerListener;)V public final fun clearOnLayoutCompletedListeners ()V public final fun clearOnViewHolderSelectedListeners ()V + protected fun dispatchDraw (Landroid/graphics/Canvas;)V protected final fun dispatchGenericFocusedEvent (Landroid/view/MotionEvent;)Z public final fun dispatchKeyEvent (Landroid/view/KeyEvent;)Z public fun dispatchTouchEvent (Landroid/view/MotionEvent;)Z + public final fun enableMaxEdgeFading (Z)V + public final fun enableMinEdgeFading (Z)V public final fun findFirstCompletelyVisibleItemPosition ()I public final fun findFirstVisibleItemPosition ()I public final fun findLastCompletelyVisibleItemPosition ()I @@ -52,6 +55,10 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle public final fun getCurrentSubPositions ()I public final fun getFocusableDirection ()Lcom/rubensousa/dpadrecyclerview/FocusableDirection; public final fun getInitialPrefetchItemCount ()I + public final fun getMaxEdgeFadingLength ()I + public final fun getMaxEdgeFadingOffset ()I + public final fun getMinEdgeFadingLength ()I + public final fun getMinEdgeFadingOffset ()I public final fun getOnKeyInterceptListener ()Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView$OnKeyInterceptListener; public final fun getOnMotionInterceptListener ()Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView$OnMotionInterceptListener; public final fun getOnUnhandledKeyListener ()Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView$OnUnhandledKeyListener; @@ -69,11 +76,14 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle public final fun isItemPrefetchEnabled ()Z public final fun isLayoutEnabled ()Z public final fun isLayoutReversed ()Z + public final fun isMaxEdgeFadingEnabled ()Z + public final fun isMinEdgeFadingEnabled ()Z public final fun isScrollEnabled ()Z protected final fun onFocusChanged (ZILandroid/graphics/Rect;)V protected final fun onRequestFocusInDescendants (ILandroid/graphics/Rect;)Z public final fun onRtlPropertiesChanged (I)V public fun onScrollStateChanged (I)V + protected fun onSizeChanged (IIII)V public final fun removeOnLayoutCompletedListener (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView$OnLayoutCompletedListener;)V public final fun removeOnViewHolderSelectedListener (Lcom/rubensousa/dpadrecyclerview/OnViewHolderSelectedListener;)V public final fun removeView (Landroid/view/View;)V @@ -83,6 +93,7 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle public static synthetic fun setChildAlignment$default (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView;Lcom/rubensousa/dpadrecyclerview/ChildAlignment;ZILjava/lang/Object;)V public final fun setChildDrawingOrderCallback (Landroidx/recyclerview/widget/RecyclerView$ChildDrawingOrderCallback;)V public final fun setExtraLayoutSpaceStrategy (Lcom/rubensousa/dpadrecyclerview/ExtraLayoutSpaceStrategy;)V + public final fun setFadingEdgeLength (I)V public final fun setFocusDrawingOrderEnabled (Z)V public final fun setFocusOutAllowed (ZZ)V public final fun setFocusOutSideAllowed (ZZ)V @@ -97,6 +108,10 @@ public class com/rubensousa/dpadrecyclerview/DpadRecyclerView : androidx/recycle public final fun setItemPrefetchEnabled (Z)V public final fun setLayoutEnabled (Z)V public final fun setLayoutManager (Landroidx/recyclerview/widget/RecyclerView$LayoutManager;)V + public final fun setMaxEdgeFadingLength (I)V + public final fun setMaxEdgeFadingOffset (I)V + public final fun setMinEdgeFadingLength (I)V + public final fun setMinEdgeFadingOffset (I)V public final fun setOnChildLaidOutListener (Lcom/rubensousa/dpadrecyclerview/OnChildLaidOutListener;)V public final fun setOnKeyInterceptListener (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView$OnKeyInterceptListener;)V public final fun setOnMotionInterceptListener (Lcom/rubensousa/dpadrecyclerview/DpadRecyclerView$OnMotionInterceptListener;)V diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt index 36a3b47d..f9d76d52 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadRecyclerView.kt @@ -18,6 +18,7 @@ package com.rubensousa.dpadrecyclerview import android.content.Context import android.content.res.TypedArray +import android.graphics.Canvas import android.graphics.Rect import android.util.AttributeSet import android.view.Gravity @@ -25,6 +26,7 @@ import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.view.animation.Interpolator +import androidx.annotation.Px import androidx.core.view.ViewCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -57,6 +59,7 @@ open class DpadRecyclerView @JvmOverloads constructor( private val viewHolderTaskExecutor = ViewHolderTaskExecutor() private val focusableChildDrawingCallback = FocusableChildDrawingCallback() + private val fadingEdge = FadingEdge() private var pivotLayoutManager: PivotLayoutManager? = null private var isOverlappingRenderingEnabled = true @@ -97,6 +100,13 @@ open class DpadRecyclerView @JvmOverloads constructor( // Call setItemAnimator to set it up this.itemAnimator = itemAnimator + val fadingEdgeLength = typedArray.getDimensionPixelOffset( + R.styleable.DpadRecyclerView_android_fadingEdgeLength, 0 + ) + if (fadingEdgeLength > 0) { + setFadingEdgeLength(fadingEdgeLength) + } + setWillNotDraw(true) setChildDrawingOrderCallback(focusableChildDrawingCallback) overScrollMode = OVER_SCROLL_NEVER @@ -357,6 +367,45 @@ open class DpadRecyclerView @JvmOverloads constructor( } } + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + fadingEdge.onSizeChanged(w, h, oldw, oldh, this) + } + + final override fun setFadingEdgeLength(length: Int) { + super.setFadingEdgeLength(length) + layoutManager?.let { + enableMinEdgeFading(true) + enableMaxEdgeFading(true) + setMaxEdgeFadingLength(length) + setMinEdgeFadingLength(length) + } + } + + override fun dispatchDraw(canvas: Canvas) { + val applyMinEdgeFading = fadingEdge.isMinFadingEdgeRequired(this) + val applyMaxEdgeFading = fadingEdge.isMaxFadingEdgeRequired(this) + if (!applyMaxEdgeFading && !applyMinEdgeFading) { + super.dispatchDraw(canvas) + return + } + val minFadeLength = if (applyMinEdgeFading) fadingEdge.minShaderLength else 0 + val maxFadeLength = if (applyMaxEdgeFading) fadingEdge.maxShaderLength else 0 + val minEdge = fadingEdge.getMinEdge(this) + val maxEdge = fadingEdge.getMaxEdge(this) + + val save = canvas.save() + fadingEdge.clip(minEdge, maxEdge, applyMinEdgeFading, applyMaxEdgeFading, canvas, this) + super.dispatchDraw(canvas) + if (minFadeLength > 0) { + fadingEdge.drawMin(canvas, this) + } + if (maxFadeLength > 0) { + fadingEdge.drawMax(canvas, this) + } + canvas.restoreToCount(save) + } + /** * Sets the strategy for calculating extra layout space. * @@ -402,6 +451,81 @@ open class DpadRecyclerView @JvmOverloads constructor( */ fun isLayoutEnabled(): Boolean = requireLayout().isLayoutEnabled() + /** + * Enables fading out the min edge to transparent. + * @param enable true if edge fading should be enabled for the left or top of the layout + */ + fun enableMinEdgeFading(enable: Boolean) { + fadingEdge.enableMinEdgeFading(enable, this) + } + + /** + * @return true if edge fading is enabled for the left or top of the layout + */ + fun isMinEdgeFadingEnabled(): Boolean = fadingEdge.isFadingMinEdge + + /** + * Sets the length of the fading effect applied to the min edge in pixels + */ + fun setMinEdgeFadingLength(@Px length: Int) { + fadingEdge.setMinEdgeFadingLength(length, this) + } + + /** + * See: [setMinEdgeFadingLength] + */ + fun getMinEdgeFadingLength(): Int = fadingEdge.minShaderLength + + /** + * Sets the start position of the fading effect applied to the min edge in pixels. + * Default is 0, which means that the fading effect starts from the min edge (left or top) + */ + fun setMinEdgeFadingOffset(@Px offset: Int) { + fadingEdge.setMinEdgeFadingOffset(offset, this) + } + + /** + * See: [setMinEdgeFadingOffset] + */ + fun getMinEdgeFadingOffset(): Int = fadingEdge.minShaderOffset + + /** + * Enables fading out the max edge to transparent. + * @param enable true if edge fading should be enabled for the right or bottom of the layout + */ + fun enableMaxEdgeFading(enable: Boolean) { + fadingEdge.enableMaxEdgeFading(enable, this) + } + + /** + * @return true if edge fading is enabled for the right or bottom of the layout + */ + fun isMaxEdgeFadingEnabled(): Boolean = fadingEdge.isFadingMaxEdge + + /** + * Sets the length of the fading effect applied to the max edge in pixels + */ + fun setMaxEdgeFadingLength(@Px length: Int) { + fadingEdge.setMaxEdgeFadingLength(length, this) + } + + /** + * See: [setMaxEdgeFadingLength] + */ + fun getMaxEdgeFadingLength(): Int = fadingEdge.maxShaderLength + + /** + * Sets the length of the fading effect applied to the min edge in pixels + */ + fun setMaxEdgeFadingOffset(@Px offset: Int) { + fadingEdge.setMaxEdgeFadingOffset(offset, this) + } + + /** + * See: [setMaxEdgeFadingOffset] + */ + fun getMaxEdgeFadingOffset(): Int = fadingEdge.maxShaderOffset + /** * Enables or disables the default rule of drawing the selected view after all other views. * Default is true @@ -1007,7 +1131,7 @@ open class DpadRecyclerView @JvmOverloads constructor( && position != NO_POSITION && position == getSelectedPosition() ) { - pivotLayoutManager?.removeCurrentViewHolderSelection() + pivotLayoutManager?.removeCurrentViewHolderSelection() } } } diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/FadingEdge.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/FadingEdge.kt new file mode 100644 index 00000000..606a8aad --- /dev/null +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/FadingEdge.kt @@ -0,0 +1,301 @@ +/* + * Copyright 2023 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.LinearGradient +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.Rect +import android.graphics.Shader +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +internal class FadingEdge { + + var isFadingMinEdge = false + private set + + var minShaderLength = 0 + private set + + var minShaderOffset = 0 + private set + + var isFadingMaxEdge = false + private set + + var maxShaderLength = 0 + private set + + var maxShaderOffset = 0 + private set + + private var minShader: LinearGradient? = null + private var maxShader: LinearGradient? = null + private val rect = Rect() + private val paint = Paint() + + init { + paint.apply { + xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) + isDither = true + } + } + + fun onSizeChanged( + width: Int, + height: Int, + oldWidth: Int, + oldHeight: Int, + recyclerView: DpadRecyclerView + ) { + if (maxShaderLength == 0) return + var changed = false + if (recyclerView.getOrientation() == RecyclerView.HORIZONTAL) { + if (width != oldWidth) { + maxShader = createMaxHorizontalShader(width, recyclerView.paddingRight) + changed = true + } + } else if (height != oldHeight) { + maxShader = createMaxVerticalShader(height, recyclerView.paddingBottom) + changed = true + } + if (changed) { + recyclerView.invalidate() + } + } + + fun enableMinEdgeFading(enable: Boolean, recyclerView: DpadRecyclerView) { + if (isFadingMinEdge == enable) return + isFadingMinEdge = enable + recyclerView.invalidate() + updateLayerType(recyclerView) + } + + fun setMinEdgeFadingLength(length: Int, recyclerView: DpadRecyclerView) { + if (minShaderLength == length) return + minShaderLength = length + minShader = if (minShaderLength != 0) { + if (recyclerView.getOrientation() == RecyclerView.HORIZONTAL) { + LinearGradient( + 0f, 0f, minShaderLength.toFloat(), 0f, + Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP + ) + } else { + LinearGradient( + 0f, 0f, 0f, minShaderLength.toFloat(), + Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP + ) + } + } else { + null + } + recyclerView.invalidate() + } + + fun setMinEdgeFadingOffset(offset: Int, recyclerView: DpadRecyclerView) { + if (minShaderOffset != offset) { + minShaderOffset = offset + recyclerView.invalidate() + } + } + + fun enableMaxEdgeFading(enable: Boolean, recyclerView: DpadRecyclerView) { + if (isFadingMaxEdge == enable) return + isFadingMaxEdge = enable + recyclerView.invalidate() + updateLayerType(recyclerView) + } + + fun setMaxEdgeFadingLength(length: Int, recyclerView: DpadRecyclerView) { + if (maxShaderLength == length) return + maxShaderLength = length + maxShader = if (maxShaderLength != 0) { + if (recyclerView.getOrientation() == RecyclerView.HORIZONTAL) { + createMaxHorizontalShader(recyclerView.width, recyclerView.paddingRight) + } else { + createMaxVerticalShader(recyclerView.height, recyclerView.paddingBottom) + } + } else { + null + } + recyclerView.invalidate() + } + + fun setMaxEdgeFadingOffset(offset: Int, recyclerView: DpadRecyclerView) { + if (maxShaderOffset != offset) { + maxShaderOffset = offset + recyclerView.invalidate() + } + } + + fun isMinFadingEdgeRequired(recyclerView: DpadRecyclerView): Boolean { + if (!isFadingMinEdge) { + return false + } + val childCount = recyclerView.childCount + if (childCount == 0) return false + val child = recyclerView.getChildAt(0) + val isHorizontal = recyclerView.getOrientation() == RecyclerView.HORIZONTAL + val first = isFirstItemView(child, recyclerView) + val childStart: Int + val start: Int + if (isHorizontal) { + childStart = child.left + start = recyclerView.paddingLeft + } else { + childStart = child.top + start = recyclerView.paddingTop + } + return (childStart < start + minShaderOffset && !first) || (childStart < start && first) + } + + fun isMaxFadingEdgeRequired(recyclerView: DpadRecyclerView): Boolean { + if (!isFadingMaxEdge) { + return false + } + val childCount = recyclerView.childCount + if (childCount == 0) return false + val isHorizontal = recyclerView.getOrientation() == RecyclerView.HORIZONTAL + val child = recyclerView.getChildAt(childCount - 1) + val last = isLastItemView(child, recyclerView) + val childEnd: Int + val end: Int + if (isHorizontal) { + childEnd = child.right + end = recyclerView.width - recyclerView.paddingRight + } else { + childEnd = child.bottom + end = recyclerView.height - recyclerView.paddingBottom + } + return (childEnd > end - maxShaderOffset && !last) || (childEnd > end && last) + } + + fun getMinEdge(recyclerView: DpadRecyclerView): Int { + if (!isFadingMinEdge) return 0 + val padding = if (recyclerView.getOrientation() == RecyclerView.HORIZONTAL) { + recyclerView.paddingLeft + } else { + recyclerView.paddingTop + } + return padding + minShaderOffset + } + + fun getMaxEdge(recyclerView: DpadRecyclerView): Int { + var padding = 0 + var size = 0 + if (recyclerView.getOrientation() == RecyclerView.HORIZONTAL) { + padding = recyclerView.paddingRight + size = recyclerView.width + } else { + padding = recyclerView.paddingBottom + size = recyclerView.height + } + if (!isFadingMaxEdge) return size + return size - padding - maxShaderOffset + } + + fun clip( + minEdge: Int, + maxEdge: Int, + applyMinFading: Boolean, + applyMaxFading: Boolean, + canvas: Canvas, + recyclerView: DpadRecyclerView + ) { + if (recyclerView.getOrientation() == RecyclerView.HORIZONTAL) { + val start = if (applyMinFading) minEdge else 0 + val end = if (applyMaxFading) maxEdge else recyclerView.width + canvas.clipRect(start, 0, end, recyclerView.height) + } else { + val top = if (applyMinFading) minEdge else 0 + val bottom = if (applyMaxFading) maxEdge else recyclerView.height + canvas.clipRect(0, top, recyclerView.width, bottom) + } + } + + fun drawMin(canvas: Canvas, recyclerView: DpadRecyclerView) { + paint.shader = minShader + if (recyclerView.getOrientation() == RecyclerView.HORIZONTAL) { + rect.top = 0 + rect.bottom = recyclerView.height + rect.left = minShaderOffset + rect.right = minShaderOffset + minShaderLength + } else { + rect.left = 0 + rect.right = recyclerView.width + rect.top = minShaderOffset + rect.bottom = minShaderOffset + minShaderLength + } + canvas.drawRect(rect, paint) + } + + fun drawMax(canvas: Canvas, recyclerView: DpadRecyclerView) { + paint.shader = maxShader + if (recyclerView.getOrientation() == RecyclerView.HORIZONTAL) { + rect.top = 0 + rect.bottom = recyclerView.height + rect.right = recyclerView.width - recyclerView.paddingRight - maxShaderOffset + rect.left = rect.right - maxShaderLength + } else { + rect.left = 0 + rect.right = recyclerView.width + rect.bottom = recyclerView.height - recyclerView.paddingBottom - maxShaderOffset + rect.top = rect.bottom - maxShaderLength + } + canvas.drawRect(rect, paint) + } + + private fun isFirstItemView(view: View, recyclerView: DpadRecyclerView): Boolean { + return recyclerView.getChildLayoutPosition(view) == 0 + } + + private fun isLastItemView(view: View, recyclerView: DpadRecyclerView): Boolean { + val itemCount = recyclerView.adapter?.itemCount ?: 0 + return recyclerView.getChildLayoutPosition(view) == itemCount - 1 + } + + private fun createMaxHorizontalShader(width: Int, paddingEnd: Int): LinearGradient { + val end = width.toFloat() - paddingEnd.toFloat() - maxShaderOffset + return LinearGradient( + end - maxShaderLength, 0f, end, 0f, + Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP + ) + } + + private fun createMaxVerticalShader(height: Int, paddingBottom: Int): LinearGradient { + val bottom = height.toFloat() - paddingBottom.toFloat() - maxShaderOffset + return LinearGradient( + 0f, bottom - maxShaderLength, 0f, bottom, + Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP + ) + } + + private fun updateLayerType(recyclerView: DpadRecyclerView) { + if (isFadingMinEdge || isFadingMaxEdge) { + recyclerView.setLayerType(View.LAYER_TYPE_HARDWARE, null) + recyclerView.setWillNotDraw(false) + } else { + recyclerView.setLayerType(View.LAYER_TYPE_NONE, null) + recyclerView.setWillNotDraw(true) + } + } + +} diff --git a/dpadrecyclerview/src/main/res/values/attrs.xml b/dpadrecyclerview/src/main/res/values/attrs.xml index a906c007..2ccdbc76 100644 --- a/dpadrecyclerview/src/main/res/values/attrs.xml +++ b/dpadrecyclerview/src/main/res/values/attrs.xml @@ -29,6 +29,7 @@ + diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/DimensionExtensions.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/DimensionExtensions.kt new file mode 100644 index 00000000..d476e889 --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/DimensionExtensions.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui + +import androidx.compose.ui.unit.Dp +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.RecyclerView +import kotlin.math.roundToInt + +fun Fragment.dpToPx(dimension: Dp): Int { + return (resources.displayMetrics.density * dimension.value).roundToInt() +} + +fun RecyclerView.ViewHolder.dpToPx(dimension: Dp): Int { + return (itemView.resources.displayMetrics.density * dimension.value).roundToInt() +} \ No newline at end of file diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/fading/FadingEdgeFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/fading/FadingEdgeFragment.kt new file mode 100644 index 00000000..ddff9a96 --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/fading/FadingEdgeFragment.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2023 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.screen.fading + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.rubensousa.dpadrecyclerview.sample.R +import com.rubensousa.dpadrecyclerview.sample.databinding.FadingAdapterListBinding +import com.rubensousa.dpadrecyclerview.sample.databinding.HorizontalAdapterAnimatedItemBinding +import com.rubensousa.dpadrecyclerview.sample.databinding.ScreenFadingEdgesBinding +import com.rubensousa.dpadrecyclerview.sample.ui.dpToPx +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.item.ItemViewHolder +import com.rubensousa.dpadrecyclerview.spacing.DpadLinearSpacingDecoration + +class FadingEdgeFragment : Fragment(R.layout.screen_fading_edges) { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val binding = ScreenFadingEdgesBinding.bind(view) + val adapter = Adapter() + adapter.submitList( + listOf( + Configuration( + title = "Fade start", + minEdgeLength = 60.dp, + ), + Configuration( + title = "Fade both sides with end offset", + minEdgeLength = 60.dp, + maxEdgeLength = 60.dp, + maxEdgeOffset = 16.dp + ), + Configuration( + title = "Fade both sides with large end fading", + minEdgeLength = 60.dp, + maxEdgeLength = 120.dp, + maxEdgeOffset = 24.dp + ), + ) + ) + binding.dpadRecyclerView.adapter = adapter + binding.dpadRecyclerView.requestFocus() + } + + data class Configuration( + val title: String, + val minEdgeLength: Dp = 0.dp, + val minEdgeOffset: Dp = 0.dp, + val maxEdgeLength: Dp = 0.dp, + val maxEdgeOffset: Dp = 0.dp + ) + + class Adapter : ListAdapter( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Configuration, newItem: Configuration): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame( + oldItem: Configuration, + newItem: Configuration + ): Boolean { + return oldItem == newItem + } + }) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RowViewHolder { + return RowViewHolder( + FadingAdapterListBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + } + + override fun onBindViewHolder(holder: RowViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + } + + + class RowViewHolder(val binding: FadingAdapterListBinding) : + RecyclerView.ViewHolder(binding.root) { + + private val adapter = RowAdapter() + + init { + binding.recyclerView.adapter = adapter + } + + fun bind(configuration: Configuration) { + binding.textView.text = configuration.title + binding.recyclerView.apply { + addItemDecoration(DpadLinearSpacingDecoration.create(itemSpacing = dpToPx(16.dp))) + enableMinEdgeFading(configuration.minEdgeLength > 0.dp) + setMinEdgeFadingLength(dpToPx(configuration.minEdgeLength)) + setMinEdgeFadingOffset(dpToPx(configuration.minEdgeOffset)) + + enableMaxEdgeFading(configuration.maxEdgeLength > 0.dp) + setMaxEdgeFadingLength(dpToPx(configuration.maxEdgeLength)) + setMaxEdgeFadingOffset(dpToPx(configuration.maxEdgeOffset)) + } + } + + } + + class RowAdapter : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + val binding = HorizontalAdapterAnimatedItemBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return ItemViewHolder( + binding.root, binding.textView, animateFocusChanges = false + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(position, null) + holder.itemView.isFocusable = true + holder.itemView.isFocusableInTouchMode = true + } + + override fun getItemCount(): Int = 25 + + } + + +} diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/focus/SearchPivotFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/focus/SearchPivotFragment.kt index 3c039092..1eaf2548 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/focus/SearchPivotFragment.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/focus/SearchPivotFragment.kt @@ -20,29 +20,39 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.unit.dp +import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import com.rubensousa.dpadrecyclerview.sample.R -import com.rubensousa.dpadrecyclerview.sample.databinding.AdapterItemRowBinding +import com.rubensousa.dpadrecyclerview.sample.databinding.HorizontalAdapterAnimatedItemBinding import com.rubensousa.dpadrecyclerview.sample.databinding.ScreenLeanbackHorizontalBinding +import com.rubensousa.dpadrecyclerview.sample.ui.dpToPx import com.rubensousa.dpadrecyclerview.sample.ui.widgets.RecyclerViewLogger import com.rubensousa.dpadrecyclerview.sample.ui.widgets.item.ItemViewHolder +import com.rubensousa.dpadrecyclerview.spacing.DpadLinearSpacingDecoration class SearchPivotFragment : Fragment(R.layout.screen_leanback_horizontal) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val binding = ScreenLeanbackHorizontalBinding.bind(view) + binding.horizontalGridView.clipToPadding = false + binding.horizontalGridView.updatePadding(left = dpToPx(24.dp)) + binding.horizontalGridView.setItemSpacing(dpToPx(24.dp)) binding.horizontalGridView.adapter = Adapter() RecyclerViewLogger.logChildrenWhenIdle(binding.horizontalGridView) RecyclerViewLogger.logChildrenWhenIdle(binding.dpadRecyclerView) + binding.dpadRecyclerView.addItemDecoration( + DpadLinearSpacingDecoration.create(dpToPx(24.dp)) + ) binding.dpadRecyclerView.adapter = Adapter() } class Adapter : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { - val binding = AdapterItemRowBinding.inflate( + val binding = HorizontalAdapterAnimatedItemBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return ItemViewHolder( 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 87976e1d..ba419722 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 @@ -52,6 +52,10 @@ class MainViewModel : ViewModel() { direction = MainFragmentDirections.openList(), title = "Nested" ), + ScreenDestination( + direction = MainFragmentDirections.openFadingEdge(), + title = "Fading Edges" + ), ScreenDestination( direction = MainFragmentDirections.openList().apply { showOverlay = true @@ -66,7 +70,7 @@ class MainViewModel : ViewModel() { ScreenDestination( direction = MainFragmentDirections.openList().apply { reverseLayout = true }, title = "Reversed" - ) + ), ), ) } diff --git a/sample/src/main/res/layout/fading_adapter_list.xml b/sample/src/main/res/layout/fading_adapter_list.xml new file mode 100644 index 00000000..4e8ab0f3 --- /dev/null +++ b/sample/src/main/res/layout/fading_adapter_list.xml @@ -0,0 +1,34 @@ + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/screen_fading_edges.xml b/sample/src/main/res/layout/screen_fading_edges.xml new file mode 100644 index 00000000..08162fde --- /dev/null +++ b/sample/src/main/res/layout/screen_fading_edges.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/sample/src/main/res/navigation/nav_graph.xml b/sample/src/main/res/navigation/nav_graph.xml index f29ed717..b49e8c5d 100644 --- a/sample/src/main/res/navigation/nav_graph.xml +++ b/sample/src/main/res/navigation/nav_graph.xml @@ -58,7 +58,11 @@ + app:destination="@id/detail_fragment" /> + + @@ -153,4 +157,8 @@ + +