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 @@
+
+