Detect scrollview's position at android - android

I'm working on a code at android for applying a form and before completing it, user has to accept a policy. If i put a TextView inside of ScrollView and a button for accepting, user can accept directly before read. But i must force user to scroll to bottom and then make Accept button became enabled and user accepts it then continue. First i tried this inside of Dialog Window. It's getting much more complicating. Now I'm trying it to inside another activity.
Is there any way to detect Scroll View's position has reached to end ?

You can detect it via below code
scrollView.getViewTreeObserver()
.addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener()
{
#Override
public void onScrollChanged() {
if (scrollView.getChildAt(0).getBottom()
<= (scrollView.getHeight() + scrollView.getScrollY())) {
//scroll view is at bottom
} else {
//scroll view is not at bottom
}
}
});

You can use the canScrollVertically method with a setOnScrollChangeListener, exemple :
scrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
#Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if (scrollView.canScrollVertically(1)) {
//user can't scroll more in bottom direction
}
}
});
If the canScrollVertically is reach the user is in the bottom of the scrollView and can't scroll more.
If you put a negative value like -1 the method detect if the user can scrolling up

Fragments automatically save and restore the states of their Views, as long they have IDs assigned to it.
By Assiging an Id(android:id) to scroll view, do magic for you(scroll state restored) if you are using Fragment
For Activity
// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : (v.getTop() - mList.getPaddingTop());
// ...
// restore index and position
mList.setSelectionFromTop(index, top);

Related

scrollBy doesn't work properly in nested recyclerview

I have a vertically scrolling RecyclerView with horizontally scrolling inner RecyclerViews just like this.
With this implementation, users can scroll each horizontal recyclerview synchronously. However, when a user scroll vertically to the parent recyclerView, a new horizontal recyclerview which has just attached on window doesn't display on same scroll x position. This is normal. Because it has just created.
So, I had tried to scroll to the scrolled position before it was displayed. Just like this:
Note: this is in adapter of the parent recyclerview whose orientation is vertical.
#Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
CellColumnViewHolder viewHolder = (CellColumnViewHolder) holder;
if (m_nXPosition != 0) {
// this doesn't work properly
viewHolder.m_jRecyclerView.scrollBy(m_nXPosition, 0);
}
}
As you can see, scrollBy doesn't effect for row 10, row 11, row 12 and row 13 After that, I debugged the code to be able find out find out what's happening. When I set scroll position using scrollBy, childCount() return zero for row 10, row 11, row 12 and row 13 So they don't scroll. But why ? and Why others work ?
How can I fix this ?
Is onViewAttachedToWindow right place to scroll new attached recyclervViews ?
Note: I have also test scrollToPosition(), it doesn't get any problem like this. But I can't use it at my case. Because users can scroll to the any x position which may not the exact position. So I need to set scroll position using x value instead of the position.
Edit: You can check The source code
I found a solution that is use scrollToPositionWithOffset method instead using scrollBy. Even if both of two scroll another position, they have really different work process in back side.
For example: if you try to use scrollBy to scroll any pixel position and your recyclerView had not been set any adapter which means there is no any data to display and so it has no any items yet, then scrollBy doesn't work. RecyclerView uses its layoutManager's scrollBy method. So in my case, I am using LinearLayoutManager to the horizontal recyclerViews.
Lets see what it's doing :
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
mLayoutState.mRecycle = true;
ensureLayoutState();
final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
updateLayoutState(layoutDirection, absDy, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
if (DEBUG) {
Log.d(TAG, "Don't have any more elements to scroll");
}
return 0;
}
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
mOrientationHelper.offsetChildren(-scrolled);
if (DEBUG) {
Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
As you can see scrollBy ignores the scroll intentions if there is no any child at that time.
if (getChildCount() == 0 || dy == 0) {
return 0;
}
On the other hand scrollToPosition can work perfectly even if there is no any set data yet.
According to the Pro RecyclerView slide, the below sample works perfectly. However you can not do that with scrollBy.
void onCreate(SavedInstanceState state) {
....
mRecyclerView.scrollToPosition(selectedPosition);
mRecyclerView.setAdapter(myAdapter);
}
As a result, I have changed little thing to use scrollToPositionWithOffset().
Before this implementation I was calculating the exact scroll x position as a pixel.
After that, when the scroll came idle state, calculating the first complete visible position to the first parameter of the scrollToPositionWithOffset().
For second parameter which is the offset, I am getting the value using view.getLeft() function which helps to get left position of this view relative to its parent.
And it works perfectly!!

Android - ListView first visible position not updating

So I have a ListView and also up and down buttons.
My goal is to scroll the ListView by the it's height everytime I press one of the buttons, simple up / down navigation.
The scrolling does work, sort of. When I press down, the ListView scrolls by the correct distance every time it scrolls.
However, only the up button works as intended. The down button has problems which I believe are due to the ListView's first visible scroll position not updating.
When I press down, the onNavigationDownPressed() method occurs as seen below. This scrolls the correct amount down the ListView with a starting ListView firstVisiblePosition at 0. The next time that method is called, the first visible position is still 0.
But if I manually scroll the ListView down ever so slightly and then press down, it scrolls down correctly and then the same thing, it won't go down any more due to the firstVisiblePostion not changing unless I scroll first to make the position update.
// Scroll up
#Override
public void onNavigationUpPressed()
{
Log.i("MAKE UP, FIRST VISIBLE POSITION: ", String.valueOf(listView.getFirstVisiblePosition()));
listView.setSelectionFromTop(listView.getFirstVisiblePosition(), fragmentHeight);
}
// Scroll down
#Override
public void onNavigationDownPressed()
{
Log.i("MAKE DOWN, FIRST VISIBLE POSITION: ", String.valueOf(listView.getFirstVisiblePosition()));
listView.setSelectionFromTop(listView.getFirstVisiblePosition(), -fragmentHeight);
}
Any ideas?
Cheers
Perhaps you should try to call :
listView.smoothScrollToPosition(yourPosition);
Another route is to manually set the scroll of the items but the first approach should work for you.
Solution:
I set an onScrollListener for the ListView which notified me both when the ListView had scrolled and some other handy parameters such as first visible item index, total number of items visible on screen etc. so I stored these variables.
listView.setOnScrollListener(new OnScrollListener()
{
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
Log.i("MMY: ", "Scroll");
Log.i("MMY: ", "First visible item: " + String.valueOf(firstVisibleItem));
Log.i("MMY: ", "Visible item count: " + String.valueOf(visibleItemCount));
// The variables that belong to this class now equal ->
General_Navigation_Fragment.this.visibleItemCount = visibleItemCount;
General_Navigation_Fragment.this.firstVisibleItem = firstVisibleItem;
totalCount = totalItemCount;
}
});
When the down button was pressed, I got the index of the first visible item and I got the number of visible items (n) on the screen and then scrolled down to current position + n. Simply did the reverse for scrolling up.
// Scroll up
#Override
public void onNavigationUpPressed()
{
currentVisibleItem = firstVisibleItem - visibleItemCount;
if (currentVisibleItem < 0)
{
listView.setSelection(0);
}
else
listView.setSelection(currentVisibleItem - 1);
}
// Scroll down
#Override
public void onNavigationDownPressed()
{
currentVisibleItem = visibleItemCount + firstVisibleItem;
listView.setSelection(currentVisibleItem - 1);
}
Since I have varying size items in my ListView, this way seemed the easiest way to go about it.

Android ListView - stop scrolling at 'whole' row position

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);
}
}

List view snap to item

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.

Android: ListView.getScrollY() - does it work?

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);

Categories

Resources