In my app, I have a ScrollView that listens to Scroll Changes, thanks to the ViewTreeObserver class.
scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
// I want to directly target this scrollview changes
// if specific scrollview { do calculations }
}
});
However, that class listens to global scroll changes, and I have 3 ScrollViews. So, the main ScrollView is activated and performs the calculations and causes everything to go awry.
Is there a way to ignore other ScrollViews and only target the main one?
What you can do to overcome it is store the getScrollX() values (if horizontal scrollview) or the getScrollY() values (if vertical scrollview) in some int and every time an onScrollChanged() is fired, just check if the new scroll Values are different from the ones previously stored.
If they're different, the correct scrollview is being called. If they are the same, the wrong scrollview is being called and therefore ignore it.
int prevScrollX_for1 = -1;
int prevScrollX_for2 = -1;
scrollView1.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
// Assuming horizontal scrollview
if(scrollView1.getScrollX() != prevScrollX_for1)
{
// Do something
}
prevScrollX_for1 = getScrollX();
}
});
scrollView2.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
// Assuming horizontal scrollview
if(scrollView2.getScrollX() != prevScrollX_for2)
{
// Do something
}
prevScrollX_for2 = getScrollX();
}
});
I have implemented a horizontal scrollable RecyclerView. My RecyclerView uses a LinearLayoutManager, and the problem I am facing is that when I try to use scrollToPosition(position) or smoothScrollToPosition(position) or from LinearLayoutManager's scrollToPositionWithOffset(position). Neither works for me. Either a scroll call doesn't scroll to the desired location or it doesn't invoke the OnScrollListener.
So far I have tried so many different combinations of code that I cannot post them all here. Following is the one that works for me (But only partially):
public void smoothUserScrollTo(final int position) {
if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}
if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}
if (getChildAdapterPosition(getCenterView()) == position) {
return;
}
stopScroll();
scrollToPosition(position);
if (lastScrollPosition == position) {
addOnLayoutChangeListener(new OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (left == oldLeft && right == oldRight && top == oldTop && bottom == oldBottom) {
removeOnLayoutChangeListener(this);
updateViews();
// removing the following line causes a position - 3 effect.
scrollToView(getChildAt(0));
}
}
});
}
lastScrollPosition = position;
}
#Override
public void scrollToPosition(int position) {
if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}
if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}
// stopScroll();
((LinearLayoutManager) getLayoutManager()).scrollToPositionWithOffset(position, 0);
// getLayoutManager().scrollToPosition(position);
}
I opted for scrollToPositionWithOffset() because of this but the case perhaps is different as I use a LinearLayoutManager instead of GridLayoutManager. But the solution does work for me too, but as I said earlier only partially.
When the call to scroll is from 0th position to totalSize - 7 scroll works like a charm.
When scroll is from totalSize - 7 to totalSize - 3, First time I only scroll to 7th last item in the list. The second time however I can scroll fine
When scrolling from totalSize - 3 to totalSize, I start getting unexpected behavior.
If anyone has found a work around I'd Appreciate it. Here's the gist to my code of custom ReyclerView.
I had the same issue some weeks ago, and found only a really bad solution to solve it. Had to use a postDelayed with 200-300ms.
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
yourList.scrollToPosition(position);
}
}, 200);
If you found a better solution, please let me know! Good luck!
Turns out I was having a similar issue until I utilized
myRecyclerview.scrollToPosition(objectlist.size()-1)
It would always stay at the top when only putting in the objectlist size. This was until i decided to set the size equal to a variable. Again, that didn't work. Then I assumed that perhaps it was handling an outofboundsexception without telling me. So I subtracted it by 1. Then it worked.
The accepted answer will work, but it may also break. The main reason for this issue is that the recycler view may not be ready by the time you ask it to scroll. The best solution for the same is to wait for the recycler view to be ready and then scroll. Luckily android has provided one such option. Below solution is for Kotlin, you can try the java alternative for the same, it will work.
newsRecyclerView.post {
layoutManager?.scrollToPosition(viewModel.selectedItemPosition)
}
The post runnable method is available for every View elements and will execute once the view is ready, hence ensuring the code is executed exactly when required.
You can use LinearSmoothScroller this worked every time in my case:
First create an instance of LinearSmoothScroller:
LinearSmoothScroller smoothScroller=new LinearSmoothScroller(activity){
#Override
protected int getVerticalSnapPreference() {
return LinearSmoothScroller.SNAP_TO_START;
}
};
And then when you want to scroll recycler view to any position do this:
smoothScroller.setTargetPosition(pos); // pos on which item you want to scroll recycler view
recyclerView.getLayoutManager().startSmoothScroll(smoothScroller);
Done.
So the problem for me was that I had a RecyclerView in a NestedScrollView. Took me some time to figure out this was the problem. The solution for this is (Kotlin):
val childY = recycler_view.y + recycler_view.getChildAt(position).y
nested_scrollview.smoothScrollTo(0, childY.toInt())
Java (credits to Himagi https://stackoverflow.com/a/50367883/2917564)
float y = recyclerView.getY() + recyclerView.getChildAt(selectedPosition).getY();
scrollView.smoothScrollTo(0, (int) y);
The trick is to scroll the nested scrollview to the Y instead of the RecyclerView. This works decently at Android 5.0 Samsung J5 and Huawei P30 pro with Android 9.
I also faced a similar problem (having to scroll to the top when the list is getting updated), but none of the above options worked 100%
However I finally found a working solution at https://dev.to/aldok/how-to-scroll-recyclerview-to-a-certain-position-5ck4 archive link
Summary
scrollToPosition only seems to work when the underlying dataset is ready.
So therefore postDelay works (randomly) but it's depending on the speed of the device/app. If the timeout is too short it fails. smoothScrollToPosition also only works if the adapter is not too busy (see https://stackoverflow.com/a/61403576/11649486)
To observe when the dataset is ready, a AdapterDataObserver can be added and certain methods overridden.
The code that fixed my problem:
adapter.registerAdapterDataObserver( object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(
positionStart: Int,
itemCount: Int
) {
// This will scroll to the top when new data was inserted
recyclerView.scrollToPosition(0)
}
}
None of the methods seems to be working for me. Only the below single line of code worked
((LinearLayoutManager)mRecyclerView.getLayoutManager()).scrollToPositionWithOffset(adapter.currentPosition(),200);
The second parameter refers to offset, which is actually the distance (in pixels) between the start edge of the item view and start edge of the RecyclerView. I have supplied it with a constant value to make the top items also visible.
Check for more reference over here
Using Kotlin Coroutines in Fragment or Activity, and also using the lifecycleScope since any coroutine launched in this scope is canceled when the Lifecycle is destroyed.
lifecycleScope.launch {
delay(100)
recyclerView.scrollToPosition(0)
This worked for me
Handler().postDelayed({
(recyclerView.getLayoutManager() as LinearLayoutManager).scrollToPositionWithOffset( 0, 0)
}, 100)
I had the same issue while creating a cyclic/circular adapter, where I could only scroll downward but not upward considering the position initialises to 0. I first considered using Robert's approach, but it was too unreliable as the Handler only fired once, and if I was unlucky the position wouldn't get initialised in some cases.
To resolve this, I create an interval Observable that checks every XXX amount of time to see whether the initialisation succeeded and afterward disposes of it. This approach worked very reliably for my use case.
private fun initialisePositionToAllowBidirectionalScrolling(layoutManager: LinearLayoutManager, realItemCount: Int) {
val compositeDisposable = CompositeDisposable() // Added here for clarity, make this into a private global variable and clear in onDetach()/onPause() in case auto-disposal wouldn't ever occur here
val initPosition = realItemCount * 1000
Observable.interval(INIT_DELAY_MS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe ({
if (layoutManager.findFirstVisibleItemPosition() == 0) {
layoutManager.scrollToPositionWithOffset(initPosition, 0)
if (layoutManager.findFirstCompletelyVisibleItemPosition() == initPosition) {
Timber.d("Adapter initialised, setting position to $initPosition and disposing interval subscription!")
compositeDisposable.clear()
}
}
}, {
Timber.e("Failed to initialise position!\n$it")
compositeDisposable.clear()
}).let { compositeDisposable.add(it) }
}
This worked perfectly for when scrolling to last item in the recycler
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
if (((LinearLayoutManager) recyclerView.getLayoutManager())
.findLastVisibleItemPosition() != adapter.getItemCount() - 1) {
recyclerView.scrollToPosition(adapter.getItemCount() - 1);
handler.postDelayed(this, 200);
}
}
}, 200 /* change it if you want*/);
Pretty weird bug, anyway I managed to work around it without post or post delayed as follow:
list.scrollToPosition(position - 1)
list.smoothScrollBy(1, 0)
Hopefully, it helps someone too.
Had the same issue. My problem was, that I refilled the view with data in an async task, after I tried to scroll. From onPostExecute ofc fixed this problem. A Delay fixed this issue too, because when the scroll executed, the list had already been refilled.
I use below solution to make the selected item in recycler view visible after the recycler view is reloaded (orientation change, etc). It overrides LinearLayoutManager and uses onSaveInstanceState to save current recycler position. Then in onRestoreInstanceState the saved position is restored. Finaly, in onLayoutCompleted, scrollToPosition(mRecyclerPosition) is used to make the previously selected recycler position visible again, but as Robert Banyai stated, for it to work reliably a certain delay must be inserted. I guess it is needed to provide enough time for adapter to load the data before scrollToPosition is called.
private class MyLayoutManager extends LinearLayoutManager{
private boolean isRestored;
public MyLayoutManager(Context context) {
super(context);
}
public MyLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public MyLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
if(isRestored && mRecyclerPosition >-1) {
Handler handler=new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
MyLayoutManager.this.scrollToPosition(mRecyclerPosition);
}
},200);
}
isRestored=false;
}
#Override
public Parcelable onSaveInstanceState() {
Parcelable savedInstanceState = super.onSaveInstanceState();
Bundle bundle=new Bundle();
bundle.putParcelable("saved_state",savedInstanceState);
bundle.putInt("position", mRecyclerPosition);
return bundle;
}
#Override
public void onRestoreInstanceState(Parcelable state) {
Parcelable savedState = ((Bundle)state).getParcelable("saved_state");
mRecyclerPosition = ((Bundle)state).getInt("position",-1);
isRestored=true;
super.onRestoreInstanceState(savedState);
}
}
If you use recyclerview in nestedScrollView you must scroll nestScrollview
nestedScrollview.smoothScrollTo(0,0)
Maybe It's not so elegant way to do it, But this always works for me. Add a new method to the RecyclerView and use it insted of scrollToPosition:
public void myScrollTo(int pos){
stopScroll();
((LinearLayoutManager)getLayoutManager()).scrollToPositionWithOffset(pos,0);
}
The answer is to use the Post Method, it will guarantee correct execution for any action
This is the ultimate solution using kotlin in this date ... if you navigate to another fragment and go back and your recyclerview resets to the first position just add this line in onCreateView or wherever you need can call the adapter...
pagingAdapter.stateRestorationPolicy=RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
BTW pagingAdapter is my adapter with diffUtil.
I had a similar issue, (but not the same), I try to explain it, maybe be could help someone else:
By the time I call to 'scrollToPosition' dataset is already set but some content like images loaded async (using Glide library) and probably when RecyclerView tries to compute the height amount to scroll down, image should return 0 as no loaded yet. So that gives an inaccurate scroll down I could solve it that way:
fun LinearLayoutManager.accurateScrollToPosition(position: Int) {
this.scrollToPosition(position)
this.postOnAnimation {
val realPosition = this.findFirstVisibleItemPosition()
if (position != realPosition) {
this.accurateScrollToPosition(position)
} else {
this.scrollToPosition(position) // this looks redunadant or inecessary but must be call to ensure accurate scroll
}
}
}
PD: In my case was not possible to know the size of the image to be loaded, if you know or you can resize the image you can add a placeholder on glide with de image size or override de size so recyclerView can compute the size correctly and don't need the above walkaraound.
I've subclassed HorizontalScrollView so that I can have some custom scrolling behavior, but have found that smoothScrollTo doesn't always fire.
I've had to work around this problem by using the following code:
smoothScrollTo(x, y);
scrollTo(x, y);
This makes sure the scrolling actually gets done even if smoothScrollTo doesn't work, since scrollTo works every time. Why is this happening? How can I get smoothScrollTo to work every time?
try this:
mScrollView.post(new Runnable() {
#Override
public void run() {
mScrollView.smoothScrollTo(x, y);
}
});
Following code will work:
final int scrollposition = Math.round(hr/24.0f * 1440f);
final ScrollView sv = (ScrollView)findViewById(R.id.graphScrollView);
//sv.smoothScrollTo(0, scrollposition);
sv.post(new Runnable() {
#Override
public void run() {
sv.smoothScrollTo(0, scrollposition);
}
});
Reason:
It will wait for the scrollview to be posted before running the underlying code.
As I've a master in MS Paint, I will just upload a picture selfdescripting what I'm trying to achieve.
I've searched, but I'm not really sure what do I've to search. I've found something called Animations. I managed to rotate, fade, etc an element from a View (with this great tutorial http://www.vogella.com/articles/AndroidAnimation/article.html)
But this is a bit limited for what I'm trying to achieve, and now, I'm stuck, because I don't know how is this really called in android development. Tried words like "scrollup layouts" but I didn't get any better results.
Can you give me some tips?
Thank you.
You can see a live example, with this app: https://play.google.com/store/apps/details?id=alexcrusher.just6weeks
Sincerely,
Sergi
Use something like this as your layout (Use Linear, Relative or other layout if you wish):
<LinearLayout
android:id="#+id/lty_parent">
<LinearLayout
android:id="#+id/lyt_first" />
<LinearLayout
android:id="#+id/lyt_second"/>
</LinearLayout>
And then in an onClick method on whatever you want to use to control it, set the Visibility between Visible and Gone.
public void buttonClickListener(){
((Button) findViewById(R.id.your_button))
.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
if (lyt_second.getVisibility() == View.GONE) {
lyt_second.setVisibility(View.VISIBILE);
}
else {
lyt_second.setVisibility(View.GONE);
}
});
Which is fine if you just want a simple appear/disappear with nothing fancy. Things get a little bit more complicated if you want to animate it, as you need to play around with negative margins in order to make it appear to grow and shrink, like so:
We use the same onClick method that we did before, but this time when we click it starts up a custom SlideAnimation for the hidden/visible view.
#Override
public void onClick(View v) {
SlideAnimation slideAnim = new SlideAnimation(lyt_second, time);
lyt_second.startAnimation(slideAnim);
}
The implementation of the SlideAnimation is based on a general Animation class, which we extend and then Override the transformation.
public SlideAnimation(View view, int duration) {
//Set the duration of the animation to the int we passed in
setDuration(duration);
//Set the view to be animated to the view we passed in
viewToBeAnimated = view;
//Get the Margin Parameters for the view so we can edit them
viewMarginParams = (MarginLayoutParams) view.getLayoutParams();
//If the view is VISIBLE, hide it after. If it's GONE, show it before we start.
hideAfter = (view.getVisibility() == View.VISIBLE);
//First off, start the margin at the bottom margin we've already set.
//You need your layout to have a negative margin for this to work correctly.
marginStart = viewMarginParams.bottomMargin;
//Decide if we're expanding or collapsing
if (marginStart == 0){
marginEnd = 0 - view.getHeight();
}
else {
marginEnd = 0;
}
//Make sure the view is visible for our animation
view.setVisibility(View.VISIBLE);
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
if (interpolatedTime < 1.0f) {
// Setting the new bottom margin to the start of the margin
// plus the inbetween bits
viewMarginParams.bottomMargin = marginStart
+ (int) ((marginEnd - marginStart) * interpolatedTime);
// Request the layout as it happens so we can see it redrawing
viewToBeAnimated.requestLayout();
// Make sure we have finished before we mess about with the rest of it
} else if (!alreadyFinished) {
viewMarginParams.bottomMargin = marginEnd;
viewToBeAnimated.requestLayout();
if (hideAfter) {
viewToBeAnimated.setVisibility(View.GONE);
}
alreadyFinished = true;
}
hideAfter = false;
}
}
EDIT: If anyone had used this code before and found that if you click on the button that starts the animation more than once before the animation was finished, it would mess up the animation from then on, causing it to always hide the view after the animation finished. I missed the reset of the hideAfter boolean near the bottom of the code, added it now.
you can do this manually by using setvisibility feature on the event onClick()
or
use this
dynamically adding two views one below other
I'm creating a list of pictures using a ListView and the photos are of a size that would fit 2 to 3 photos on the screen.
The problem that I'm having is that I would like to when the user stops scrolling that the first item of the visible list would snap to the top of screen, for example, if the scroll ends and small part of the first picture displayed, we scroll the list down so the picture is always fully displayed, if mostly of the picture is displayed, we scroll the list up so the next picture is fully visible.
Is there a way to achieve this in android with the listview?
I've found a way to do this just listening to scroll and change the position when the scroll ended by implementing ListView.OnScrollListener
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case OnScrollListener.SCROLL_STATE_IDLE:
if (scrolling){
// get first visible item
View itemView = view.getChildAt(0);
int top = Math.abs(itemView.getTop()); // top is a negative value
int bottom = Math.abs(itemView.getBottom());
if (top >= bottom){
((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition()+1, 0);
} else {
((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition(), 0);
}
}
scrolling = false;
break;
case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
case OnScrollListener.SCROLL_STATE_FLING:
Log.i("TEST", "SCROLLING");
scrolling = true;
break;
}
}
The change is not so smooth but it works.
Utilizing a couple ideas from #nininho's solution, I got my listview to snap to the item with a smooth scroll instead of abruptly going to it. One caveat is that I've only tested this solution on a Moto X in a basic ListView with text, but it works very well on the device. Nevertheless, I'm confident about this solution, and encourage you to provide feedback.
listview.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
if (scrollState == SCROLL_STATE_IDLE) {
View itemView = view.getChildAt(0);
int top = Math.abs(itemView.getTop());
int bottom = Math.abs(itemView.getBottom());
int scrollBy = top >= bottom ? bottom : -top;
if (scrollBy == 0) {
return;
}
smoothScrollDeferred(scrollBy, (ListView)view);
}
}
private void smoothScrollDeferred(final int scrollByF,
final ListView viewF) {
final Handler h = new Handler();
h.post(new Runnable() {
#Override
public void run() {
// TODO Auto-generated method stub
viewF.smoothScrollBy(scrollByF, 200);
}
});
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// TODO Auto-generated method stub
}
});
The reason I defer the smooth scrolling is because in my testing, directly calling the smoothScrollBy method in the state changed callback had problems actually scrolling. Also, I don't foresee a fully-tested, robust solution holding very much state, and in my solution below, I hold no state at all. This solution is not yet in the Google Play Store, but should serve as a good starting point.
Using #nininho 's solution,
In the onScrollStateChanged when the state changes to SCROLL_STATE_IDLE, remember the position to snap and raise a flag:
snapTo = view.getFirstVisiblePosition();
shouldSnap = true;
Then, override the computeScroll() method:
#Override
public void computeScroll() {
super.computeScroll();
if(shouldSnap){
this.smoothScrollToPositionFromTop(snapTo, 0);
shouldSnap = false;
}
}
You can do a much more smooth scrolling if you use RecyclerView. The OnScrollListener is way better.
I have made an example here: https://github.com/plattysoft/SnappingList
Well.. I know 10 years have past since this question was asked, but now we can use LinearSnapHelper:
new LinearSnapHelper().attachToRecyclerView(recyclerView);
Source:
https://proandroiddev.com/android-recyclerview-snaphelper-19eaa9598da6
Apart from trying the code above one thing you should make sure of is that your listView have a height that can fit exact number of items you want to be displayed.
e.g
If you want 4 items to be displayed after snap effect and your row height (defined in its layout) should be 1/4 of the total height of the list.
Note that after the smoothScrollBy() call, getFirstVisiblePosition() may point to the list item ABOVE the topmost one in the listview. This is especially true when view.getChildAt(0).getBottom() == 0. I had to call view.setSelection(view.getFirstVisiblePosition() + 1) to remedy this odd behavior.