Search code examples
javaandroidscrollandroid-custom-viewhorizontalscrollview

HorizontalScrollView scrollTo doesn't scroll above original width of the view


I have a custom view containing an HorizontalScrollView. The width of the scroll view is match_parent, and the width of its children is initially programmatically set based on the value of an attribute of the custom view. At some point, the width of the children of the scroll view are updated (increased) programmatically. The problem is that, after the update, the scrollTo method is still unable to scroll above the original width value (the same for scrollBy).

The enclosing view (the custom one) have a left and right padding equal to half the screen, if this is relevant.

Example:

  • Initial HorizontalScrollView's children width: 1000;
  • HorizontalScrollView's parent width: 1080;
  • HorizontalScrollView's left/right padding = 540;
  • New HorizontalScrollView's children width: 2000;
  • scrollTo(1100, 0) = scrollTo(1000, 0); <---- here is the problem

Relevant code:

Custom view initialization:

private void init(AttributeSet attrs, int defStyle) {
    inflate(getContext(), R.layout.timeline_view, this);

    // Load attributes
    final TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.TimelineView, defStyle, 0);

    m_timelineInitialLength = a.getDimension(R.styleable.TimelineView_timelineInitialLength, DEFAULT_LENGTH);

    a.recycle();

    DisplayMetrics displayMetrics = new DisplayMetrics();
    ((Activity) getContext()).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
    LinearLayout timelineWidget = findViewById(R.id.timeline_widget);
    int horizontalPadding = displayMetrics.widthPixels / 2;
    timelineWidget.setPadding(horizontalPadding, 0, horizontalPadding, 0);

    m_horizontalScrollView = findViewById(R.id.scroll_view);
    m_horizontalScrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
        @Override
        public void onScrollChanged() {
            m_currentScrollX = m_horizontalScrollView.getScrollX();
        }
    });
    m_currentScrollX = m_horizontalScrollView.getScrollX();

    update(m_timelineInitialLength, m_timelineInitialStepWidth, m_currentScrollX);
}

Update and scrollTo (this method is called also at the end of the initialization. m_rulerView and m_timelineLayout are the children of the HorizontalScrollView):

private void update(float timelineLength, float timelineStepWidth, int currentScrollX) {
    m_rulerView = findViewById(R.id.ruler);
    m_rulerView.setIndicator(null);
    m_rulerView.getLayoutParams().width = (int) timelineLength;
    m_rulerView.requestLayout();

    m_timelineLayout = findViewById(R.id.timeline);
    m_timelineLayout.getLayoutParams().width = (int) timelineLength;
    m_timelineLayout.requestLayout();

    m_horizontalScrollView.scrollTo(currentScrollX, 0);
}

Solution

  • The problem seems to be that the scrolling happens before the children have been redrawn, before calling requestLayout only schedule the update of the view to be executed at some point in the future (the same for invalidate()).

    I solved using a workaround, by executing scrollTo after some time:

    new Handler().postDelayed(() -> ((Activity) getContext()).runOnUiThread(() -> m_horizontalScrollView.scrollTo(currentScrollX, 0)), 200);
    

    I'm still open to better solutions.