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.
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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With