Android: ScrollView scrolls to the top inadvertently - android

I've got such a layout:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ScrollView
android:id="#+id/scroll_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:fillViewport="true" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
...
other views
...
<TextView
android:id="#+id/text_view_that_can_change"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>
</ScrollView>
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
layout="#layout/btns_ok_cancel" />
</RelativeLayout>
The problem is that when I add text to initially empty text_view_that_can_change - my ScrollView scrolls to the top.
The same problem happens when I set visibility to some of views inside ScrollView/RelativeLayout.
I can scroll it back using scrollView.scrollTo(0, yScroll) but it gives a visible and, I must say, ugly jerk to the whole content.
It there any way to prevent this behavior?

Have spent some hours and here is what I've discovered.
Scroll to the top happens because inner RelativeLayout is being re-laid out as a result of the content change.
You should add a couple of lines to control this situation:
1) declare class member that will hold scroll position:
int mSavedYScroll;
2) Add GlobalLayoutListener to the main view (here it's a fragment's main view, but that doesn't matter):
public View onCreateView(...) {
mView = inflater.inflate(...);
...
mView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (mSavedYScroll != 0) {
mScrollContainer.scrollTo(0, mSavedYScroll);
mSavedYScroll = 0;
}
}
});
}
3) When you do something (anywhere in you code) that may change content of ScrollView - just save current scroll position:
mSavedYScroll = mScrollContainer.getScrollY();
//DO SOMETHING THAT MAY CHANGE CONTENT OF mScrollContainer:
...
That's it!

Related

Unable to scroll to item in RecyclerView that is inside NestedScrollView

I'm trying to programmatically scroll to a particular item within my RecyclerView which is nested within a NestedScrollView.
The Problem
The NestedScrollView scrolls to the complete bottom rather than the desired item.
Note: It works correctly when desired item is the 2nd item, probably since that item is visible in the screen.
What I've tried
I've searched through a dozen solutions from StackOverFlow and came up with the function below.
I've tried:
binding.variantsRecyclerView.getChildAt()
binding.variantsRecyclerView.findViewWithTag()
binding.variantsRecyclerView.findViewHolderForAdapterPosition()
All these do return the correct item, (I know since the edit text within that item is focused as coded) however, the NestedScrollView does not scroll correctly to that item. It is almost always scrolling to the bottom. Sometimes however it scrolls to somewhere in between the required item instead of it's start. The only time this works is when the item is either the 1st or 2nd item. (As stated before)
private fun scrollToPosition(position: Int) {
if (position in 0 until variantsAdapter.itemCount) {
val view = binding.variantsRecyclerView.findViewWithTag<ConstraintLayout>("pos_$position")
if (view != null) {
val height = binding.nestedScrollView.height
val target = view.y.toInt()
binding.nestedScrollView.postDelayed({
binding.nestedScrollView.smoothScrollTo(0, target)
view.requestFocus()
}, 200)
}
}
}
My XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#eff1f4">
<LinearLayout>...</LinearLayout>
<androidx.core.widget.NestedScrollView
android:id="#+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constrainedHeight="true"
app:layout_constraintVertical_bias="0"
app:layout_constraintBottom_toTopOf="#+id/btnNextLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/toolbarLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="#dimen/ui_10_dp"
android:layout_marginEnd="#dimen/ui_10_dp"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/variantsRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingTop="12dp"
android:paddingBottom="12dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="2"
tools:listitem="#layout/sell_variant_row_item" />
<androidx.constraintlayout.widget.ConstraintLayout>
...
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<LinearLayout>...</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
My Understanding
After debugging, I found out that the NestedScrollView height is lesser than the y co-ordinate of the desired item. Hence it scrolls to the bottom of the view instead of the desired item. My understanding could be completely wrong and if so, please correct me.
I resolved this with a really simple fix.
private fun scrollToPosition(position: Int) {
if (position in 0 until variantsAdapter.itemCount) {
val view = binding.variantsRecyclerView.getChildAt(position)
if (view != null) {
val target = binding.variantsRecyclerView.top + view.top
binding.nestedScrollView.scrollY = target
}
}
}
All I wanted was to get the desired item within the RecyclerView to the top of the screen.

How to trigger a recyclerview scroll listener when it is inside a scroll view?

How to trigger a recyclerview scroll listener when it is inside a scroll view?
Here the scroll view listener is alone triggering
1. Set nested scrolling enabled false of recycler view.
recyclerView.setNestedScrollingEnabled(false);
2. Add scroll listner to nested scrollview.
mScrollView.getViewTreeObserver().addOnScrollChangedListener(new
ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged()
{
View view = (View)mScrollView.getChildAt(mScrollView.getChildCount() - 1);
int diff = (view.getBottom() - (mScrollView.getHeight() + mScrollView
.getScrollY()));
if (diff == 0) {
// your pagination code
}
}
});
Try with NestedScrollView instead of ScrollView.
NestedScrollView is just like ScrollView, but it supports acting as both a nested scrolling parent and child on both new and old versions of Android. Nested scrolling is enabled by default.
Note: You have to set setNestedScrollingEnable(false) to your recylerview.
in xml add android:nestedScrollingEnabled="false" in recyclerview
OR
programmatically yourRecylerview.setNestedScrollingEnabled(false);
See this article.
You can try this one, but still I assuming you may have the same code like mine as you didnot post your needed code into your question.
Try this:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v4.widget.NestedScrollView
android:id="#+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</LinearLayout>
Use NestedScrollView with <android.support.v7.widget.RecyclerView as I have mention above.
Its ScrollListener will not work as all the rows gets layed when recyclerview is inside scrollview whole purpose of recyclerview gets destroyed when you put it inside scroll view.
For Example as the recyclerview will stretch to full fill its parent ,when you will put it inside scroll view , in case of scroll view it will stretch to its fullest. if there are 1000 items in recyclerview all it will draw 1000 different rows which is also a performance issue.
You should remove it from a scroll view to make its scroll listener working

How to animate height change of views inside a LinearLayout?

I am working on an Android 4+ app which uses a quite simply layout: Multiple views are stacked using a LinearLayout within a ScrollView
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="HardcodedText,UseCompoundDrawables,UselessParent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
<!-- Top Container -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
<Button... />
</LinearLayout>
<!-- Hidden Container -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
... Some Content ...
</LinearLayout>
<!-- Bottom Container -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
... Some Content ...
</LinearLayout>
</LinearLayout>
</ScrollView>
The HiddenContainer should not be visible when the layout it created. Thus in the beginning the BottomContainer is directly beneath the TopContainer. A click on the Button within the TopContainer toggles the visibility of the HiddenContainer.
Doing this with hiddenContainer.setVisibility(hiddenContainer.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE) works find and without any problem. However it does not look good when the view suddenly appears or disappears. Instead I would like to animate the change.
I was surprised that I was not able to find an easy solution for this:
Using android:animateLayoutChanges="true" does work, however I am not able to control the animation.
While using a ValueAnimator to change hiddenContainer.setScaleY(...) gives me control over the animation setScaleY(0) makes the container invisible without reducing the space it occupies within the layout.
Using the ValueAnimator to change hiddenContainer.setHeight(...) might work, however I don't want to use a fixed height value when showing the container (e.g. hiddenContainer.setHeight(300)) but the height which is determined by the containers content.
So, how to solve this?
For animate your changes of layout (alpha, visibility, height, etc) you can use TransitionManager. For example: I have three static methods and use them when I want to animate layout changes:
public static final int DURATION = 200;
public static void beginAuto(ViewGroup viewGroup) {
AutoTransition transition = new AutoTransition();
transition.setDuration(DURATION);
transition.setOrdering(TransitionSet.ORDERING_TOGETHER);
TransitionManager.beginDelayedTransition(viewGroup, transition);
}
public static void beginFade(ViewGroup viewGroup) {
Fade transition = new Fade();
transition.setDuration(DURATION);
TransitionManager.beginDelayedTransition(viewGroup, transition);
}
public static void beginChangeBounds(ViewGroup viewGroup) {
ChangeBounds transition = new ChangeBounds();
transition.setDuration(DURATION);
TransitionManager.beginDelayedTransition(viewGroup, transition);
}
And when you want to animate layout changes you can just call one of this methods before layout changings:
beginAuto(hiddenContainerParentLayout);
hiddenContainer.setVisibility(hiddenContainer.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE)

Request focus of view and place it on top of the screen within a ScrollView

See bottom of this entry for an answer to this "problem".
In my app I inflate some xml views and add them to a LinearLayout, list, within a ScrollView. The XML looks like this:
<ScrollView
android:layout_width="0dp"
android:layout_weight="19"
android:layout_height="wrap_content"
android:fillViewport="true">
<LinearLayout
android:id="#+id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
I then try to give focus to one of the views I've put into 'list'. This is done by using this code :
view.getParent().requestChildFocus(view, view);
If the above code results in a scroll down, the focused view get placed at the bottom of the screen and if it result in a scroll up, the focused view get placed at the top of the screen.
Is there a way to make the focused view always get placed at the top of the screen if the length of the list permits it?
Edit: This works! See accepted answer.
XML
<ScrollView
android:id="#+id/scroll_list"
android:layout_width="0dp"
android:layout_weight="19"
android:layout_height="wrap_content"
android:fillViewport="true">
<LinearLayout
android:id="#+id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
Sample code to put somewhere in your activity or fragment
view = findViewById(get_view_that_should_have_focus);
ScrollView scrollView = findViewById(R.id.scroll_list);
scrollView.smoothScrollTo(0, view.getTop());
You can vertical scroll to specific view by using this:
scrollView.smoothScrollTo(0,view.getTop());
It will make your view top of the scrollview(if there is enough height) After scrolling, you can request focus for this view.
Since I can't comment, I'm just gonna post this here.
The solution provided by Oğuzhan Döngül is correct, but if some of you can't get it running, try to do the smoothScroll inside runnable :
val topOffset = view.height
scrollView.post {
scrollView.smoothScrollTo(0, view.top - topOffset)
}
I add topOffset to give some spaces on top of the highlighted view.
source

Android: Passing touch event to overlapped view

I have a Relative layout which has two child layouts. First child is linear layout which is container of a fragment. The fragment has few buttons. The second child is a linear layout which has a View which is blank and transparent. The second child overlaps the first child.The first child is smaller than second child. I want to send touch events from second child to first child so that those buttons on the fragment which first child contains receives click.
I read few posts on internet for solving my problem but could not solve it.
So far i have overriden dispatchTouchEvent of Activity and have tried to detect whether touch is in bounds of first child and if so i do firstChild.onTouch(ev). Actually i just dont know what to do and just trying get it working. So please, help me.
#Override
public boolean dispatchTouchEvent(MotionEvent ev)
{
float x,y,backViewX,backViewY,frontViewX,frontViewY;
x = ev.getX();
y = ev.getY();
frontViewX = frontView.getX();
frontViewY = frontView.getY();
backViewX = backView.getX();
backViewY = backView.getY();
if(y >= backViewY && y <= (backViewY+backViewHeight))
{
if(x >= backViewX && x<=(backViewX+backViewWidth))
{
return backView.onTouchEvent(ev);
}
}
return super.dispatchTouchEvent(ev);
}
Orignal Layout is not the exact as i described but it is little more complex but the concept is same as i described....i am shortning the layout for simplifying things.The frontLayout here overlaps layoutBackFragmentArea.
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/back_view_background">
<LinearLayout
android:id="#+id/layoutBackFragmentArea"
android:layout_width="match_parent"
android:layout_height="#dimen/back_view_height"
android:orientation="vertical"
android:gravity="center_vertical"
android:background="#android:color/transparent">
</LinearLayout>
<LinearLayout
android:id="#+id/layoutFrontArea"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_vertical"
android:background="#android:color/transparent">
<View
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</RelativeLayout>
layoutBackFragmentArea will contain a fragment which has UI which should receive click events.

Categories

Resources