AndroidFastScroll icon indicating copy to clipboard operation
AndroidFastScroll copied to clipboard

Smarter position provided by getPopupText(position)

Open herbeth1u opened this issue 4 years ago • 8 comments

Actual Behavior

I implemented in my app a fullscreen RecyclerView with getPopupText support. It looks like this: The problem is, the position provided by getPopupText is the first visible item in the Adapter. Though as you can see here, this item (framed in red) is actually barely visible. So while the user only sees items starting with "L", the popup still shows "K", because the very last "K" item still hasn't been recycled.

I was thinking maybe there is a smarter way than getFirstItemAdapterPosition() to get this position. As I'm a bit curious, I investigated how other fast scrollers handle this issue. For instance, Nova Launcher's widget list uses a fast scroll which applies some kind of padding to this position, proportional to how low you are in the list (the more you scroll, the lower the item targeted by getPopupText is on the screen). Which enables their popup to use the very first item's position when you're at the top, the very last item's position when you're at the bottom, and the position of an item which is always visible when you're anywhere in between.

It's kinda hard to explain with words. Actually experimenting with it helps to understand. I am also aware that the Google Contacts app works in a similar way (using the first visible item), though it's not a problem because their list is not full screen, and the list snaps when fast scrolling, to make sure the first item is always fully visible. Not all lists can mimic this behavior though.

Expected Behavior

The position provided by getPopupText should target an item which is always (clearly) visible to the user.

Steps to Reproduce the Problem

  1. Create a RecyclerView and simply bind a FastScroller with FastScrollerBuilder(recyclerView).build() (the issue is more visible with a full screen RV).
  2. Implement PopupTextProvider in the Adapter.
  3. Scroll.

Specifications

  • Version: v1.1.0
  • Platform: Android 10, OnePlus 6 OxygenOS 10.3.1, no root

Don't hesitate to ask for more details if it wasn't clear. :) Thanks!

herbeth1u avatar Feb 15 '20 23:02 herbeth1u

I have the same problem but I wonder if this is solved if we implement sections and add PopupTextProvider to the section instead of each item, that's what Google Contacts app does, will try to implement this.

eamiguinho avatar Apr 30 '20 16:04 eamiguinho

@zhanghai Wouldn't it be better to replace the implementation of RecyclerViewHelper.getFirstItemAdapterPosition by something that uses LinearLayoutManager's FindXXXVisibleItemPosition instead of fetching the first visible view of the layout and then computing its position ?

This way it would be possible to give the user/developer control over which method to use between :

  • findFirstVisibleItemPosition (= more or less what the current implementation does)
  • findFirstCompletelyVisibleItemPosition (= what the OP seems to want)
  • findLastVisibleItemPosition
  • findLastCompletelyVisibleItemPosition

RobbWatershed avatar May 05 '20 12:05 RobbWatershed

findFirstCompletelyVisibleItemPosition is interesting. The only drawback I see is it would never be able to see the last element (e.g. let's say the end of my list is "W, W, X, Y, Z" ; the popup would certainly show "W" at the very bottom). But it would certainly be an improvement.

herbeth1u avatar May 06 '20 16:05 herbeth1u

getFirstItemAdapterPosition is also used for computing the scroll offset, so replacing it with methods like findFirstCompletelyVisibleItemPosition or findLastVisibleItemPosition will break fast scrolling.

Even if we use a separate method for position in getPopupText(), the real problem is that I don't want to expose RecyclerViewHelper as an API, which will be too much to maintain compatibility for. I would recommend implementing one's own ViewHelper instead, which may or may not involve copying the RecyclerViewHelper implementation and make whatever changes according one's specific needs.

zhanghai avatar May 09 '20 22:05 zhanghai

Has anyone been able to come up with a solution?

paolovalerdi avatar Jun 12 '20 00:06 paolovalerdi

This is a hacky way but works, I am strictly open to suggestions and improvements

Declare a variable

var itemPosition = 0

in you onBindView assign position to itemPosition var

itemPosition = position

then in your getPopupText function pass itemPosition as reference

override fun getPopupText(position: Int): String { return list[itemPosition].someText.substring(0, 1) }

Hamza417 avatar Dec 13 '20 22:12 Hamza417

Hi. I am using the following code to improve the accuracy of obtaining the popup text.

public class BetterRecyclerViewHelper  implements FastScroller.ViewHelper {
    ...

    @Nullable
    @Override
    public String getPopupText() {
        PopupTextProvider popupTextProvider = mPopupTextProvider;
        if (popupTextProvider == null) {
            RecyclerView.Adapter<?> adapter = mView.getAdapter();
            if (adapter instanceof PopupTextProvider) {
                popupTextProvider = (PopupTextProvider) adapter;
            }
        }
        if (popupTextProvider == null) {
            return null;
        }
        int position = getPopupTextPosition();
        if (position == RecyclerView.NO_POSITION) {
            return null;
        }
        return popupTextProvider.getPopupText(position);
    }

    private int getPopupTextPosition() {
        int position = getFirstItemAdapterPosition();
        if (position == RecyclerView.NO_POSITION) return RecyclerView.NO_POSITION;

        LinearLayoutManager linearLayoutManager = getVerticalLinearLayoutManager();
        if (linearLayoutManager == null) return position;

        int viewportHeight = mView.getHeight();
        int range = Math.max(getScrollRange() - viewportHeight, 1);
        int offset = Math.min(getScrollOffset(), range);

        int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
        int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();

        if (firstVisibleItemPosition == RecyclerView.NO_POSITION || lastVisibleItemPosition == RecyclerView.NO_POSITION) return position;

        int positionOffset = (int)((lastVisibleItemPosition - firstVisibleItemPosition + 1) * 1.0 * offset / range);
        int correctedPosition = Math.min((position + positionOffset), Objects.requireNonNull(mView.getAdapter()).getItemCount() - 1);

        return correctedPosition;
    }

    ...
}

h6ah4i avatar Sep 19 '21 04:09 h6ah4i

Is there any working solution for this issue yet?

ismailnurudeen avatar Nov 04 '21 08:11 ismailnurudeen