Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gridrecyclerview with height 0.5 of the screen height - alignment problem (last row cut off) #203

Closed
fankloano opened this issue Mar 22, 2024 · 14 comments · Fixed by #204
Closed
Labels
bug Something isn't working
Milestone

Comments

@fankloano
Copy link

Hello,

hopefully you can help me to solve my issue. I wanted to implement a grid-recyclerview, that fills only 0.5 percentage of the height of the screen. I am using following code in the xml layout:

<com.rubensousa.dpadrecyclerview.DpadRecyclerView android:id="@+id/rv_layout_Movies" android:layout_width="0dp" android:layout_height="0dp" android:orientation="vertical" app:spanCount="8" app:layout_constraintStart_toEndOf="@id/linLayout_movieAccounts_categories" app:layout_constraintHeight_percent="0.5" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintWidth_percent="1.0" android:nextFocusRight="@id/tv_showFullEpg" tools:listitem="@layout/rv_item_movies"/>

What I want to achieve is, that the grid-list fills the half height of the screen and the user can navigate throw this list (right/left + up/down). Everything works as aspected, except that the last row of items is cut off. Like in the following picture (green are the cards, red the background)
grid_current

And how it should be (sorry I used Paint to "design" this :-) ):
grid_wanted

I tried to change the edge alignments and modified the constraints and similar but I couldn't get it to work correctly . (using match_parent, so that the whole screen height is filled, would work). Is there some work-around or did I miss something?

ps: When I navigate to the last item of the last row (so the item on the far right) the last row suddenly is aligned correctly (as in the second picture).

thanks in advance

@fankloano fankloano changed the title Gridlayout with height 0.5 of the screen height - alignment problem (last row cut off) Gridrecyclerview with height 0.5 of the screen height - alignment problem (last row cut off) Mar 22, 2024
@rubensousa
Copy link
Owner

Hi @fankloano . Thank you for the detailed bug report, this will help me a lot to investigate.
Will let you know once a fix is available.

@rubensousa rubensousa added the bug Something isn't working label Mar 22, 2024
@rubensousa rubensousa added this to the 1.3.0 milestone Mar 22, 2024
@rubensousa
Copy link
Owner

Fix here: #204

Will be available in 1.3.0-alpha02

@rubensousa
Copy link
Owner

@fankloano 1.3.0-alpha02 is already available. Please let me know if the problem was solved in that version

@fankloano
Copy link
Author

@rubensousa
sorry for my late answer. The issue seems solved with 1.3.0-alpha02, thanks :-)
Another thing, regarding to the same recyclerview: I just realised that the first image/cardview is smaller then the others. I checked if I have set a margin or something similar that eventually could cause the problem, but there isn't set any. Can you reproduce it?
(should I open a new issue?)

@rubensousa
Copy link
Owner

@fankloano yes, please open a new issue and attach a sample project that reproduces it if possible. I won't be able to fix that one so soon since I will be away for vacation

@tyrel-carlson
Copy link

Hi there, I hope you are doing well

I'm facing an issue that when I use setSpanSizeLookup, the last row of items won't get focused.
I've upgraded the library version to 1.3.0-alpha02 but the problem persists.

Is it related to this issue or do I need to open another one?

@rubensousa
Copy link
Owner

Hi there @tyrel-carlson . No, this issue should be solved. Please create a new one, ideally with reproduction steps and a video recording

@tyrel-carlson
Copy link

Sorry for late answer, I'm using the dpad recycler with a span count of 4 integrated with a spanSizeLookup to decide if the span size should be 1 or 4 depending on the item type (header or cell) in a sectioned grid view.

the problem is that the last row of items (including a date header) is not getting focused unless I do a fast scrolling but not from direct upper items, so for your information I'm not allowed to publish any screen recording of the production app, so I provide some code here and let me know if I'm missing something, if not I'll open an issue:

private fun setupRecyclerView() = with(binding) {
        recycler.adapter = adapter

        recycler.setSpanCount(SPAN_COUNT)

        recycler.setSpanSizeLookup(object : DpadSpanSizeLookup() {
            override fun getSpanSize(position: Int): Int {
                return if (position < adapter.itemCount
                    && adapter.isSectionHeader(position)
                ) SPAN_COUNT else 1
            }
        })

        recycler.attachListController(viewModel.listController)
    }

    companion object {
        const val SPAN_COUNT = 4
    }

open fun isSectionHeader(position: Int): Boolean {
        return differ.currentList[position] is DataHolder.Section<*>
    }

fun DpadRecyclerView.attachListController(
    controller: StreamingListController
) {
    val listener = object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)

            val layoutManager = recyclerView.layoutManager as? PivotLayoutManager
                ?: return

            val firstVisibleIndex = layoutManager.findFirstVisibleItemPosition()
            val lastVisibleIndex = max(firstVisibleIndex, layoutManager.findLastVisibleItemPosition())

            controller.setIndexRange(range = StreamingListController.IndexRange(
                start = firstVisibleIndex,
                endInclusive = lastVisibleIndex
            ))
        }
    }

    addOnScrollListener(listener)
}

<com.rubensousa.dpadrecyclerview.DpadRecyclerView
        android:id="@+id/recycler"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_marginHorizontal="10dp"
        android:clipToPadding="false"
        android:paddingTop="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/guideline"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

@tyrel-carlson
Copy link

I'm still stuck on this, can you please help me with that? @rubensousa

@rubensousa
Copy link
Owner

rubensousa commented May 15, 2024

Please open a new issue with a video recording and attach this code snippet above. @tyrel-carlson

@rubensousa
Copy link
Owner

@tyrel-carlson did you solve this problem?

@tyrel-carlson
Copy link

tyrel-carlson commented May 31, 2024

Yeah, I managed to solve this using my own focusSearch. Here is my solution, so feel free to use it to improve the repo.

import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.rubensousa.dpadrecyclerview.DpadRecyclerView

class SectionableRecyclerView : DpadRecyclerView {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)

    init {
        installGlobalLayoutListener {
            // Here we prevent the focus from being lost when the user focuses the empty recyclerview
            if (hasFocus() && childCount > 1) {
                val focused = findFocus()
                if (focused == null || focused is RecyclerView) {
                    getChildAt(1)?.requestFocus()
                }
            }
        }
    }

    override fun setAdapter(adapter: Adapter<*>?) {
        if (adapter != null && adapter !is SectionedListAdapter<*, *, *, *>) {
            throw IllegalArgumentException("Must be a SectionedListAdapter")
        }

        super.setAdapter(adapter)
    }

    override fun focusSearch(focused: View, direction: Int): View? {
        try {
            val adapter = adapter as SectionedListAdapter<*, *, *, *>

            // Get the current position of the focused view
            val currentPosition = getChildAdapterPosition(focused)
            if (currentPosition == NO_POSITION) {
                return super.focusSearch(focused, direction)
            }

            // Find the next position based on the focus direction
            val nextPosition = when (direction) {
                View.FOCUS_UP,
                View.FOCUS_DOWN -> findNextPosition(adapter, currentPosition, direction)

                else -> return super.focusSearch(focused, direction)
            }

            // Ensure the next position is valid
            if (nextPosition == NO_POSITION || !isPositionValid(adapter, nextPosition)) {
                return when (direction) {
                    View.FOCUS_UP -> parent.focusSearch(focused, direction)
                    else -> super.focusSearch(focused, direction)
                }
            }

            // This is to prevent the recycler from reverting to previously focused item
            // in some situations
            layoutManager?.scrollToPosition(nextPosition)

            return findViewAtPosition(nextPosition) ?: return super.focusSearch(focused, direction)

        } catch (e: Exception) {
            return super.focusSearch(focused, direction)
        }
    }

    /**
     * Finds the next valid position to focus on when navigating in the specified direction.
     *
     * @param adapter The adapter of the RecyclerView, which must be a SectionedListAdapter.
     * @param currentPosition The current position of the focused item.
     * @param direction The direction of the focus movement, either View.FOCUS_DOWN or View.FOCUS_UP.
     * @return The next valid position to focus on, or NO_POSITION if no valid position is found.
     */
    private fun findNextPosition(adapter: SectionedListAdapter<*, *, *, *>, currentPosition: Int, direction: Int): Int {
        val spanCount = getSpanCount()
        val currentSpanIndex = getSpanIndex(currentPosition, spanCount)

        return when (direction) {
            View.FOCUS_DOWN -> {
                var nextPosition = currentPosition + 1

                while (
                    isPositionValid(adapter, nextPosition) &&
                    (currentSpanIndex != getSpanIndex(nextPosition, spanCount) || adapter.isSectionHeader(nextPosition))
                ) {
                    nextPosition++
                }

                // Prevent unusual focus movement if the destination cell is empty
                if (nextPosition - currentPosition > (spanCount + 1))
                    findNextPosition(adapter, currentPosition - 1, direction)
                else nextPosition
            }

            View.FOCUS_UP -> {
                var previousPosition = currentPosition - 1

                while (
                    isPositionValid(adapter, previousPosition) &&
                    (currentSpanIndex != getSpanIndex(previousPosition, spanCount) || adapter.isSectionHeader(previousPosition))
                ) {
                    previousPosition--
                }

                // Prevent unusual focus movement if the destination cell is empty
                if (currentPosition - previousPosition > (spanCount + 1))
                    findNextPosition(adapter, currentPosition - 1, direction)
                else previousPosition
            }

            else -> NO_POSITION
        }
    }

    /**
     * Checks if the given position is valid within the adapter's item count.
     *
     * @param adapter The adapter of the RecyclerView, which must be a SectionedListAdapter.
     * @param position The position to check.
     * @return True if the position is within the bounds of the adapter's item count, false otherwise.
     */
    private fun isPositionValid(adapter: SectionedListAdapter<*, *, *, *>, position: Int): Boolean {
        return position in 0 until adapter.itemCount
    }

    /**
     * Retrieves the span index for the given position in the grid.
     *
     * @param position The position of the item.
     * @param spanCount The number of spans in the grid.
     */
    private fun getSpanIndex(position: Int, spanCount: Int): Int {
        return getSpanSizeLookup().getSpanIndex(position, spanCount)
    }

    /**
     * Finds the view at the specified adapter position.
     *
     * @param position The adapter position of the item.
     * @return The view at the specified position, or null if no view is found.
     */
    private fun findViewAtPosition(position: Int): View? {
        val layoutManager = layoutManager ?: return null
        return (0 until layoutManager.childCount)
            .mapNotNull { layoutManager.getChildAt(it) }
            .firstOrNull { getChildAdapterPosition(it) == position }
    }

}

@rubensousa
Copy link
Owner

Can you please raise a new issue with the original problem so I can have a look for the next release? Meanwhile 1.3.0-alpha03 should remove the need for that workaround you have for focus in that constructor

@tyrel-carlson
Copy link

Sure, I will open a new issue. FYI, I've tested 1.3.0-alpha03, and it doesn't solve this issue yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants