Approach No.1: Using two nested RecyclerViews
Github sample
Pros:
- Views are recycled (i.e. Good performance)
- Semi-Seamless scrolling (after update 3 & 4)
Cons:
- The programmatically propagated scroll during the transition from the inner to the outer scroll when the inner far end  item is reached is not that smooth/natural like the gesture.
- Complex code.
Well, I won't address the performance issues of vertically nested RecyclerViews; But notice that:
- The inner RecyclerViewprobably loses the ability of recycling views; because the shown rows of the outer recyclerView should load their items entirely. (Thankfully it's not a right assumption as per the below UPDATE 1)
- I declared a single adapter instance in the ViewHoldernot in theonBindViewHolderto have a better performance by not creating a new adapter instance for the innerRecyclerVieweach time views are recycled.
The demo app represents the months of the year as the outer RecyclerView, and the day numbers of each month as inner RecyclerView.
The outer RecyclerView registers OnScrollListener that each time it's scrolled, we do this check on the inner RV:
- If outer scrolling up: check if the inner first item is shown.
- If outer scrolling down: check if the inner last item is shown.
    outerRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
            if (dy > 0) //scrolled to BOTTOM
                outerAdapter.isOuterScrollingDown(true, dy);
            else if (dy < 0) //scrolled to TOP
                outerAdapter.isOuterScrollingDown(false, dy);
        }
    });
In the outer adapter:
    public void isOuterScrollingDown(boolean scrollDown, int value) {
        if (scrollDown) {
            boolean isLastItemShown = currentLastItem == mMonths.get(currentPosition).dayCount;
            if (!isLastItemShown) onScrollListener.onScroll(-value);
            enableOuterScroll(isLastItemShown);
        } else {
            boolean isFirstItemShown = currentFirstItem == 1;
            if (!isFirstItemShown) onScrollListener.onScroll(-value);
            enableOuterScroll(isFirstItemShown);
        }
        if (currentRV != null)
            currentRV.smoothScrollBy(0, 10 * value);
    }
If the relevant item is not shown, then we decide to disable the outer RV scrolling. This is handled by a listener with a callback that accepts a boolean passed to a customized LinearLayoutManager class to the outer RV.
Likewise in order to re-enable scrolling of the outer RV: the inner RecyclerView registers OnScrollListener to check if the inner first/last item is shown.
innerRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
        if (!recyclerView.canScrollVertically(1) // Is it not possible to scroll more to bottom (i.e. Last item shown)
                && newState == RecyclerView.SCROLL_STATE_IDLE) {
            enableOuterScroll(true);
        } else if (!recyclerView.canScrollVertically(-1) // Is it possible to scroll more to top (i.e. First item shown)
                && newState == RecyclerView.SCROLL_STATE_IDLE) {
            enableOuterScroll(true);
        }
    }
});
Still, there are glitches because disabling/enabling scrolling; we can't pass the scroll order to the other RV, until the next scroll. This is manipulated slightly by reversing the initial outer RV scroll value; and using an arbitrary scroll value to the inner with currentRV.smoothScrollBy(0, 10 * initialScroll). I wish if someone can suggest any other alternative to this.
UPDATE 1
- The inner RecyclerViewprobably lose the ability of recycling views; because the shown rows of the outer recyclerView should load their items entirely.
Thankfully it's not the right assumption and the views are recycled by tracking the recycled list of items in the inner adapter using a List that hold the currently loaded items:
By assuming some month has a 1000 days "Feb as it's always oppressed :)", and scrolling up/down to notice the loaded list and make sure that onViewRecycled() get called.
public class InnerRecyclerAdapter extends RecyclerView.Adapter<InnerRecyclerAdapter.InnerViewHolder> {
    private final ArrayList<Integer> currentLoadedPositions = new ArrayList<>();
    @Override
    public void onBindViewHolder(@NonNull InnerViewHolder holder, int position) {
        holder.tvDay.setText(String.valueOf(position + 1));
        currentLoadedPositions.add(position);
        Log.d(LOG_TAG, "onViewRecycled: " + days + " " + currentLoadedPositions);
    }
    @Override
    public void onViewRecycled(@NonNull InnerViewHolder holder) {
        super.onViewRecycled(holder);
        currentLoadedPositions.remove(Integer.valueOf(holder.getAdapterPosition()));
        Log.d(LOG_TAG, "onViewRecycled: " + days + " " + currentLoadedPositions);
    }
    
    // Rest of code is trimmed
}
Logs:
onViewRecycled: 1000 [0]
onViewRecycled: 1000 [0, 1]
onViewRecycled: 1000 [0, 1, 2]
onViewRecycled: 1000 [0, 1, 2, 3]
onViewRecycled: 1000 [0, 1, 2, 3, 4]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
onViewRecycled: 1000 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
onViewRecycled: 1000 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
onViewRecycled: 1000 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
onViewRecycled: 1000 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
onViewRecycled: 1000 [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
onViewRecycled: 1000 [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
onViewRecycled: 1000 [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
onViewRecycled: 1000 [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
onViewRecycled: 1000 [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
onViewRecycled: 1000 [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
onViewRecycled: 1000 [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
onViewRecycled: 1000 [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
onViewRecycled: 1000 [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
onViewRecycled: 1000 [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
onViewRecycled: 1000 [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
onViewRecycled: 1000 [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
onViewRecycled: 1000 [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
onViewRecycled: 1000 [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
onViewRecycled: 1000 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
onViewRecycled: 1000 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
onViewRecycled: 1000 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
onViewRecycled: 1000 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
onViewRecycled: 1000 [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
onViewRecycled: 1000 [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
onViewRecycled: 1000 [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
onViewRecycled: 1000 [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
onViewRecycled: 1000 [14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
onViewRecycled: 1000 [14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
UPDATE 2
Still there are glitches because disabling/enabling scrolling; we can't pass the scroll order to the other RV, until the next scroll. This is manipulated slightly by reversing the initial outer RV scroll value; and using an arbitrary scroll value to the inner with currentRV.smoothScrollBy(0, 10 * initialScroll). I wish if someone can suggest any other alternative to this.
- Using a greater arbitrary value (like 30) makes the grammatical scroll looks smoother >> - currentRV.smoothScrollBy(0, 30 * initialScroll)
 
- And scrolling the outer scroll without reversing the scroll, makes it also looks more natural in the same direction of the scroll: 
if (scrollDown) {
    boolean isLastItemShown = currentLastItem == mMonths.get(currentPosition).dayCount;
    if (!isLastItemShown) onScrollListener.onScroll(value);
    enableOuterScroll(isLastItemShown);
} else {
    boolean isFirstItemShown = currentFirstItem == 1;
    if (!isFirstItemShown) onScrollListener.onScroll(value);
    enableOuterScroll(isFirstItemShown);
}
UPDATE 3
Issue: glitches during the transition from the outer to inner RecyclerView because the onScroll() of the outer gets called before deciding whether we can scroll the inner or not.
By using OnTouchListener to the outer RecyclerView and overriding onTouch() and return true to consume the event (so that onScrolled() won't get called) until we decide that the inner can take the scroll over.
private float oldY = -1f;
outerRecyclerView.setOnTouchListener((v, event) -> {
    Log.d(LOG_TAG, "onTouch: ");
    switch (event.getAction()) {
        case MotionEvent.ACTION_UP:
            oldY = -1;
            break;
        case MotionEvent.ACTION_MOVE:
            float newY = event.getRawY();
            Log.d(LOG_TAG, "onTouch: MOVE " + (oldY - newY));
            if (oldY == -1f) {
                oldY = newY;
                return true; // avoid further listeners (i.e. addOnScrollListener)
            } else if (oldY < newY) { // increases means scroll UP
                outerAdapter.isOuterScrollingDown(false, (int) (oldY - newY));
                oldY = newY;
            } else if (oldY > newY) { // decreases means scroll DOWN
                outerAdapter.isOuterScrollingDown(true, (int) (oldY - newY));
                oldY = newY;
            }
            break;
    }
    return false;
});
UPDATE 4
- Enabling the scroll transition from the inner to outer RecyclerViewwhenever the inner RV scrolled to its top or bottom edge so that it continues scrolling to the outer RV by a proportional speed.
The scroll speed is inspired by this post. By applying the first/last item checks in the touched inner RV's OnTouchListener & OnScrollListener , and resetting stuff in a brand new touch event i.e. in MotionEvent.ACTION_DOWN
- Disable over-scroll mode in both inner & outer RecyclerViews
Preview:

Approach No.2: Wrapping outer RecyclerView in NestedScrollView
Github sample
The main issue of the nested scrolling of a RecyclerView is that it doesn't implement NestedScrollingParent3 interface which is implemented by NestedScrollView; So RecyclerView can't handle nested scrolling of child views. So, trying to compensate that with a NestedScrollView by wrapping the outer RecyclerView within a NestedScrollView, and disable the scrolling of the outer RecyclerView
Pros:
- Simple code (You don't have to manipulate inner/outer scrolling at all)
- No glitch
- Seamless scrolling
Cons:
- Low performance as views of the outer RecyclerVieware not recycled and so that they have to be all loaded before showing up on the screen.
Reason: Due to the nature of the NestedScrollView >> Check (1), (2) questions that discussed the recycling issues:
Approach No.3: Using ViewPager2 as the outer RecyclerView
Github sample
Using a ViewPager2 that functions internally using a RecyclerView solves the problem of recycling views, but only one page (one outer row) can present at a time.
Pros:
- No glitches & Seamless scrolling upon using NestedScrollableHost
- Views are recycled as there is an internal RecyclerViewinViewPager2
Cons:
- Showing only a single item per page
So we probably tackle this by researching either:
- How to show Multiple views per page
- How to wrap_content a page