I have a layout in which it has parallax effect. So this are the elements in it -
AppBarLayout
CollapsingToolbarLayout inside AppBarLayout
Toolbar inside CollapsingToolbarLayout
RecyclerView
All this views are within CoordinatorLayout. Now I require to find out what is the first completely visible item of RecyclerView. Normally I used following logic to get it -
int firstVisibleItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
But here I am getting lots of 1 when even 0th position is not completely visible.
getChildAt begins at the first visible position, not at the position of the adapter.
Here is the resulting code.
int firstVisiblePosition = layoutManager.findFirstVisibleItemPosition();
View v = layoutManager.getChildAt(0);
if (firstVisiblePosition > 0 && v != null) {
int offsetTop = v.getTop();
chatAdapter.notifyDataSetChanged();
if (firstVisiblePosition - 1 >= 0 && chatAdapter.getItemCount() > 0) {
layoutManager.scrollToPositionWithOffset(firstVisiblePosition - 1, offsetTop);
}
}
I found it myself. As I am using AppBarLayout, I need to check it out whether particular view is available on screen at that particular scroll or not.
I did is:
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
View v = recyclerView.getLayoutManager().getChildAt(1);
int offset = 0;
if (v != null) {
offset = v.getTop();
}
if ((verticalOffset * -1) >= offset) {
layoutBuy.setVisibility(View.GONE);
} else {
layoutBuy.setVisibility(View.VISIBLE);
}
}
I used recyclerView.getLayoutManager().getChildAt(1); because I wanted to work around with that particular position which is 1.
Since, vertical offset becomes minus values while scrolling I multiplied it by -1. Then just checked whether offset and top of the view which I am looking for is same or not.
Thus while using parallax effect in a screen and at that same time one needs to check which view is visible in RecyclerView, needs the logic as mentioned above.
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);
}
}
I tried to consult documentation, but found setScrollIndicators empty there. What it does?
I tried to search but couldn't find documentation either.
But while looking at source of AbsListView, I found following
View mScrollUp;
View mScrollDown;
public void setScrollIndicators(View up, View down) {
mScrollUp = up;
mScrollDown = down;
}
void updateScrollIndicators() {
if (mScrollUp != null) {
boolean canScrollUp;
// 0th element is not visible
canScrollUp = mFirstPosition > 0;
// ... Or top of 0th element is not visible
if (!canScrollUp) {
if (getChildCount() > 0) {
View child = getChildAt(0);
canScrollUp = child.getTop() < mListPadding.top;
}
}
mScrollUp.setVisibility(canScrollUp ? View.VISIBLE : View.INVISIBLE);
}
if (mScrollDown != null) {
boolean canScrollDown;
int count = getChildCount();
// Last item is not visible
canScrollDown = (mFirstPosition + count) < mItemCount;
// ... Or bottom of the last element is not visible
if (!canScrollDown && count > 0) {
View child = getChildAt(count - 1);
canScrollDown = child.getBottom() > mBottom - mListPadding.bottom;
}
mScrollDown.setVisibility(canScrollDown ? View.VISIBLE : View.INVISIBLE);
}
}
Here mListPadding is
Rect mListPadding = new Rect();
This might help in understanding concept better. I haven't tried this yet but from my understanding, if top of first element of the listview or bottom of the last element or last element is not visible and if list can be scrollable (to up or down) then respective view gets visible by calling updateScrollIndicators() method
Hope this will be useful for you
Scope
I need to scroll to certain position smoothly and then "jump" to another position with setSelection(anotherPosition). This is done to create an illusion of smooth scrolling of (e.g.) 100 items in ListView. smoothScrollToPosition(100) lasts too much, you know.
Problem
setSelection() doesn't wait till smoothScrollToPosition finishes its work, so setSelection() is being called immediately and user sees quick jumping only;
Code
private final int scrollableItems = 20;
int firstVisiblePosition = mListView.getFirstVisiblePosition();
if (firstVisiblePosition < scrollableItems) {
mListView.smoothScrollToPosition(0);
} else {
mListView.smoothScrollToPosition(firstVisiblePosition - scrollableItems);
mListView.setSelection(0);
}
mListView.clearFocus();
Idea
OK, we could change logic of smoothness illusion: first setSelection(), then scroll smoothly (we're scrolling to the very first item on top of the list):
int firstVisiblePosition = mListView.getFirstVisiblePosition();
if (firstVisiblePosition < scrollableItems) {
mListView.smoothScrollToPosition(0);
} else {
mListView.setSelection(scrollableItems);
mListView.smoothScrollToPosition(0);
}
mListView.clearFocus();
final ListView listView = ...;
View listItemView = ...;
listView.smoothScrollBy(listItemView.getHeight() * NUMBER_OF_VIEWS,
DURATION * 2);
listView.postDelayed(new Runnable() {
public void run() {
listView.smoothScrollBy(0, 0); // Stops the listview from overshooting.
listView.setSelection(0);
}
}, DURATION);
Of course, direction of the scroll etc. would need to be adjusted for your use case (go to the top of the list)
EDIT: Old solution could overshoot if the velocity of the scroll was too high, smoothScrollBy(0,0) will stop the smooth scrolling before setting the selection properly and immediately.
Another way is to add an OnScrollListener.
private final int scrollableItems = 20;
int firstVisiblePosition = mListView.getFirstVisiblePosition();
if (firstVisiblePosition < scrollableItems) {
mListView.smoothScrollToPosition(0);
} else {
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView absListView, int i) {
if (i == SCROLL_STATE_IDLE) {
mListView.setSelection(0);
}
}
})
mListView.smoothScrollToPosition(firstVisiblePosition - scrollableItems);
}
mListView.clearFocus();
Is there a way that I can makle sure a given item in an android listview is entirely visible?
I'd like to be able to programmatically scroll to a specific item, like when I press a button for example.
ListView.setSelection() will scroll the list so that the desired item is within the viewport.
Try it:
public static void ensureVisible(ListView listView, int pos)
{
if (listView == null)
{
return;
}
if(pos < 0 || pos >= listView.getCount())
{
return;
}
int first = listView.getFirstVisiblePosition();
int last = listView.getLastVisiblePosition();
if (pos < first)
{
listView.setSelection(pos);
return;
}
if (pos >= last)
{
listView.setSelection(1 + pos - (last - first));
return;
}
}
I believe what you are looking for is ListView.setSelectionFromTop() (although I'm a bit late to the party).
Recently I met the same problem, paste my solution here in case someone need it (I was trying to make the entire last visible item visible):
if (mListView != null) {
int firstVisible = mListView.getFirstVisiblePosition()
- mListView.getHeaderViewsCount();
int lastVisible = mListView.getLastVisiblePosition()
- mListView.getHeaderViewsCount();
View child = mListView.getChildAt(lastVisible
- firstVisible);
int offset = child.getTop() + child.getMeasuredHeight()
- mListView.getMeasuredHeight();
if (offset > 0) {
mListView.smoothScrollBy(offset, 200);
}
}
I have a shorter and, in my opinion, better solution to do this : ListView requestChildRectangleOnScreen method is designed for it.
The answer above ensures that the item will be displayed, but sometimes it will be displayed partly (ie. when it is at the bottom of the screen). The code below ensures that the whole item will be displayed and that the view will scroll only the necessary zone :
private void ensureVisible(ListView parent, View view) {
Rect rect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
parent.requestChildRectangleOnScreen(view, rect, false);
}