Near1999
Near1999

Reputation: 1607

Smoothy scroll to Child in NestedScrollView

I have expandable views inside CardView thats parent is NestedScrollView. I'm trying to create smooth scroll to child when expand animation ended. But I found only one solution:

 scrollView.requestChildFocus(someView, someView);

This code works fine, but, when call requestChildFocus it scrolls immediately, and that annoying me a little bit. Is it possible to scroll to child smoothly?

Upvotes: 17

Views: 19098

Answers (9)

Alesh17
Alesh17

Reputation: 386

I implemented auto scrolling of the focused EditText (lying inside the NestedScrollView), if the EditText is hidden (partly hidden) under the keyboard.

Since RecyclerView doing this stuff without any additional work, my testers said that NestedScrollView without autoscrolling - is a bug.

fun EditText.setupAutoscrollInNestedScrollView() {
  setOnFocusChangeListener { _, hasFocus ->
    if (hasFocus.not()) return@setOnFocusChangeListener

    /*
     * The distance from the beginning of the NestedScrollView to our TextField.
     * Since the EditText.top parameter returns a value relative to the direct parent,
     * we must add up all the padding until we get to the NestedScrollView.
     */
    var textFieldYFromTopScrollView = top
    var viewParent = parent

    while (true) {
        if (viewParent is NestedScrollView || viewParent == null) break
        textFieldYFromTopScrollView += (viewParent as? View)?.top.orZero()
        viewParent = viewParent.parent
    }

    val offset = dpToPx(4f)
    val textFieldHeight = height
    val scrollView = viewParent as? NestedScrollView
    val scrollViewVisibleHeight = scrollView?.height.orZero() - getKeyboardHeight()
    val scrollViewYPosition = textFieldYFromTopScrollView - (scrollViewVisibleHeight - textFieldHeight - offset)

    scrollView?.smoothScrollTo(0, scrollViewYPosition)
}}

fun Int?.orZero(): Int = this ?: 0

fun View.getKeyboardHeight(): Int = ViewCompat.getRootWindowInsets(this)?.getInsets(WindowInsetsCompat.Type.ime())?.bottom.orZero()

Upvotes: 0

Vanheden
Vanheden

Reputation: 680

I find the accepted answer to work, but it is specific to their view structure as of now and it could not be used as a static method to use for all similar scrolls with different view structures. I made a variety of it in a utility class that measures until it find it's ScrollView or NestedScrollView parent. I made it so that the scrollToView(View,View) method should work with both ScrollView and NestedScrollView in case I would update which I use later on or whatever. You could of course call the right "child method" directly.

public static void scrollToView(View scrollView, View scrollToView) {
    if (scrollToView == null) {
        Log.d(TAG, "scrollToView() failed due to scrollToView == NULL!");
        return;
    }
    if (scrollView instanceof NestedScrollView) {
        scrollToInNestedView((NestedScrollView) scrollView, scrollToView);
    } else if (scrollView instanceof ScrollView) {
        scrollToInScrollView((ScrollView) scrollView, scrollToView);
    } else {
        Log.d(TAG, "scrollToView() failed due to scrollView not appearing to be any kind of scroll view!");
    }
}

public static void scrollToInNestedView(NestedScrollView scrollView, View scrollToView) {
    if (scrollView == null || scrollToView == null) {
        return;
    }
    scrollView.post(() -> {
        int startY = scrollView.getScrollY();
        int requiredY = scrollToView.getTop();
        View parent = (View) scrollToView.getParent();
        while (parent != null && !(parent instanceof NestedScrollView)) {
            requiredY += parent.getTop();
            parent = (View) parent.getParent();
        }
        if (requiredY != startY) {
            scrollView.smoothScrollTo(0, requiredY);
        }
    });
}

public static void scrollToInScrollView(ScrollView scrollView, View scrollToView) {
    if (scrollView == null || scrollToView == null) {
        return;
    }
    scrollView.post(() -> {
        int startY = scrollView.getScrollY();
        int requiredY = scrollToView.getTop();
        View parent = (View) scrollToView.getParent();
        while (parent != null && !(parent instanceof ScrollView)) {
            requiredY += parent.getTop();
            parent = (View) parent.getParent();
        }
        if (requiredY != startY) {
            scrollView.smoothScrollTo(0, requiredY);
        }
    });
}

Upvotes: 0

mathew11
mathew11

Reputation: 3580

The main issue is to calculate the correct x/y position relative to the nestedscrollview. However, most of the answers here try to propose a solution by navigating in the hierarchy and hardcoding the hierarchy-level of the desired view. Imho, this is very error prone, if you change your viewgroup hierarchy.

Therefore, I would suggest to use a much more cleaner approach, which computes the relative position based on a Rect. Then, you can use the nestedscrollview's smoothScrollTo(..) methods to scroll to the desired position.

Upvotes: 1

Jerry Sha
Jerry Sha

Reputation: 3991

I had child views at different levels to the scrollview so made this function based off the accepted answer to calculate the distance and scroll

  private int findDistanceToScroll(View view){
    int distance = view.getTop();
    ViewParent viewParent = view.getParent();
    //traverses 10 times
    for(int i = 0; i < 10; i++) {
        if (((View) viewParent).getId() == R.id.journal_scrollview) {
            return distance;
        }
        distance += ((View) viewParent).getTop();
        viewParent = viewParent.getParent();
    }
    Timber.w("view not found");
    return 0;
}

Then scroll using

journal_scrollview.smoothScrollTo(0, distance);

Upvotes: 4

CHAN
CHAN

Reputation: 1566

more to the answer from @jerry-sha

fun NestedScrollView.smoothScrollTo(view: View) {
  var distance = view.top
  var viewParent = view.parent
  //traverses 10 times
  for (i in 0..9) {
    if ((viewParent as View) === this) break
    distance += (viewParent as View).top
    viewParent = viewParent.getParent()
  }
  smoothScrollTo(0, distance)
}

Upvotes: 6

tiny sunlight
tiny sunlight

Reputation: 6251

Try to read the source code.

svMain.setSmoothScrollingEnabled(true);
Rect rect = new Rect();
rect.top = 0;
rect.left = 0;
rect.right = tv4.getWidth();
rect.bottom =tv4.getHeight();
svMain.requestChildRectangleOnScreen(tv4,rect,false);

rect is the place u want the view to be shown on screen.

Upvotes: 2

Jitheesh S Khan
Jitheesh S Khan

Reputation: 31

This should get the work done, where childView is the view you want to scroll to

public static void scrollToView(final NestedScrollView nestedScrollView, final View viewToScrollTo) {
        final int[] xYPos = new int[2];
        viewToScrollTo.getLocationOnScreen(xYPos);
        final int[] scrollxYPos = new int[2];
        nestedScrollView.getLocationOnScreen(scrollxYPos);
        int yPosition = xYPos[1];
        if (yPosition < 0) {
            yPosition = 0;
        }
        nestedScrollView.scrollTo(0, scrollxYPos[1] - yPosition);
    }

Upvotes: 0

Near1999
Near1999

Reputation: 1607

The childView, to which I wanted to scroll, has CardView parrent, so childView.getTop() returns the value relative to the CardView not to the ScrollView. So, to get top relative to ScrollView I should get childView.getParent().getParent() then cast it to View and call getTop().

Scroll position calculates like

int scrollTo = ((View) childView.getParent().getParent()).getTop() + childView.getTop();
nestedScrollView.smoothScrollTo(0, scrollTo);

Upvotes: 32

Bartek Lipinski
Bartek Lipinski

Reputation: 31438

You can use my library ViewPropertyObjectAnimator for that.

Assuming mNestedScrollView is your NestedScrollView and mChildView is the child View you want to scroll to, you can do the following:

ViewPropertyObjectAnimator.animate(mNestedScrollView).scrollY(mChildView.getTop()).start();

Just make sure mChildView.getTop() is not 0 at the moment of calling .animate(...).

Edit:

As I said: make sure your View's top is non-zero when CALL .animate(...). In other words: call .animate(...) only when your child View already has dimensions. How can you determine that? For example like this:

mChildView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

    @Override
    public void onGlobalLayout() {
       int width = mChildView.getWidth();
       int height = mChildView.getHeight();
        if (width > 0 && height > 0) {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
              mChildView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            } else {
              mChildView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            }

            ViewPropertyObjectAnimator.animate(mNestedScrollView)
                    .scrollY(mChildView.getTop())
                    .start();
        }
    }
});

Upvotes: 1

Related Questions