68

I'm using a Scrollview for an infinite "Time Picker Carousel" and found out, that it is not the best approach (last question)

Now, I found the Recycler View but I am unable to get the current scroll offset in X direction of the recyclerView? (Let's say each item is 100px width, and the second item is only visible to 50%, so the scroll-offset is 150px)

  • all items have the same width, but therer is some gap before the first, after the last item
  • recyclerView.getScrollX() returns 0 (docs say: initial scroll value)
  • the LayoutManager has findFirstVisibleItemPosition, but I cannot calculate the X offset with that

UPDATE

I just found a way to keep tracking the X-Position, while updating the value with the onScrolled callback, but I would prefer getting the actual value instead of tracking it all the time!

private int overallXScroll = 0;
//...
mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);

            overallXScroll = overallXScroll + dx;

            Log.i("check","overallXScroll->" + overallXScroll);

        }
    });
Community
  • 1
  • 1
longi
  • 11,104
  • 10
  • 55
  • 89
  • you mean not "x" but rather item position? – pskink Dec 16 '14 at 15:22
  • i just clarified (?) my question. I'm looking for the current offset in X direction of all items. – longi Dec 16 '14 at 15:31
  • 2
    Did you try that https://developer.android.com/reference/android/support/v7/widget/RecyclerView.OnScrollListener.html ? – pskink Dec 16 '14 at 15:42
  • haha, are you reading my mind? I just updated my answer... – longi Dec 16 '14 at 15:46
  • 2
    RecyclerView does not support getScrollX/Y because it cannot guarantee correct result. For example, user may call scrollToPosition(100). To calculate real scrollY, it will have to layout all of the items between 0 and 100. Not feasible. `setOnScrollListener` will work as long as your adapter contents do not change. Alternatively, you can use `LLM#findLastVisibleItemPosition` and calculate total offset yourself using view.getLeft() – yigit Dec 17 '14 at 22:33
  • @yigit: you are right. please post your comment as answer that i can accept it. i gonna add my final implementation soon – longi Dec 18 '14 at 11:22
  • 3
    Saving total scroll by adding scrolled distance in listener is the most simple and working solution, I've found so far. But I know, that RecyclerView saves it's scroll position when resuming activity after it was killed by system. I mean, that our custom variable with total scroll may be be out of sync after resuming from background. – Deepscorn Mar 28 '15 at 09:09
  • Hey @longilong can you please help me with this https://stackoverflow.com/questions/49952965/recyclerview-horizontal-scrolling-to-left?noredirect=1#comment87836903_49952965 –  May 18 '18 at 12:23

7 Answers7

71

RecyclerView already have method to get horizontal and vertical scroll offset

mRecyclerView.computeHorizontalScrollOffset()
mRecyclerView.computeVerticalScrollOffset()

This will work for RecyclerViews containing cells of the same height (for vertical scroll offset) and the same width (for horizontal scroll offset)

andreimarinescu
  • 3,541
  • 2
  • 25
  • 32
Umar Qureshi
  • 5,985
  • 2
  • 30
  • 40
  • 13
    This does not work for me, sometimes jumps by almost 400px while just scrolling along the list, something seems to be missing – A. Steenbergen Jan 03 '16 at 09:51
  • 29
    it's the fact that your cells don't have the same width (or height, depending on the scroll direction). computeXScrollOffset uses the average cell dimensions, so if your cells vary in size this method is useless. I've been trying for hours to find a decent solution for computing this :) – andreimarinescu Feb 26 '16 at 17:39
  • 1
    @andreimarinescu any solutions yet? – urSus Sep 02 '16 at 15:39
  • @urSus I've ended up computing the offset myself, by adding a scroll listener and computing the difference between the previous offset and the scrolled distance. It works pretty well for my use-case, however if the recycler view cells change their height dynamically this will not work, since the distance scrolled down won't be equal to the distance you would need to scroll upwards. – andreimarinescu Sep 07 '16 at 08:00
66

enter image description here

Solution 1: setOnScrollListener

Save your X-Position as class variable and update each change within the onScrollListener. Ensure you don't reset overallXScroll (f.e. onScreenRotationChange)

 private int overallXScroll = 0;
 //...
 mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);

        overallXScroll = overallXScroll + dx;
        Log.i("check","overall X  = " + overallXScroll);

    }
 });

Solution 2: Calculate current position.

In my case, I have a horizontal List which is filled with time values (0:00, 0:30, 1:00, 1:30 ... 23:00, 23:30). I'm calculating the time from the time-item, which is in the middle of the screen (calculation point). That's why I need the exact X-Scroll Position of my RecycleView

  • Each time item has the same width of 60dp (fyi: 60dp = 30min, 2dp = 1min)
  • First item (Header item) has an extra padding, to set 0min to the center

    private int mScreenWidth = 0;
    private int mHeaderItemWidth = 0;
    private int mCellWidth = 0;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
    
      //init recycle views
      //...
      LinearLayoutManager mLLM = (LinearLayoutManager) getLayoutManager();
      DisplayMetrics displaymetrics = new DisplayMetrics();
      this.getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
      this.mScreenWidth = displaymetrics.widthPixels;
    
      //calculate value on current device
      mCellWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 60, getResources()
            .getDisplayMetrics());
    
      //get offset of list to the right (gap to the left of the screen from the left side of first item)
      final int mOffset = (this.mScreenWidth / 2) - (mCellWidth / 2);
    
      //HeaderItem width (blue rectangle in graphic)
      mHeaderItemWidth = mOffset + mCellWidth;
    
      mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
    
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
    
            //get first visible item
            View firstVisibleItem = mLLM.findViewByPosition(mLLM.findFirstVisibleItemPosition());
    
            int leftScrollXCalculated = 0;
            if (firstItemPosition == 0){
                   //if first item, get width of headerview (getLeft() < 0, that's why I Use Math.abs())
                leftScrollXCalculated = Math.abs(firstVisibleItem.getLeft());
            }
            else{
    
                   //X-Position = Gap to the right + Number of cells * width - cell offset of current first visible item
                   //(mHeaderItemWidth includes already width of one cell, that's why I have to subtract it again)
                leftScrollXCalculated = (mHeaderItemWidth - mCellWidth) + firstItemPosition  * mCellWidth + firstVisibleItem.getLeft();
            }
    
            Log.i("asdf","calculated X to left = " + leftScrollXCalculated);
    
        }
    });
    

    }

longi
  • 11,104
  • 10
  • 55
  • 89
  • 4
    what is `firstItemPosition ` – ZeroOne Nov 24 '16 at 01:36
  • Solution 1 could make a certain amount of error when scroll very fast. – lovefish Jan 13 '17 at 09:41
  • Solution 2 totally saved my behind. I was using Solution 1 and my scrollY kept getting off by a few pixels when doing fast up and down scrolls but now I just check if `mLLM.findFirstCompletelyVisibleItemPosition() == 0` and I know if I'm at the top and I can reset my scrollY to be in sync again. FYI: if anyone is wondering `mLLM` is the `LinearLayoutManager` and you can get one by doing: `LinearLayoutManager mLLM = (LinearLayoutManager) getLayoutManager();` assuming you are working with a `LayoutManager` that extends form `LinearLayoutManager` of course Thx a lot @longilong ! – Francois Dermu Jul 25 '17 at 23:29
  • @ZeroOne, firstImtePosition = mLLM.findFirstVisibleItemPosition(); From Linear Layout manager, get the first visible item position. – Mallikarjungouda Annigeri Apr 16 '18 at 06:07
  • excellent answer except firstVisibleItem.getLeft() should always leading with a negative sign for firstVisibleItem.getLeft() is always negative – Wei JingNing Apr 05 '22 at 09:09
43

Thanks to @umar-qureshi for the right lead! It appears that you can determine the scroll percentage with Offset, Extent, and Range such that

percentage = 100 * offset / (range - extent)

For example (to be put in an OnScrollListener):

int offset = recyclerView.computeVerticalScrollOffset();
int extent = recyclerView.computeVerticalScrollExtent();
int range = recyclerView.computeVerticalScrollRange();

int percentage = (int)(100.0 * offset / (float)(range - extent));

Log.i("RecyclerView, "scroll percentage: "+ percentage + "%");
jbohren
  • 576
  • 4
  • 5
8
public class CustomRecyclerView extends RecyclerView {

    public CustomRecyclerView(Context context) {
        super(context);
    }

    public CustomRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public int getHorizontalOffset() {
        return super.computeHorizontalScrollOffset();
    }
}

Than where you need to get the offset:

recyclerView.getHorizontalOffset()
  • @MichaelDePhillips Your looking at the source of the inner class LayoutManager which does return zero but we don't care about that method ;) The actual method we care about returns the following: return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollOffset(mState) : 0; – SemaphoreMetaphor Apr 16 '15 at 05:18
  • @MichaelDePhillips And the reason we don't care about that method is because one of the first things we do while initializing a recyclerview is set a layoutmanager. In most cases that is going to be a LinearLayoutManager which extends said LayoutManager's method to provide an accurate offset for bother vertical and horizontal. – SemaphoreMetaphor Apr 16 '15 at 05:27
  • On furthur inspection, you are correct. Thanks for clearing it up. I will edit my comment. – Michael DePhillips Apr 27 '15 at 20:35
  • 3
    This returns an int in an arbitrary unit, and will not reliably give you an accurate scroll amount; that's not what it is for. For instance, it drops from 1850px to 300px as soon as my first visible position moves from 0 to 1. – Tom Apr 28 '15 at 05:36
  • `computeHorizontalScrollOffset()` is used to calculate the scrollbar size. I got an issue that this value suddenly drops when a center of a cell smoothly scrolls out of screen. On the other hand, @longilong 's 1st solution can get reliable offset. – Xiang Jun 18 '15 at 06:59
  • @Xiang longilong's solution does work, if the onScroll listener has been active for every scroll, which is not the case after configuration changes, so this solution works for probably a majority of cases, but is not working in the general case – A. Steenbergen Jan 03 '16 at 09:53
7

If you need to know the current page and its offset in comparison to the width of RecyclerView you may wanna try something like this:

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);

        final int scrollOffset = recyclerView.computeHorizontalScrollOffset();
        final int width = recyclerView.getWidth();
        final int currentPage = scrollOffset / width;
        final float pageOffset = (float) (scrollOffset % width) / width;

        // the following lines just the example of how you can use it
        if (currentPage == 0) {
            leftIndicator.setAlpha(pageOffset);
        }

        if (adapter.getItemCount() <= 1) {
            rightIndicator.setAlpha(pageOffset);
        } else if (currentPage == adapter.getItemCount() - 2) {
            rightIndicator.setAlpha(1f - pageOffset);
        }
    }
});

If currentPage has index 0 then leftIndicator (arrow) will be transparent, and so will be and rightIndicator if there's only one page (in the example, the adapter always has at least one item to show so there's no check for the empty value).

This way you will basically have almost the same functionality as if you have been using the callback pageScrolled from ViewPager.OnPageChangeListener.

Sébastien
  • 893
  • 2
  • 9
  • 13
2

by override RecyclerView onScrolled(int dx,int dy) , you can get the scroll offset. but when you add or remove the item, the value may be wrong.

public class TempRecyclerView extends RecyclerView {
   private int offsetX = 0;
   private int offsetY = 0;
   public TempRecyclerView(Context context) {
       super(context);
   }

   public TempRecyclerView(Context context, @Nullable AttributeSet attrs) {
      super(context, attrs);
   }

   public TempRecyclerView(Context context, @Nullable AttributeSet attrs,int defStyle){
      super(context, attrs, defStyle);
   }
/**
 * Called when the scroll position of this RecyclerView changes. Subclasses 
   should usethis method to respond to scrolling within the adapter's data 
   set instead of an explicit listener.
 * <p>This method will always be invoked before listeners. If a subclass 
   needs to perform any additional upkeep or bookkeeping after scrolling but 
   before listeners run, this is a good place to do so.</p>
 */
   @Override
   public void onScrolled(int dx, int dy) {
  //  super.onScrolled(dx, dy);
      offsetX+=dx;
      offsetY+=dy;
     }
 }
chen
  • 172
  • 1
  • 7
0
recyclerViewScrollListener = new RecyclerView.OnScrollListener() {

    @Override
    public void onScrollStateChanged(@NonNull @NotNull RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        
    }

    @Override
    public void onScrolled(@NonNull @NotNull RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);

       

        int scrollBetween += dy;

        int scrollBetweenDP = pxToDp(scrollBetween, getContext());

        if (scrollBetweenDP > 50 ) {
            // scroll down distance 50dp

        } else if (scrollBetweenDP < -50) {
           //scroll up distance 50dp
        }

       
    }
};
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 14 '21 at 17:06