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.
Related
I have a ListView with a set of children vertically listed (View objects) to be viewed by the users. I have to track the the user views, say,
a. If a user views a set of items for around 1 second, I should track the impressions.
b. If the same user scrolls the items out of the viewport and return back, I should track again, if he viewed for 1 second.
I tried several options like getGlobalVisibleRect(), getLocalVisibleRect(), getLocationOnScreen() and they are confusing in the first place and didn't help me get the right coordinates and visibility of the child items of the listView.
I checked Track impression of items in an android ListView which is a bit similar to my requirement but I thought to check if there is a better solution. I am new to Android and apologies if I am not clear on some explanations
To get your desired result, I think we have two different solutions. First, create Handler for each of the item and call / remove in scroll view if it is visible. But this is very much stupid one as creating so many Handlers will make your app's life hell.
Second and best way is to use call / remove a single Handler for the entire visible items. If it persist for a time "A second" (1 second for you), use impression count in each of your item's model class and increase it with ++ operator.
You can add scroll listener in your listivew. The script will be like-
ListView listView = null;
int firstVisibleItemIndex = 0;
int visibleCount = 0;
Handler handler = new Handler();
Runnable runnable = new Runnable() {
#Override
public void run() {
for (int i = firstVisibleItemIndex; i < firstVisibleItemIndex + visibleCount; i++) {
try {
//Get impression count from model for the visible item index i
int count = modelList.get(i).getImpressionCount();
//Set impression count to the model for the visible item index i
modelList.get(i).setImpressionCount(++count);
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
//Can call this method body in onCreate directly
private void addListScrollListener() {
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// You cat determine first and last visible items here
// final int lastVisibleItem = firstVisibleItem + visibleItemCount - 1;
handler.removeCallbacks(runnable);
firstVisibleItemIndex = firstVisibleItem;
visibleCount = visibleItemCount;
handler.postDelayed(runnable, 1000);
}
#Override
public void onScrollStateChanged(AbsListView arg0, int arg1) {
// TODO Auto-generated method stub
}
});
}
I assume that you will bind your ListView with the id in your onCreate method. Also you can call the listener thing in your onCreate after binding the view with the variable.
I hope this will work for your requirement.
Let me know your feedback.
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.
Sorry for the confusing title, I cannot express the problem very concisely...
I have an Android app with a ListView that uses a circular / "infinite" adapter, which basically means I can scroll it up or down as much as I want and the items will wrap around when it reaches the top or bottom, making it seem to the user as if he is spinning an infinitely long list of (~100) repeating items.
The point of this setup is to let the user select a random item, simply by spinning / flinging the listview and waiting to see where it stops. I decreased the friction of the Listview so it flings a bit faster and longer and this seems to work really nice. Finally I placed a partially transparent image on top of the ListView to block out the top and bottom items (with a transition from transparent to black), making it seem as if the user is "selecting" the item in the middle, as if they were on a rotating "wheel" that they control by flinging.
There is one obvious problem: after flinging the ListView does not stop at a particular item, but it can stop hovering between two items (where the first visible item is then only partially shown). I want to avoid this because in that case it is not obvious which item has been "randomly selected".
Long story short: after the ListView has finished scrolling after flinging, I want it to stop on a "whole" row, instead of on a partially visible row.
Right now I implemented this behavior by checking when the scrolling has stopped, and then selecting the first visible item, as such:
lv = this.getListView();
lv.setFriction(0.005f);
lv.setOnScrollListener(new OnScrollListener() {
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE)
{
if (isAutoScrolling) return;
isAutoScrolling = true;
int pos = lv.getFirstVisiblePosition();
lv.setSelection(pos);
isAutoScrolling = false;
}
}
});
This works reasonably well, apart from one glaringly obvious problem... The first visible item might only be visible for a pixel or two. In that case, I want the ListView to jump "up" for those two pixels so that the second visible item is selected. Instead, of course, the first visible item is selected which means the ListView jumps "down" almost an entire row (minus those two pixels).
In short, instead of jumping to the first visible item, I want it to jump to the item that is visible the most. If the first visible item is less than half visible, I want it to jump to the second visible item.
Here's an illustration that hopefully conveys my point. The left most ListView (of each pair) shows the state after flinging has stopped (where it comes to a halt), and the right ListView shows how it looks after it made the "jump" by selecting the first visible item. On the left I show the current (wrong) situation: Item B is only barely visible, but it is still the first visible item so the listView jumps to select that item - which is not logical because it has to scroll almost an entire item height to get there. It would be much more logical to scroll to Item C (which is depicted on the right) because that is "closer".
(source: nickthissen.nl)
How can I achieve this behavior? The only way I can think of is to somehow measure how much of the first visible item is visible. If that is more than 50%, then I jump to that position. If it is less than 50%, I jump to that position + 1. However I have no clue how to measure that...
Any idea's?
You can get the visible dimensions of a child using the getChildVisibleRect method. When you have that, and you get the total height of the child, you can scroll to the appropriate child.
In the example below I check whether at least half of the child is visible:
View child = lv.getChildAt (0); // first visible child
Rect r = new Rect (0, 0, child.getWidth(), child.getHeight()); // set this initially, as required by the docs
double height = child.getHeight () * 1.0;
lv.getChildVisibleRect (child, r, null);
if (Math.abs (r.height ()) < height / 2.0) {
// show next child
}
else {
// show this child
}
Here's my final code inspired by Shade's answer.
I forgot to add "if(Math.abs(r.height())!=height)" at first. Then it just scrolls twice after it scroll to correct position because it's always greater than height/2 of childView.
Hope it helps.
listView.setOnScrollListener(new AbsListView.OnScrollListener(){
#Override
public void onScrollStateChanged(AbsListView view,int scrollState) {
if (scrollState == SCROLL_STATE_IDLE){
View child = listView.getChildAt (0); // first visible child
Rect r = new Rect (0, 0, child.getWidth(), child.getHeight()); // set this initially, as required by the docs
double height = child.getHeight () * 1.0;
listView.getChildVisibleRect (child, r, null);
if(Math.abs(r.height())!=height){//only smooth scroll when not scroll to correct position
if (Math.abs (r.height ()) < height / 2.0) {
listView.smoothScrollToPosition(listView.getLastVisiblePosition());
}
else if(Math.abs (r.height ()) > height / 2.0){
listView.smoothScrollToPosition(listView.getFirstVisiblePosition());
}
else{
listView.smoothScrollToPosition(listView.getFirstVisiblePosition());
}
}
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount) {
}});
Follow these 3 steps, then you can get exactly what you want!!!!
1.Initialize the two variable for scrolling up and down:
int scrollingUp=0,scrollingDown=0;
2.Then increment the value of the variable based on scrolling:
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if(mLastFirstVisibleItem<firstVisibleItem)
{
scrollingDown=1;
}
if(mLastFirstVisibleItem>firstVisibleItem)
{
scrollingUp=1;
}
mLastFirstVisibleItem=firstVisibleItem;
}
3.Then do the changes in the onScrollStateChanged():
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case SCROLL_STATE_IDLE:
if(scrollingUp==1)
{
mainListView.post(new Runnable() {
public void run() {
View child = mainListView.getChildAt (0); // first visible child
Rect r = new Rect (0, 0, child.getWidth(), child.getHeight()); // set this initially, as required by the docs
double height = child.getHeight () * 1.0;
mainListView.getChildVisibleRect (child, r, null);
int dpDistance=Math.abs (r.height());
double minusDistance=dpDistance-height;
if (Math.abs (r.height()) < height/2)
{
mainListView.smoothScrollBy(dpDistance, 1500);
}
else
{
mainListView.smoothScrollBy((int)minusDistance, 1500);
}
scrollingUp=0;
}
});
}
if(scrollingDown==1)
{
mainListView.post(new Runnable() {
public void run() {
View child = mainListView.getChildAt (0); // first visible child
Rect r = new Rect (0, 0, child.getWidth(), child.getHeight()); // set this initially, as required by the docs
double height = child.getHeight () * 1.0;
mainListView.getChildVisibleRect (child, r, null);
int dpDistance=Math.abs (r.height());
double minusDistance=dpDistance-height;
if (Math.abs (r.height()) < height/2)
{
mainListView.smoothScrollBy(dpDistance, 1500);
}
else
{
mainListView.smoothScrollBy((int)minusDistance, 1500);
}
scrollingDown=0;
}
});
}
break;
case SCROLL_STATE_TOUCH_SCROLL:
break;
}
}
You probably solved this problem but I think that this solution should work
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
View firstChild = lv.getChildAt(0);
int pos = lv.getFirstVisiblePosition();
//if first visible item is higher than the half of its height
if (-firstChild.getTop() > firstChild.getHeight()/2) {
pos++;
}
lv.setSelection(pos);
}
getTop() for first item view always return nonpositive value so I don't use Math.abs(firstChild.getTop()) but just -firstChild.getTop(). Even if this value will be >0 then this condition is still working.
If you want to make this smoother then you can try to use lv.smoothScrollToPosition(pos) and enclose all above piece of code in
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
post(new Runnable() {
#Override
public void run() {
//put above code here
//and change lv.setSelection(pos) to lv.smoothScrollToPosition(pos)
}
});
}
Once you know the first visible position, you should be able to use View.getLocationinWindow() or View.getLocationOnScreen() on the next position's view to get the visible height of the first. Compare that to the View's height, and scroll to the next position if appropriate.
You may need to tweak it to account for padding, depending on what your rows look like.
I haven't tried the above, but it seems like it should work. If it doesn't, here's another, probably less robust idea:
getLastVisiblePosition(). If you take the difference between last and first, you can see how many positions are visible on the screen. Compare that to how many positions were visible when the list was first populated(scroll position 0).
If the same number of positions are visible, simply scroll to the first visible position as you are doing. If there is one more visible, scroll to "first + 1" position.
If you can get the position of the row that needs to be scrolled to, you can use the method:
smoothScrollToPosition
So something like:
int pos = lv.getFirstVisiblePosition();
lv.smoothScrollToPosition(pos);
Edit
Try this, sorry I don't have time to test, I'm out and about.
ImageView iv = //Code to find the image view
Rect rect = new Rect(iv.getLeft(), iv.getTop(), iv.getRight(), iv.getBottom());
lv.requestChildRectangleOnScreen(lv, rect, false);
My Solution:
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
if (swipeLayout.isRefreshing()) {
swipeLayout.setRefreshing(false);
} else {
int pos = firstVisibleItem;
if (pos == 0 && lv_post_list.getAdapter().getCount()>0) {
int topOfNext = lv_post_list.getChildAt(pos + 1).getTop();
int heightOfFirst = lv_post_list.getChildAt(pos).getHeight();
if (topOfNext > heightOfFirst) {
swipeLayout.setEnabled(true);
} else {
swipeLayout.setEnabled(false);
}
}
else
swipeLayout.setEnabled(false);
}
}
This is my first ever post here and I'm a dumb novice, so I hope someone out there can both help me and excuse my ignorance.
I have a ListView which is populated with an ArrayAdapter. When I either scroll or click, I want the selected item, or the item nearest the vertical center, to be forced to the exact vertical center of the screen. If I call listView.setSelection(int position) it aligns the selected position at the top of the screen, so I need to use listView.setSelectionFromTop(position, offset) instead. To find my offset, I take half of the View's height from the half of the ListView's height.
So, I can vertically center my item easy enough, within OnItemClick or OnScrollStateChanged, with the following:
int x = listView.getHeight();
int y = listView.getChildAt(0).getHeight();
listView.setSelectionFromTop(myPosition, x/2 - y/2);
All this works fine. My problem is with the initial ListView setup. I want an item to be centered when the activity starts, but I can't because I get a NullPointerException from:
int y = listView.getChildAt(0).getHeight();
I understand this is because the ListView has not yet rendered, so it has no children, when I call this from OnCreate() or OnResume().
So my question is simply: how can I force my ListView to render at startup so I can get the height value I need? Or, alternatively, is there any other way to center items vertically within a ListView?
Thanks in advance for any help!
int y = listView.getChildAt(0).getHeight();
I understand this is because the ListView has not yet rendered, so it has no children, when I call this from onCreate() or onResume().
You should call it in onScroll.
listView.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
//Write your logic here
int y = listView.getChildAt(0).getHeight();
}
});
I'm answering my own question here, but it's very much a hack. I think it's interesting because it sheds some light on the behavior of listviews.
The problem was in trying to act on data (a listview row) that did not yet exist (it had not been rendered). listview.getChildAt(int) was null because the listview had no children yet. I found out onScroll() is called immediately when the activity is created, so I simply put everything in a thread and delayed the getChildAt() call. I then enclosed the whole thing in a boolean wrapper to make sure it is only ever called once (on startup).
The interesting thing was that I only had to delay the call by 1ms for everything to be OK. And that's too fast for the eye to see.
Like I said, this is all a hack so I'm sure all this is a bad idea. Thanks for any help!
private boolean listViewReady = false;
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (!listViewReady){
Thread timer = new Thread() {
public void run() {
try{
sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}finally{
runOnUiThread(new Runnable() {
public void run() {
myPosition = 2;
int x = listView.getHeight();
int y = listView.getChildAt(0).getHeight();
listView.setSelectionFromTop(myPosition, x/2 - y/2);
listViewReady = true;
}
});
}
}
};
timer.start();
}//if !ListViewReady
I have achieved the same using a in my opinion slighlty simpler solution
mListView.post(new Runnable() {
#Override
public void run() {
int height = mListView.getHeight();
int itemHeight = mListView.getChildAt(0).getHeight();
if (positionOfMyItem == myCollection.size() - 1) {
// last element - > don't subtract item height
itemHeight = 0;
}
mListView.setSelectionFromTop(position, height / 2 - itemHeight / 2);
}
});
I am using it, but it always returns 0, even though I have scrolled till the end of the list.
getScrollY() is actually a method on View, not ListView. It is referring to the scroll amount of the entire view, so it will almost always be 0.
If you want to know how far the ListView's contents are scrolled, you can use listView.getFirstVisiblePosition();
It does work, it returns the top part of the scrolled portion of the view in pixels from the top of the visible view. See the getScrollY() documentation. Basically if your list is taking up the full view then you will always get 0, because the top of the scrolled portion of the list is always at the top of the screen.
What you want to do to see if you are at the end of a list is something like this:
public void onCreate(final Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.main);
// The list defined as field elswhere
this.view = (ListView) findViewById(R.id.searchResults);
this.view.setOnScrollListener(new OnScrollListener() {
private int priorFirst = -1;
#Override
public void onScroll(final AbsListView view, final int first, final int visible, final int total) {
// detect if last item is visible
if (visible < total && (first + visible == total)) {
// see if we have more results
if (first != priorFirst) {
priorFirst = first;
//Do stuff here, last item is displayed, end of list reached
}
}
}
});
}
The reason for the priorFirst counter is that sometimes scroll events can be generated multiple times, so you only need to react to the first time the end of the list is reached.
If you are trying to do an auto-growing list, I'd suggest this tutorial.
You need two things to precisely define the scroll position of a listView:
To get current position:
int firstVisiblePosition = listView.getFirstVisiblePosition();
int topEdge=listView.getChildAt(0).getTop(); //This gives how much the top view has been scrolled.
To set the position:
listView.setSelectionFromTop(firstVisiblePosition,0);
// Note the '-' sign for scrollTo..
listView.scrollTo(0,-topEdge);