Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stop ScrollView from scrolling when TextView with selectable text becomes focused

I have a ScrollView with multiple TextView children nested inside its layout. I want the text in these children to be selectable (android:textIsSelectable="true"), so the user can use the copy, share and select all actions. But when one of the children receives focus (from touch or long press), it causes the parent ScrollView to scroll to the focused child. I suppose this is a feature, but it introduces three problems for me.

  1. When text is selected with a long press, it causes the text to scroll and as a result the children above the focused child will scroll outside of visible bounds.
  2. The position of the selected text will change and will not appear on the screen where the user made the long press, which is unintuitive.
  3. My app responds to double-tap gestures. The first touch (or tap) of a double-tap will cause the child that received the touch event to receive focus and thus cause a scroll. Apparently, the scroll will cause the gesture to fail; the child is touched, the parent scrolls, the child is touched again, but the double-tap gesture is not detected.

Making the children unfocusable prevents the scroll, but then the text cannot be selected. So how can I have TextView views nested inside a ScrollView with selectable text, but prevent scrolling when one of the views receives focus?

like image 402
Adam Martinu Avatar asked Sep 08 '25 09:09

Adam Martinu


1 Answers

While searching for possible solutions to this problem, I found this article https://programmersought.com/article/8487993014/. It is not directly related to this problem, but one specific part where the article shows some source code from ScrollView caught my attention:

    public void requestChildFocus(View child, View focused) {
        if (focused != null && focused.getRevealOnFocusHint()) {
                         If (!mIsLayoutDirty) {//This boolean value marks whether the layout has been completed. If it is completed, it is false.
                scrollToChild(focused);
            } else {
                // The child may not be laid out yet, we can't compute the scroll yet
                mChildToScrollTo = focused;
            }
        }
 //super method [handling FOCUS_BLOCK_DESCENDANTS case] is called after scrolling on top.
 //So setting the android:descendantFocusability="blocksDescendants" attribute to the ScrollView is invalid.
        super.requestChildFocus(child, focused);
    }

Here you can see that the ScrollView will attempt to scroll to the focused child if focused != null. So to disable this behaviour, create a subclass of ScrollView and override this method like so:

package com.test

// imports here...

public class MyScrollView extends ScrollView {

    // constructors here...

    @Override
    public void requestChildFocus(View child, View focused) {
        super.requestChildFocus(child, null);
    }
}

By ignoring the focused parameter and passing null to the super implementation, the scroll will never occur, but the parent will still request the child to receive focus and therefore text can still be selected.

The only thing left to do is replace the ScrollView parent in the layout file with the custom implementation defined above.

EDIT The solution was tested and worked fine on API 30, but when I tested it on API 25 a NPE is thrown and the app crashes. The Android Docs recommend to use the NestedScrollView view instead for vertical scrolling, but the app still crashes. I looked at the source code for requestChildFocus(View, View) and it is slightly different:

@Override
public void requestChildFocus(View child, View focused) {
    if (!mIsLayoutDirty) {
        scrollToChild(focused);
    } else {
        // The child may not be laid out yet, we can't compute the scroll yet
        mChildToScrollTo = focused;
    }
    super.requestChildFocus(child, focused);
}

private void scrollToChild(View child) {
    child.getDrawingRect(mTempRect);

    /* Offset from child's local coordinates to ScrollView coordinates */
        offsetDescendantRectToMyCoords(child, mTempRect);

    int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);

    if (scrollDelta != 0) {
        scrollBy(0, scrollDelta);
    }
}

The method scrollToChild(View) is made private and so we cannot override the default implementation (because why would anyone ever want to do that, right?), but scrollBy(int, int) is public. So instead of overriding requestChildFocus(View, View) in the MyScrollView class, override scrollBy(int, int) and make it do nothing. This was tested on both API 30 and 25 and worked as intended without crashing. I later tried to revert back to extending ScrollView and it still works. So you just have to override the method, the supertype does not matter.

like image 144
Adam Martinu Avatar answered Sep 10 '25 04:09

Adam Martinu