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.