Unable to scroll to item in RecyclerView that is inside NestedScrollView - android

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.

Related

Button overlapping views on top of it

I made a custom view with a progress bar. To add it in all my fragments i made a base fragment. In the method onActivityCreated i added the following code:
activity?.run {
loading = LoadingView(this)
loading?.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
loading?.visibility = View.GONE
(view as? ViewGroup)?.addView(loading)
}
And it works, but, when i make it visible, the buttons overlapped the progres bar in my LoadingView. So, when i show it i added brintToFront() method on a first test (that didnt work) and i saw i also had to use invalidate
protected fun showLoading() {
loading?.visibility = View.VISIBLE
loading?.bringToFront()
loading?.invalidate()
}
As this wasnt working either i started looking for a solution here and i found that the solution could be adding translationZ or elevation properties. So i tried to, but none is working.
The XML file of my view is:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
What am i doing wrong?
I've tried this and settings translationZ to 6f or more brings my view in front of buttons.
It seems that it should be set programmatically.
protected fun showLoading() {
loading?.translationZ = 6f
loading?.visibility = View.VISIBLE
}

Loading items into a RecyclerView with stackFromEnd = true doesn't scroll to bottom

I have a chat app and i'm trying to ensure that when the user opens the screen the items display from the bottom with the most recent right above the input area. I've used a custom view for the input area and both the recycler and the custom view are within a ConstraintLayout.
My problem is that when I load items into the list, if the number of items is greater than the size of the recycler, it will not fully show the last item. When I make the input area visibility = Gone then the items display properly at the bottom. It's almost like the recyclers LinearLayoutManager thinks that the height of the Recycler is of the screen without the input field. I've manually printed out the size of the views and used layout inspector to ensure that the recycler is indeed drawn in the correct location (above the input and below the navigation bar).
What could be causing such an issue? I should note that whenever you click on a Linkified text in a chat bubble that the list scrolls a small amount equal to the offset that's incorrect when you open the screen. Clearly something is not measuring right here and not sure where to begin.
I should also note that if I try to add a post with smoothScroll it will go to the end of the list but then whenever a new item appears in the list from sending a message the items above the most recently added one seem to jump up a little with an unnecessary animation. It's like the last item in the list is in some special state?
if you're curious this is my scrolling function:
private fun scrollToFirstItem(dueToKeyboard: Boolean = false) {
val stackingFromEnd = (recyclerView.layoutManager as LinearLayoutManager).stackFromEnd
if (stackingFromEnd) {
val firstPosition = recyclerView.adapter?.itemCount?: 0 - 1
if (dueToKeyboard ) {
recyclerView.scrollBy(0, Integer.MAX_VALUE)
} else {
recyclerView.scrollToPosition(firstPosition)
}
recyclerView.post { recyclerView.smoothScrollToPosition(firstPosition) }
} else {
recyclerView.scrollToPosition(0)
}
}
And my xml for my fragment:
<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">
<include
android:id="#+id/searchView"
layout="#layout/compose_new_message_include"/>
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="#+id/conversationEpoxyRV"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scrollbars="vertical"
app:layout_constraintTop_toBottomOf="#id/searchView"
app:layout_constraintBottom_toTopOf="#id/composeView"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
tools:listitem="#layout/conversation_item_inbound"/>
<include layout="#layout/conversation_pip_view"
android:id="#+id/selectedMediaContainer"/>
<****.ComposeView
android:id="#+id/composeView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Any help is appreciated, I'm so lost..
To me this feels like more of an issue in the layout rather than in the scrolling function. I hope that this can be resolved easily if you use a Relative Layout instead of a Linear Layout. So in case if it may be helpful, i'll add my solution below using a Relative Layout.
<RelativeLayout
android:id="#+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycle_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#id/input_field">
</androidx.recyclerview.widget.RecyclerView>
<EditText
android:id="#+id/input_field"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
So in case if it is not clear what i have done, an EditText element is set to align to the bottom of parent which is a Relative Layout. Then a RecyclerView is added so that RecylerView will be always constraint above the EditText but not overlapped.
Everything looks fine, except the restrictions you added to recycleview design, I see that you are setting the recycleview to top of searchView app:layout_constraintTop_toBottomOf="#id/searchView" while the searchView view is not restricted as should it be.
It's better when using ConstraintLayout to restrict every view inorder avoid unexpected behaviors, because every view has a relation with other view will be effected with other view (x, y), therefore your searchView should be look like:
<include
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
layout="#layout/compose_new_message_include"/>
Give the size of arraylist of your data to smoothScrollToPosition
var linearLayoutManager = LinearLayoutManager(context)
recyclerView.layoutManager=linearLayoutManager
recyclerView.adapter=adapter
linearLayoutManager.setStackFromEnd(true)
adapter.notifyDataSetChanged()
recyclerView.smoothScrollToPosition(dataMsgs.size())
I had a similar problem some years back and I solved it with scrollToPositionWithOffset (support library). Of course this was before constraint was used....
I kept my newer items at the bottom, too. The code I used for scrolling after inserting an item was:
((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(getItemCount() - 1, 0);
To scroll and adjust the awkward positioning after removing an item, I used:
View vTopCard = layoutManager.getChildAt(0);
// Get its adapter position
final int topCardPos = layoutManager.getPosition(vTopCard);
// Get top of card position
int topOffset = layoutManager.getDecoratedTop(vTopCard);
int cardMargin = (int) getResources().getDimension(R.dimen.card_vertical_margin);
((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(topCardPos, topOffset - cardMargin);
The getDecoratedTop helps with the "bounce" and final positioning, as does factoring the vertical margin.
I hope this helps (at least part of your issue)! Like I said, this is old code (when I was learning to program Android and Java at the same time), so if I left something out, let me know and I'll reexamine the app's code in more detail (though, I'll have to find the time).
Good luck!

RecyclerView not updating the view properly with ConstraintLayout Group Visibility

Problem:
It seems when I have a specific combination of views and setVisibility between Visible and Gone the RecyclerView does not update properly after initial load. I have a RelativeLayout->ConstraintLayout->Constraint Group(with visibility dynamically set). The view within the constraint group is not updating properly.
Example Use Case of Problem:
So the linked code below it will show a search view at the top. The initial state of empty shows the view properly(With the search icon showing). Then if you type "T" then the search icon will disappear(it shouldn't). So if you either delete the T or type the next letter "E" then it shows again. Also if you delete all search text and type T again it will show.
View Code:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.example.davidcorrado.myapplication.PlayerVM" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:id="#+id/playerCell_main_layout"
android:layout_width="match_parent"
android:layout_height="84dp"
android:layout_alignParentTop="true">
<android.support.constraint.Group
android:id="#+id/playerCell_status_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{vm.showingStatusIcons}"
app:constraint_referenced_ids="playerCell_lineup_status_image" />
<ImageView
android:id="#+id/playerCell_lineup_status_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="#drawable/ic_search" />
</android.support.constraint.ConstraintLayout>
</RelativeLayout>
</layout>
Quirks that might help:
1) If I move the visibility line to the relativeLayout or the ConstraintLayout the things seem to work
2) If I remove the RelativeLayout view things seem to work.
See the below for the full code example:
https://github.com/DavidCorrado/LayoutIssue
As far as I can tell, the reason the search icon disappears is due to the call to ObservableList.clear() then ObservableList.addAll(*)
So the disappear happens on the clear, then the reappear happens on the addAll.
As the callbacks in the adapter kick in on both calls, my hunch is it animates the view to disappear, then animates to show when the addAll is triggered.
I have verified this by not actioning a 'List.clear()' and instead in your textChange listener simply adding more views to the list.
I'm not sure how you would clear and add items to the list without animating the clear, perhaps there are some settings you can toggle in the RecyclerAdapter to ignore the remove of Views or not animate the entry of Views?
My adjusted code in your callback for class PlayersListVM
class PlayersListVM {
val filteredVms: ObservableList<ViewModel> = ObservableArrayList<ViewModel>()
val showing = PlayerVM(isShowing = true)
val missing = PlayerVM()
init {
filteredVms.add(showing)
}
fun onQueryChanged(newText: String) {
if (filteredVms.size % 2 == 1) filteredVms.add(missing)
else filteredVms.add(showing)
}
}

Android: ScrollView scrolls to the top inadvertently

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!

Android L: ActionBar setHideOnContentScrollEnabled

I'm trying to use the setHideOnContentScrollEnabled and setHideOffset in the new L API. However, none of the mentioned functions seem to have any effect. Anyone else encountered the same issue?
My Activity's layout is a ScrollView with a TextView displaying a large amount of text, so there are def scrolling. I have also, as required by the documentation, added FEATURE_ACTION_BAR_OVERLAY
getWindow().requestFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
setContentView(R.layout.main_activity);
getActionBar().setHideOnContentScrollEnabled(true);
getActionBar().setHideOffset(40);
Notice that:
If enabled, the action bar will scroll out of sight along with a
nested scrolling child view's content.
View.setNestedScrollingEnabled(boolean)
I was facing the same problem, using a RecyclerView, a Toolbar and trying to support API10+. I just could not get setHideOffset() or setHideOnContentScrollEnabled() on my SupportActionBar to work.
After a lot of different manual approaches on scrolling the toolbar, this is my current workaround:
I use a ScrollView only for my Toolbar. My Recycler handles its own scrolling which is being listened to.
my_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!--The Recycler is in a RefreshLayout. This is optional.-->
<android.support.v4.widget.SwipeRefreshLayout
android:id="#+id/swipe"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_alignParentTop="true">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:scrollbars="vertical" />
</android.support.v4.widget.SwipeRefreshLayout>
<!--Draw the Recycler _below_ the Toolbar-->
<!--by defining it _before_ everything else.-->
<ScrollView
android:id="#+id/scroll_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_alignParentTop="true"
android:scrollbars="none">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
<!--Add a transparent View below the Toolbar
to give the ScrollView "something to scroll".
Make sure it is _not_ clickable.-->
<View
android:layout_width="match_parent"
android:layout_height="128dp"
android:clickable="false" />
</RelativeLayout>
</ScrollView>
In myActivity.class
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_room_list);
mToolbarScroller = (ScrollView) findViewById(R.id.scroll_toolbar);
mRecycler = (RecyclerView) findViewById(R.id.recycler_rooms);
// [...]
// Do not forget to give your Recycler a Layout before listening to scroll events.
mRecycler.setOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// Only handle scrolling further if there is at least one child element in the list.
if (recyclerView.getChildCount() == 0) {
mSwipeLayout.setEnabled(true);
return;
}
final boolean didReachTop = recyclerView.getChildAt(0).getTop() >= 0;
if (mToolbarScroller == null) return;
// Simply let the Toolbar follow the scrolling Recycler
// by passing on the scroll-down (positive) values of dy.
if (dy > 2) mToolbarScroller.scrollBy(0, dy);
// Let the Toolbar reappear immediately
// when scrolling up a bit or if the top has been reached.
else if (dy < -4 || didReachTop) mToolbarScroller.scrollTo(0, 0);
}
});
This leads to your Toolbar always overlapping the first element in your Recycler. If you want to avoid this, add an invisible View to your item layouts that has the size of the Toolbar. In your Adapter you simply set it to VISIBLE, if it is the first element in the list, or to GONE if it is any other element:
In myRecyclerItemAdapter.java (optional):
#Override
public void onBindViewHolder(RoomViewHolder viewHolder, Cursor cursor) {
// To compensate for the overlaying toolbar,
// offset the first element by making its spacer visible.
if (cursor.isFirst()) viewHolder.mSpacer.setVisibility(View.VISIBLE);
else viewHolder.mSpacer.setVisibility(View.GONE);
I am probably going to tweak the threshold dy values in the OnScrollListener. They are supposed to filter jittery scroll values, such as a rapid succession of -1, +1, -1, +1 that sometimes happen.
If anyone has a better way or thinks I am making huge mistakes, please let me know! I am always looking for better solutions.

Categories

Resources