Android: Detect when ScrollView stops scrolling - android

I'm using a ScrollView in Android and where the visible portion of the ScrollView is the same size as one of the cells inside the Scrollview. Every "cell" is the same height. So what I am trying to do is snap into position after the ScrollView has been scrolled.
Currently I am detecting when the user has touched the ScrollView and when they've started scrolling and working it out from there, but it is quite buggy. It also needs to work when the user just flicks it and it scrolls and then decelerates.
On iPhone there is a function that is something like didDecelerate and there I can do any code I want when the ScrollView has finished scrolling. Is there such a thing with Android? Or is there some code I could look at to figure out a better way of doing it?
I've looked over the Android docs and could not find anything like that.

I recently had to implement the function you described. What i did was to have a Runnable checking out if the ScrollView had stopped scrolling by comparing the value returned by getScrollY() when the onTouchEvent is first triggered with the value returned after a time defined by the variable newCheck.
See code below (working solution):
public class MyScrollView extends ScrollView{
private Runnable scrollerTask;
private int initialPosition;
private int newCheck = 100;
private static final String TAG = "MyScrollView";
public interface OnScrollStoppedListener{
void onScrollStopped();
}
private OnScrollStoppedListener onScrollStoppedListener;
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
scrollerTask = new Runnable() {
public void run() {
int newPosition = getScrollY();
if(initialPosition - newPosition == 0){//has stopped
if(onScrollStoppedListener!=null){
onScrollStoppedListener.onScrollStopped();
}
}else{
initialPosition = getScrollY();
MyScrollView.this.postDelayed(scrollerTask, newCheck);
}
}
};
}
public void setOnScrollStoppedListener(MyScrollView.OnScrollStoppedListener listener){
onScrollStoppedListener = listener;
}
public void startScrollerTask(){
initialPosition = getScrollY();
MyScrollView.this.postDelayed(scrollerTask, newCheck);
}
}
Then i have:
scroll.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
scroll.startScrollerTask();
}
return false;
}
});
scroll.setOnScrollStoppedListener(new OnScrollStoppedListener() {
public void onScrollStopped() {
Log.i(TAG, "stopped");
}
});

Here is yet another fix to the, IMHO, missing OnEndScroll event bug in the ScrollView.
Its inspired by hambonious answer.
Simply drop this class into your project (change package to match your own) and use the below xml
package com.thecrag.components.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ScrollView;
public class ResponsiveScrollView extends ScrollView {
public interface OnEndScrollListener {
public void onEndScroll();
}
private boolean mIsFling;
private OnEndScrollListener mOnEndScrollListener;
public ResponsiveScrollView(Context context) {
this(context, null, 0);
}
public ResponsiveScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ResponsiveScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public void fling(int velocityY) {
super.fling(velocityY);
mIsFling = true;
}
#Override
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
super.onScrollChanged(x, y, oldX, oldY);
if (mIsFling) {
if (Math.abs(y - oldY) < 2 || y >= getMeasuredHeight() || y == 0) {
if (mOnEndScrollListener != null) {
mOnEndScrollListener.onEndScroll();
}
mIsFling = false;
}
}
}
public OnEndScrollListener getOnEndScrollListener() {
return mOnEndScrollListener;
}
public void setOnEndScrollListener(OnEndScrollListener mOnEndScrollListener) {
this.mOnEndScrollListener = mOnEndScrollListener;
}
}
again changing the package name to match your project
<com.thecrag.components.ui.ResponsiveScrollView
android:id="#+id/welcome_scroller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="#+id/welcome_scroll_command_help_container"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_below="#+id/welcome_header_text_thecrag"
android:layout_margin="6dp">
....
</com.thecrag.components.ui.ResponsiveScrollView>

I subclassed (Horizontal)ScrollView and did something like this:
#Override
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
if (Math.abs(x - oldX) > SlowDownThreshold) {
currentlyScrolling = true;
} else {
currentlyScrolling = false;
if (!currentlyTouching) {
//scrolling stopped...handle here
}
}
super.onScrollChanged(x, y, oldX, oldY);
}
I used a value of 1 for the SlowDownThreshold since it always seems to be the difference of the last onScrollChanged event.
In order to make this behave correctly when dragging slowly, I had to do this:
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
currentlyTouching = true;
}
return super.onInterceptTouchEvent(event);
}
#Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
currentlyTouching = false;
if (!currentlyScrolling) {
//I handle the release from a drag here
return true;
}
}
return false;
}

My approach is determine scrolling state by a timestamp changed each time the onScrollChanged() is called.
It's very easy to determine when is start and end of scrolling.
You can also change threshold ( I use 100ms ) to fix sensitivity.
public class CustomScrollView extends ScrollView {
private long lastScrollUpdate = -1;
private class ScrollStateHandler implements Runnable {
#Override
public void run() {
long currentTime = System.currentTimeMillis();
if ((currentTime - lastScrollUpdate) > 100) {
lastScrollUpdate = -1;
onScrollEnd();
} else {
postDelayed(this, 100);
}
}
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (lastScrollUpdate == -1) {
onScrollStart();
postDelayed(new ScrollStateHandler(), 100);
}
lastScrollUpdate = System.currentTimeMillis();
}
private void onScrollStart() {
// do something
}
private void onScrollEnd() {
// do something
}
}

Here is yet another solution, quite simple and clean in my opinion, naturally inspired by answers above. Basically once user ended gesture check if getScrollY() is still changing, after a brief delay (here 50ms).
public class ScrollViewWithOnStopListener extends ScrollView {
OnScrollStopListener listener;
public interface OnScrollStopListener {
void onScrollStopped(int y);
}
public ScrollViewWithOnStopListener(Context context) {
super(context);
}
public ScrollViewWithOnStopListener(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_UP:
checkIfScrollStopped();
}
return super.onTouchEvent(ev);
}
int initialY = 0;
private void checkIfScrollStopped() {
initialY = getScrollY();
this.postDelayed(new Runnable() {
#Override
public void run() {
int updatedY = getScrollY();
if (updatedY == initialY) {
//we've stopped
if (listener != null) {
listener.onScrollStopped(getScrollY());
}
} else {
initialY = updatedY;
checkIfScrollStopped();
}
}
}, 50);
}
public void setOnScrollStoppedListener(OnScrollStopListener yListener) {
listener = yListener;
}
}

My approach for this question is to use a timer to check for the following 2 "events".
1) onScrollChanged() stopped being called
2) User's finger is lift from the scrollview
public class CustomScrollView extends HorizontalScrollView {
public CustomScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
Timer ntimer = new Timer();
MotionEvent event;
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt)
{
checkAgain();
super.onScrollChanged(l, t, oldl, oldt);
}
public void checkAgain(){
try{
ntimer.cancel();
ntimer.purge();
}
catch(Exception e){}
ntimer = new Timer();
ntimer.schedule(new TimerTask() {
#Override
public void run() {
if(event.getAction() == MotionEvent.ACTION_UP){
// ScrollView Stopped Scrolling and Finger is not on the ScrollView
}
else{
// ScrollView Stopped Scrolling But Finger is still on the ScrollView
checkAgain();
}
}
},100);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
this.event = event;
return super.onTouchEvent(event);
}
}

For a simple case like you described, you can probably get away with overriding fling method in your custom scroll view. Fling method gets called to perform "deceleration" every time user raises his finger from the screen.
So what you should do is something like this:
Subclass ScrollView.
public class MyScrollView extends ScrollView {
private Scroller scroller;
private Runnable scrollerTask;
//...
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
scroller = new Scroller(getContext()); //or OverScroller for 3.0+
scrollerTask = new Runnable() {
#Override
public void run() {
scroller.computeScrollOffset();
scrollTo(0, scroller.getCurrY());
if (!scroller.isFinished()) {
MyScrollView.this.post(this);
} else {
//deceleration ends here, do your code
}
}
};
//...
}
}
Subclass fling method and DO NOT call superclass implementation.
#Override
public void fling(int velocityY) {
scroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, container.getHeight());
post(scrollerTask);
//add any extra functions you need from android source code:
//show scroll bars
//change focus
//etc.
}
Fling will not trigger if the user stops scrolling before raising up his finger (velocityY == 0). In case you want to intercept this sort of events aswell, override onTouchEvent.
#Override
public boolean onTouchEvent(MotionEvent ev) {
boolean eventConsumed = super.onTouchEvent(ev);
if (eventConsumed && ev.getAction() == MotionEvent.ACTION_UP) {
if (scroller.isFinished()) {
//do your code
}
}
return eventConsumed;
}
NOTE Although this works, overriding fling method might be a bad idea. It is public, but its barely designed for subclassing. Right now it does 3 things - it initiates fling for private mScroller, handles possible focus changes and shows scroll bars. This might change in future android release. For instance, private mScroller instance changed its class from Scroller to OvershootScroller between 2.3 and 3.0. You have to keep in mind all this small differences. In any case, be ready for unforeseen consequences in the future.

My solution is a variation of Lin Yu Cheng's great solution and also detects when scrolling has started and stopped.
Step 1. Define a HorizontalScrollView and OnScrollChangedListener:
CustomHorizontalScrollView scrollView = (CustomHorizontalScrollView) findViewById(R.id.horizontalScrollView);
horizontalScrollListener = new CustomHorizontalScrollView.OnScrollChangedListener() {
#Override
public void onScrollStart() {
// Scrolling has started. Insert your code here...
}
#Override
public void onScrollEnd() {
// Scrolling has stopped. Insert your code here...
}
};
scrollView.setOnScrollChangedListener(horizontalScrollListener);
Step 2. Add the CustomHorizontalScrollView class:
public class CustomHorizontalScrollView extends HorizontalScrollView {
public interface OnScrollChangedListener {
// Developer must implement these methods.
void onScrollStart();
void onScrollEnd();
}
private long lastScrollUpdate = -1;
private int scrollTaskInterval = 100;
private Runnable mScrollingRunnable;
public OnScrollChangedListener mOnScrollListener;
public CustomHorizontalScrollView(Context context) {
this(context, null, 0);
init(context);
}
public CustomHorizontalScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
init(context);
}
public CustomHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
// Check for scrolling every scrollTaskInterval milliseconds
mScrollingRunnable = new Runnable() {
public void run() {
if ((System.currentTimeMillis() - lastScrollUpdate) > scrollTaskInterval) {
// Scrolling has stopped.
lastScrollUpdate = -1;
//CustomHorizontalScrollView.this.onScrollEnd();
mOnScrollListener.onScrollEnd();
} else {
// Still scrolling - Check again in scrollTaskInterval milliseconds...
postDelayed(this, scrollTaskInterval);
}
}
};
}
public void setOnScrollChangedListener(OnScrollChangedListener onScrollChangedListener) {
this.mOnScrollListener = onScrollChangedListener;
}
public void setScrollTaskInterval(int scrollTaskInterval) {
this.scrollTaskInterval = scrollTaskInterval;
}
//void onScrollStart() {
// System.out.println("Scroll started...");
//}
//void onScrollEnd() {
// System.out.println("Scroll ended...");
//}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (mOnScrollListener != null) {
if (lastScrollUpdate == -1) {
//CustomHorizontalScrollView.this.onScrollStart();
mOnScrollListener.onScrollStart();
postDelayed(mScrollingRunnable, scrollTaskInterval);
}
lastScrollUpdate = System.currentTimeMillis();
}
}
}

Try taking a look at this question here on StackOverflow - it's not exactly the same as your question, but it gives an idea on how you can manage the scroll event of a ScrollView.
Basicly you need to create your own CustomScrollView by extending ScrollView and override onScrollChanged(int x, int y, int oldx, int oldy). Then you need to reference this in your layout file instead of the standard ScrollView like com.mypackage.CustomScrollView.

There are some great answers here, but my code can detect when scrolling stops without having to extend ScrollView class.
every view instance can call getViewTreeObserver(). when Holding this instance of ViewTreeObserver you can add a OnScrollChangedListener using the function addOnScrollChangedListener().
declare the following:
private ScrollView scrollListener;
private volatile long milesec;
private Handler scrollStopDetector;
private Thread scrollcalled = new Thread() {
#Override
public void run() {
if (System.currentTimeMillis() - milesec > 200) {
//scroll stopped - put your code here
}
}
};
and in your onCreate (or another place) add:
scrollListener = (ScrollView) findViewById(R.id.scroll);
scrollListener.getViewTreeObserver().addOnScrollChangedListener(new OnScrollChangedListener() {
#Override
public void onScrollChanged() {
milesec = System.currentTimeMillis();
scrollStopDetector.postDelayed(scrollcalled, 200);
}
});
you might want to take longer or slower time between this checks, but when scrolling this listner gets called really fast so it will work very fast.

Here's my solution which includes scroll tracking and scroll ending:
public class ObservableHorizontalScrollView extends HorizontalScrollView {
public interface OnScrollListener {
public void onScrollChanged(ObservableHorizontalScrollView scrollView, int x, int y, int oldX, int oldY);
public void onEndScroll(ObservableHorizontalScrollView scrollView);
}
private boolean mIsScrolling;
private boolean mIsTouching;
private Runnable mScrollingRunnable;
private OnScrollListener mOnScrollListener;
public ObservableHorizontalScrollView(Context context) {
this(context, null, 0);
}
public ObservableHorizontalScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ObservableHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_MOVE) {
mIsTouching = true;
mIsScrolling = true;
} else if (action == MotionEvent.ACTION_UP) {
if (mIsTouching && !mIsScrolling) {
if (mOnScrollListener != null) {
mOnScrollListener.onEndScroll(this);
}
}
mIsTouching = false;
}
return super.onTouchEvent(ev);
}
#Override
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
super.onScrollChanged(x, y, oldX, oldY);
if (Math.abs(oldX - x) > 0) {
if (mScrollingRunnable != null) {
removeCallbacks(mScrollingRunnable);
}
mScrollingRunnable = new Runnable() {
public void run() {
if (mIsScrolling && !mIsTouching) {
if (mOnScrollListener != null) {
mOnScrollListener.onEndScroll(ObservableHorizontalScrollView.this);
}
}
mIsScrolling = false;
mScrollingRunnable = null;
}
};
postDelayed(mScrollingRunnable, 200);
}
if (mOnScrollListener != null) {
mOnScrollListener.onScrollChanged(this, x, y, oldX, oldY);
}
}
public OnScrollListener getOnScrollListener() {
return mOnScrollListener;
}
public void setOnScrollListener(OnScrollListener mOnEndScrollListener) {
this.mOnScrollListener = mOnEndScrollListener;
}
}

I think this has come up in the past. AFAIK, you can't easily detect that. My suggestion is that you take a look at ScrollView.java (that's how we do things in Android land :)) and figure out how you can extend the class to provide the functionality you are looking for. This is what I would try first:
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
if (mScroller.isFinished()) {
// do something, for example call a listener
}
}

this is an old thread but I'd like to add a shorter solution I came up with:
buttonsScrollView.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
handler.removeCallbacksAndMessages(null)
handler.postDelayed({
//YOUR CODE TO BE EXECUTED HERE
},1000)
}
Naturally there's a 1000 milliseconds delay. Adjust that if you need to.

I've made some improvements to ZeroG's answer. Mainly cancellation of excess task calls and implementing the whole thing as a private OnTouchListener, so all the scroll detection code would be in one place.
Paste the following code into your own ScrollView implementation:
private class ScrollFinishHandler implements OnTouchListener
{
private static final int SCROLL_TASK_INTERVAL = 100;
private Runnable mScrollerTask;
private int mInitialPosition = 0;
public ScrollFinishHandler()
{
mScrollerTask = new Runnable() {
public void run() {
int newPosition = getScrollY();
if(mInitialPosition - newPosition == 0)
{//has stopped
onScrollStopped(); // Implement this on your main ScrollView class
}else{
mInitialPosition = getScrollY();
ExpandingLinearLayout.this.postDelayed(mScrollerTask, SCROLL_TASK_INTERVAL);
}
}
};
}
#Override
public boolean onTouch(View v, MotionEvent event)
{
if (event.getAction() == MotionEvent.ACTION_UP)
{
startScrollerTask();
}
else
{
stopScrollerTask();
}
return false;
}
}
And then in your ScrollView implementation:
setOnTouchListener( new ScrollFinishHandler() );

this.getListView().setOnScrollListener(new OnScrollListener(){
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if( firstVisibleItem + visibleItemCount >= totalItemCount )
// Last item is shown...
}
Hope the snippet help :)

Related

How to stop scroll in progress in NestedScrollView?

I have a button to automatically returns to top position into a NestedScrollView. When the scroll is not in progress, it works like a charm, otherwise if the current scrolling is in progress yet, the scroll goes to top but directly scroll a lit bit down just after...
I wrote this method:
private void scrollToTop() {
mScrollView.stopNestedScroll();
mScrollView.postDelayed(new Runnable() {
#Override
public void run() {
mScrollView.scrollTo(0, 0);
}
}, 50);
}
Have you got some ideas guys to stop the current scroll and go to top immediately?
Thank you very much
The correct way would be intercepting user's touch when you scrolling pragmatically. That would require you to extend ScrollView overriding scrollTo and onInterceptTouchEvent methods and disable user touch while list is scrolling, returning touch after scroll is complete.
So the sequence would be like this:
Call scrollTo to scroll to specific position
Enable a flag to disable touch events and begin scrolling
When scroll is finished and state changed to idle disable flag
P.S: I can provide code example but it appears you have more expertise than me in writing codes. Cheers!
Here a code snippet:
import android.content.Context;
import android.support.v4.widget.NestedScrollView;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyScrollView extends NestedScrollView {
public static final int MAX_SCROLL_FACTOR = 1;
boolean isAutoScrolling;
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public void scrollTo(int x, int y) {
isAutoScrolling = true;
super.scrollTo(x, y);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (isAutoScrolling)
return super.onInterceptTouchEvent(event);
return false;
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (isAutoScrolling)
return super.onTouchEvent(event);
return false;
}
#Override
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
super.onScrollChanged(x, y, oldX, oldY);
if (isAutoScrolling) {
if (Math.abs(y - oldY) < MAX_SCROLL_FACTOR || y >= getMeasuredHeight() || y == 0
|| Math.abs(x - oldX) < MAX_SCROLL_FACTOR || x >= getMeasuredWidth() || x == 0) {
isAutoScrolling = false;
}
}
}
}

Scrolling parallel views simultaneously

I have two ScrollView's side by side and by using the code below I can scroll them simultaneously but I still can scroll them each independently throwing off the scroll positions. How can I make each view scroll simultaneously and disable scrolling each view by itself? I apologize if there's any confusion in my question. Any help would be appreciated, thanks.
ScrollView sv1;
ScrollView sv2;
View clickSource;
View touchSource;
sv1.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if(touchSource == null)
touchSource = v;
if(v == touchSource) {
sv2.dispatchTouchEvent(event);
if(event.getAction() == MotionEvent.ACTION_UP) {
clickSource = v;
touchSource = null;
}
}
return false;
}
});
sv2.setOnTouchListener(new View.OnTouchListener(){
#Override
public boolean onTouch(View v, MotionEvent event) {
if(touchSource == null)
touchSource = v;
if(v == touchSource) {
sv1.dispatchTouchEvent(event);
if(event.getAction() == MotionEvent.ACTION_UP) {
clickSource = v;
touchSource = null;
}
}
return false;
}
});
Hopefully I understand your question correctly. If you want both ScrollViews to scroll simultaneously then the code below should do the trick (untested):
First create an interface to listen to scroll events:
public interface ScrollChangeListener {
public void onScrollChanged(View view, int x, int y, int oldx, int oldy);
}
Next, create a custom view so you can listen for scroll changes:
public class ObservableScrollView extends ScrollView {
private ScrollChangeListener mScrollChangeListener;
public ObservableScrollView(Context context) {
super(context);
}
public ObservableScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ObservableScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ObservableScrollView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setScrollChangeListener(ScrollChangeListener listener) {
mScrollChangeListener = listener;
}
#Override
protected void onScrollChanged(int x, int y, int oldx, int oldy) {
if (mScrollChangeListener != null) {
mScrollChangeListener.onScrollChanged(this, x, y, oldx, oldy);
}
}
}
Use your custom view and create a listener for both ScrollViews.
ObservableScrollView mScrollView1;
ObservableScrollView mScrollView2;
...
ScrollChangeListener listener = new ScrollChangeListener() {
#Override
public void onScrollChanged(View view, int x, int y, int oldx, int oldy) {
ScrollView scrollView;
if (view == mScrollView1) {
scrollView = mScrollView2;
} else if (view == mScrollView2) {
scrollView = mScrollView1;
} else {
return;
}
scrollView.scrollTo(x, y);
}
};
...
mScrollView1.setScrollChangeListener(listener);
mScrollView2.setScrollChangeListener(listener);
Try this
sv1.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
int scrollX = sv1.getScrollX(); // for horizontalScrollView
int scrollY = sv1.getScrollY(); // for verticalScrollView
// DO SOMETHING WITH THE SCROLL COORDINATES
sv2.scrollTo(scrollX, scrollY);
}
});

New way to detect if WebView has scrolled to the bottom - getScale depreciated

After doing a lot of research on stackoverflow and looking for answers I found that I needed to create a subclass of WebView and then do an override on OnScrollChanged etc.. I have the following code...
SearchResultsWebView.setOnScrollChangedCallback(
new Sub_WebView_Results.OnScrollChangedCallback() {
#Override
public void onScroll(int l, int t) {
int tek = (int) Math.floor(SearchResultsWebView.getContentHeight() * SearchResultsWebView.getScale());
if (tek - SearchResultsWebView.getScrollY() == SearchResultsWebView.getHeight())
Toast.makeText(getActivity(), "End", Toast.LENGTH_SHORT).show();
}
});
HOWEVER the problem is that .getScale has been depreciated. I haven't found another way that works.
I tried using ..
SearchResultsWebView.setWebViewClient(new WebViewClient() {
#Override
public void onScaleChanged(WebView view, float oldScale, float newScale) {
super.onScaleChanged(view, oldScale, newScale);
currentScale = newScale;
}
});
And then just passing the [currentScale] but it seems this never gets called so I'm at a loss on how to do this.
Content height for web view is returned dp and thus we need to multiply with the devices density multiplier to get the actual height of the content
with the actual height i deduct the webview's height to calculate when the webview will be visible and compare with scroll y every time user picks up their finger.
This is what i did, working perfectly for me.
webView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
if (Math.floor((webView.getContentHeight() * getResources().getDisplayMetrics().density) - webView.getHeight()) == webView.getScrollY()) {
// Bottom Reached , it is necessary to calculate content height because
// it changes showAgreeButton();
return true;
}
return false;
}
});
}
In order to tell if the user has scrolled to the bottom of a web view, I extended the web view and had an interface callback when the user has got to the bottom of the view onScrollChanged. Here is the code:
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.webkit.WebView;
public class EULAWebView extends WebView {
//declare needed constants
private final String TAG = getClass().getSimpleName();
//declare needed variables
private EULAWebInterface eulaWebInteface;
private int paddingOffset = 200;
private boolean bottomReached;
public EULAWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public EULAWebView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public EULAWebView(Context context) {
super(context);
}
public void setEULAScrollListener(Context context) {
try {
eulaWebInteface = (EULAWebInterface)context;
} catch (ClassCastException ex) {
Log.e(TAG, "UNABLE TO CAST CONTEXT TO EULAWebInterface");
ex.printStackTrace();
throw new ClassCastException();
}
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
if(this.computeVerticalScrollRange() <= (this.computeVerticalScrollOffset() +
this.computeVerticalScrollExtent() + this.paddingOffset)) {
if(!bottomReached) {
bottomReached = true;
if(eulaWebInteface != null)
eulaWebInteface.atBottomOfScrollView(true);
}
} else {
if(bottomReached) {
bottomReached = false;
if(eulaWebInteface != null)
eulaWebInteface.atBottomOfScrollView(false);
}
}
super.onScrollChanged(l, t, oldl, oldt);
}
}
Here is the interface that is used to let the activity know that the bottom of the scroll view has changed:
public interface EULAWebInterface {
void atBottomOfScrollView(boolean atBottom);
}
And here is the interface implementation in the activity:
#Override
public void atBottomOfScrollView(boolean atBottom) {
findViewById(R.id.eula_action_layout).setVisibility(atBottom ? View.VISIBLE : View.GONE);
findViewById(R.id.eula_instruction_textview).setVisibility(atBottom ? View.GONE : View.VISIBLE);
}
Apparently I found the answer from: How can i get the current scale of a webView(android)
Instead of WebView.getScale()
You can use: getResources().getDisplayMetrics().density
Try this:
#Override
protected void onScrollChanged(int x, int y, int oldx, int oldy) {
View view = (View) getChildAt(getChildCount()-1);
int diff = (view.getBottom()-(getHeight()+getScrollY()));// Calculate the difference in scrolling
if( diff == 0 ){ // The bottom has been reached if the difference is 0
Log.d(ScrollTest.LOG_TAG, "WebView: Bottom has been reached" );
// DO SOMETHING HERE WHEN THE WEBVIEW HAS REACHED THE BOTTOM!
}
super.onScrollChanged(x, y, oldx, oldy);
}
Btw, why use the scaling method when the above method may work better (it's easier to implement I think)

Detect Scroll Up & Scroll down in ListView

I have the following requirement:
At first, data for page no: 2 is fetched from the server & the items
are populated in a ListView.
Considering that both the prev page & next page are available in a scenario, the following code has been added:
if(prevPageNo > 0){
mListViewActual.setOnScrollListener(this);
}
if(nextPageNo > 0){
mListViewActual.setOnScrollListener(this);
}
What conditions should I put to detect scroll up & scroll down on the following methods:
void onScroll(AbsListView view, int firstVisibleItem, int
visibleItemCount, int totalItemCount)
void onScrollStateChanged(AbsListView view, int scrollState)
After the action: scroll up & scroll down is detected , accordingly a service will be called with either the prev page no or next page no , to fetch the items to be populated in the Listview.
Any inputs will be helpful.
Gone through the following links but its not returning the correct scroll up / scroll down action:
link 1
link 2
try using the setOnScrollListener and implement the onScrollStateChanged with scrollState
setOnScrollListener(new OnScrollListener(){
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// TODO Auto-generated method stub
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
final ListView lw = getListView();
if(scrollState == 0)
Log.i("a", "scrolling stopped...");
if (view.getId() == lw.getId()) {
final int currentFirstVisibleItem = lw.getFirstVisiblePosition();
if (currentFirstVisibleItem > mLastFirstVisibleItem) {
mIsScrollingUp = false;
Log.i("a", "scrolling down...");
} else if (currentFirstVisibleItem < mLastFirstVisibleItem) {
mIsScrollingUp = true;
Log.i("a", "scrolling up...");
}
mLastFirstVisibleItem = currentFirstVisibleItem;
}
}
});
Here is a working modified version from some of the above-indicated solutions.
Add another class ListView:
package com.example.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AbsListView;
public class ListView extends android.widget.ListView {
private OnScrollListener onScrollListener;
private OnDetectScrollListener onDetectScrollListener;
public ListView(Context context) {
super(context);
onCreate(context, null, null);
}
public ListView(Context context, AttributeSet attrs) {
super(context, attrs);
onCreate(context, attrs, null);
}
public ListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
onCreate(context, attrs, defStyle);
}
#SuppressWarnings("UnusedParameters")
private void onCreate(Context context, AttributeSet attrs, Integer defStyle) {
setListeners();
}
private void setListeners() {
super.setOnScrollListener(new OnScrollListener() {
private int oldTop;
private int oldFirstVisibleItem;
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (onScrollListener != null) {
onScrollListener.onScrollStateChanged(view, scrollState);
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (onScrollListener != null) {
onScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
if (onDetectScrollListener != null) {
onDetectedListScroll(view, firstVisibleItem);
}
}
private void onDetectedListScroll(AbsListView absListView, int firstVisibleItem) {
View view = absListView.getChildAt(0);
int top = (view == null) ? 0 : view.getTop();
if (firstVisibleItem == oldFirstVisibleItem) {
if (top > oldTop) {
onDetectScrollListener.onUpScrolling();
} else if (top < oldTop) {
onDetectScrollListener.onDownScrolling();
}
} else {
if (firstVisibleItem < oldFirstVisibleItem) {
onDetectScrollListener.onUpScrolling();
} else {
onDetectScrollListener.onDownScrolling();
}
}
oldTop = top;
oldFirstVisibleItem = firstVisibleItem;
}
});
}
#Override
public void setOnScrollListener(OnScrollListener onScrollListener) {
this.onScrollListener = onScrollListener;
}
public void setOnDetectScrollListener(OnDetectScrollListener onDetectScrollListener) {
this.onDetectScrollListener = onDetectScrollListener;
}
}
And an interface:
public interface OnDetectScrollListener {
void onUpScrolling();
void onDownScrolling();
}
And finally how to use:
com.example.view.ListView listView = (com.example.view.ListView) findViewById(R.id.list);
listView.setOnDetectScrollListener(new OnDetectScrollListener() {
#Override
public void onUpScrolling() {
/* do something */
}
#Override
public void onDownScrolling() {
/* do something */
}
});
In your XML layout:
<com.example.view.ListView
android:id="#+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
This is my first topic, do not judge me harshly. =)
this is a simple implementation:
lv.setOnScrollListener(new OnScrollListener() {
private int mLastFirstVisibleItem;
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if(mLastFirstVisibleItem<firstVisibleItem)
{
Log.i("SCROLLING DOWN","TRUE");
}
if(mLastFirstVisibleItem>firstVisibleItem)
{
Log.i("SCROLLING UP","TRUE");
}
mLastFirstVisibleItem=firstVisibleItem;
}
});
and if you need more precision, you can use this custom ListView class:
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ListView;
/**
* Created by root on 26/05/15.
*/
public class ScrollInterfacedListView extends ListView {
private OnScrollListener onScrollListener;
private OnDetectScrollListener onDetectScrollListener;
public ScrollInterfacedListView(Context context) {
super(context);
onCreate(context, null, null);
}
public ScrollInterfacedListView(Context context, AttributeSet attrs) {
super(context, attrs);
onCreate(context, attrs, null);
}
public ScrollInterfacedListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
onCreate(context, attrs, defStyle);
}
#SuppressWarnings("UnusedParameters")
private void onCreate(Context context, AttributeSet attrs, Integer defStyle) {
setListeners();
}
private void setListeners() {
super.setOnScrollListener(new OnScrollListener() {
private int oldTop;
private int oldFirstVisibleItem;
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (onScrollListener != null) {
onScrollListener.onScrollStateChanged(view, scrollState);
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (onScrollListener != null) {
onScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
}
if (onDetectScrollListener != null) {
onDetectedListScroll(view, firstVisibleItem);
}
}
private void onDetectedListScroll(AbsListView absListView, int firstVisibleItem) {
View view = absListView.getChildAt(0);
int top = (view == null) ? 0 : view.getTop();
if (firstVisibleItem == oldFirstVisibleItem) {
if (top > oldTop) {
onDetectScrollListener.onUpScrolling();
} else if (top < oldTop) {
onDetectScrollListener.onDownScrolling();
}
} else {
if (firstVisibleItem < oldFirstVisibleItem) {
onDetectScrollListener.onUpScrolling();
} else {
onDetectScrollListener.onDownScrolling();
}
}
oldTop = top;
oldFirstVisibleItem = firstVisibleItem;
}
});
}
#Override
public void setOnScrollListener(OnScrollListener onScrollListener) {
this.onScrollListener = onScrollListener;
}
public void setOnDetectScrollListener(OnDetectScrollListener onDetectScrollListener) {
this.onDetectScrollListener = onDetectScrollListener;
}
public interface OnDetectScrollListener {
void onUpScrolling();
void onDownScrolling();
}
}
an example for use:
(don't forget to add it as an Xml Tag in your layout.xml)
scrollInterfacedListView.setOnDetectScrollListener(new ScrollInterfacedListView.OnDetectScrollListener() {
#Override
public void onUpScrolling() {
//Do your thing
}
#Override
public void onDownScrolling() {
//Do your thing
}
});
With all the method posted, there are problems recognizing when the user is scrolling up from the first element or down from the last.
Here is another approach to detect scroll up/down:
listView.setOnTouchListener(new View.OnTouchListener() {
float height;
#Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
float height = event.getY();
if(action == MotionEvent.ACTION_DOWN){
this.height = height;
}else if(action == MotionEvent.ACTION_UP){
if(this.height < height){
Log.v(TAG, "Scrolled up");
}else if(this.height > height){
Log.v(TAG, "Scrolled down");
}
}
return false;
}
});
ListView listView = getListView();
listView.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
view.setOnTouchListener(new OnTouchListener() {
private float mInitialX;
private float mInitialY;
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mInitialX = event.getX();
mInitialY = event.getY();
return true;
case MotionEvent.ACTION_MOVE:
final float x = event.getX();
final float y = event.getY();
final float yDiff = y - mInitialY;
if (yDiff > 0.0) {
Log.d(tag, "SCROLL DOWN");
scrollDown = true;
break;
} else if (yDiff < 0.0) {
Log.d(tag, "SCROLL up");
scrollDown = true;
break;
}
break;
}
return false;
}
});
My solution works perfectly giving the exact value for each scroll direction.
distanceFromFirstCellToTop contains the exact distance from the first cell to the top of the parent View. I save this value in previousDistanceFromFirstCellToTop and as I scroll I compare it with the new value. If it's lower then I scrolled up, else, I scrolled down.
private int previousDistanceFromFirstCellToTop;
listview.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
View firstCell = listview.getChildAt(0);
int distanceFromFirstCellToTop = listview.getFirstVisiblePosition() * firstCell.getHeight() - firstCell.getTop();
if(distanceFromFirstCellToTop < previousDistanceFromFirstCellToTop)
{
//Scroll Up
}
else if(distanceFromFirstCellToTop > previousDistanceFromFirstCellToTop)
{
//Scroll Down
}
previousDistanceFromFirstCellToTop = distanceFromFirstCellToTop;
}
});
For Xamarin developers, the solution is the following:
Note: don't forget to run on UI thread
listView.Scroll += (o, e) =>
{
View firstCell = listView.GetChildAt(0);
int distanceFromFirstCellToTop = listView.FirstVisiblePosition * firstCell.Height - firstCell.Top;
if (distanceFromFirstCellToTop < previousDistanceFromFirstCellToTop)
{
//Scroll Up
}
else if (distanceFromFirstCellToTop > previousDistanceFromFirstCellToTop)
{
//Scroll Down
}
previousDistanceFromFirstCellToTop = distanceFromFirstCellToTop;
};
Just set scroll listener to your listview.
If you have a header or footer you should check the visible count too. If it increases it means you are scrolling down. (Reverse it if there is a footer instead of header)
If you don't have any header or footer in your listview you can remove the lines which cheks the visible item count.
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mLastFirstVisibleItem > firstVisibleItem) {
Log.e(getClass().toString(), "scrolling up");
} else if (mLastFirstVisibleItem < firstVisibleItem) {
Log.e(getClass().toString(), "scrolling down");
} else if (mLastVisibleItemCount < visibleItemCount) {
Log.e(getClass().toString(), "scrolling down");
} else if (mLastVisibleItemCount > visibleItemCount) {
Log.e(getClass().toString(), "scrolling up");
}
mLastFirstVisibleItem = firstVisibleItem;
mLastVisibleItemCount = visibleItemCount;
}
public void onScrollStateChanged(AbsListView listView, int scrollState) {
}
});
and have this variables
int mLastFirstVisibleItem;
int mLastVisibleItemCount;
I've used this much simpler solution:
setOnScrollListener( new OnScrollListener()
{
private int mInitialScroll = 0;
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount)
{
int scrolledOffset = computeVerticalScrollOffset();
boolean scrollUp = scrolledOffset > mInitialScroll;
mInitialScroll = scrolledOffset;
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
}
To also detect scrolling with larger elements, I prefere an onTouch Listener:
listview.setOnTouchListener(new View.OnTouchListener() {
int scrollEventListSize = 5;
float lastY;
// Used to correct for occasions when user scrolls down(/up) but the onTouchListener detects it incorrectly. We will store detected up-/down-scrolls with -1/1 in this list and evaluate later which occured more often
List<Integer> downScrolledEventsHappened;
#Override
public boolean onTouch(View v, MotionEvent event) {
float diff = 0;
if(event.getAction() == event.ACTION_DOWN){
lastY = event.getY();
downScrolledEventsHappened = new LinkedList<Integer>();
}
else if(event.getAction() == event.ACTION_MOVE){
diff = event.getY() - lastY;
lastY = event.getY();
if(diff>0)
downScrolledEventsHappened.add(1);
else
downScrolledEventsHappened.add(-1);
//List needs to be filled with some events, will happen very quickly
if(downScrolledEventsHappened.size() == scrollEventListSize+1){
downScrolledEventsHappened.remove(0);
int res=0;
for(int i=0; i<downScrolledEventsHappened.size(); i++){
res+=downScrolledEventsHappened.get(i);
}
if (res > 0)
Log.i("INFO", "Scrolled up");
else
Log.i("INFO", "Scrolled down");
}
}
return false; // don't interrupt the event-chain
}
});
Store the firstVisibleItem and on the next onScroll check if the new firstVisibleItem is smaller or greater than the previous one.
Example pseudocode (not tested):
int lastVisibleItem = 0;
boolean isScrollingDown = false;
void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem > lastVisibleItem) {
isScrollingDown = true;
}
else {
isScrollingDown = false;
}
lastVisibleItem = firstVisibleItem;
}
For some reason the Android doc doesnt cover this, and the method used isnt even in the docs... took me a while to find it.
To detect if your scroll is at the top you would use this.
public boolean checkAtTop()
{
if(listView.getChildCount() == 0) return true;
return listView.getChildAt(0).getTop() == 0;
}
This will check if your scroller is at the top. Now, in order to do it for the bottom, you would have to pass it the number of children that you have, and check against that number. You might have to figure out how many are on the screen at one time, and subtract that from your number of children. I've never had to do that. Hope this helps
Those methods cannot be used to detect scrolling directions directly. There are many ways of getting the direction. A simple code(untested) for one such method is explained below :
public class ScrollTrackingListView extends ListView {
private boolean readyForMeasurement = false;
private Boolean isScrollable = null;
private float prevDistanceToEnd = -1.0;
private ScrollDirectionListener listener = null;
public ScrollTrackingListView(Context context) {
super(context);
init();
}
public ScrollTrackingListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ScrollTrackingListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
ViewTreeObserver observer = getViewTreeObserver();
observer.addOnGlobalLayoutListener(globalLayoutListener);
setOnScrollListener(scrollListener);
}
private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener
= new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
readyForMeasurement = true;
calculateDistanceToEnd();
}
};
public void registerScrollDirectionListener(ScrollDirectionListener listener) {
scrollDirectionListener = listener;
}
public void unregisterScrollDirectionListener() {
scrollDirectionListener = null;
}
private OnScrollListener scrollListener
= new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView absListView, int i) {
calculateDistanceToEnd();
}
#Override
public void onScroll(AbsListView absListView, int i, int i1, int i2) {
// Do nothing
}
};
private void calculateDistanceToEnd() {
if (readyForMeasurement) {
// I'm using the height of the layout, horizontal scrollbar and
// content along with scroll down offset
// computeVerticalScrollExtent is used to compute the length of the thumb within the scrollbar's track.
// The length of the thumb is a function of the view height and the content length.
int verticalScrollExtent = computeVerticalScrollExtent();
int verticalScrollOffset = computeVerticalScrollOffset();
int verticalScrollRange = computeVerticalScrollRange();
int horizontalScrollBarHeight = getHorizontalScrollbarHeight();
/**
* 1. Let "R" represent the range of the vertical scrollbar. This corresponds to the length of the content
* in the view.
* 2. Let "E" represent the extent of the vertical scrollbar. The extent is a constant value and is
* (probably) equal to a value proportional to the height of the view.
* 3. Offset "o" represents the current position in the range that is visible to the user. It can take
* values from "0 to E".
*
* Now the DistanceToEnd is calculated using these three values as follows :
*
* DistanceToEnd = (R - o) / E
*
* DistanceToEnd will hold the value in NumberOfScreenToEnd units.
*
*/
float distanceToEnd =
((float)(verticalScrollRange - verticalScrollOffset))/((float)(verticalScrollExtent));
if(prevDistanceToEnd == -1) {
prevDistanceToEnd = distanceToEnd;
} else {
if(listener != null) {
if(distanceToEnd > prevDistanceToEnd) {
// User is scrolling up
listener.onScrollingUp();
} else {
// User is scrolling up
listener.onScrollingDown();
}
}
prevDistanceToEnd = distanceToEnd;
}
if(isScrollable == null) {
// Check if the view height is less than a screen (i.e., no scrolling is enabled)
if((horizontalScrollBarHeight + verticalScrollExtent) >= verticalScrollRange) {
isScrollable = Boolean.FALSE;
} else {
isScrollable = Boolean.TRUE;
}
}
}
}
public interface ScrollDirectionListener {
public void onScrollingUp();
public void onScrollingDown();
}
}
The idea is to calculate the distanceToEnd. If distanceToEnd increases, the user is scrolling up and if it decreases, the user is scrolling down. That will also give you the exact distance to the end of the list.
If you are just trying to know whether the user is scrolling up or down you can override the onInterceptTouchEvent to detect the scrolling direction like below :
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mInitialX = event.getX();
mInitialY = event.getY();
return true;
case MotionEvent.ACTION_MOVE:
final float x = event.getX();
final float y = event.getY();
final float yDiff = y - mInitialY; // yDiff less than 0.0 implies scrolling down while yDiff greater than 0.0 implies scrolling up. If I try to add the less than or greater than symbols, the preview refuses to display it.
if(yDiff less than 0.0) listener.onScrollingDown();
else if(yDiff greater than 0.0) listener.onScrollingUp();
break;
}
return super.onInterceptTouchEvent(event);
}
Trick about detect scroll up or down in listview, you just call this function on onScroll function in OnScrollListener of ListView.
private int oldFirstVisibleItem = -1;
private protected int oldTop = -1;
// you can change this value (pixel)
private static final int MAX_SCROLL_DIFF = 5;
private void calculateListScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem == oldFirstVisibleItem) {
int top = view.getChildAt(0).getTop();
// range between new top and old top must greater than MAX_SCROLL_DIFF
if (top > oldTop && Math.abs(top - oldTop) > MAX_SCROLL_DIFF) {
// scroll up
} else if (top < oldTop && Math.abs(top - oldTop) > MAX_SCROLL_DIFF) {
// scroll down
}
oldTop = top;
} else {
View child = view.getChildAt(0);
if (child != null) {
oldFirstVisibleItem = firstVisibleItem;
oldTop = child.getTop();
}
}
}
Simple way to detect scroll up/down on android listview
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount){
if(prevVisibleItem != firstVisibleItem){
if(prevVisibleItem < firstVisibleItem)
//ScrollDown
else
//ScrollUp
prevVisibleItem = firstVisibleItem;
}
dont forget
yourListView.setOnScrollListener(yourScrollListener);
Simple way to load more items on scroll up/down event in android GridView
grid.setOnScrollListener(new AbsListView.OnScrollListener() {
private int mLastFirstVisibleItem;
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
Log.d("state",String.valueOf(scrollState));
if(scrollState == 0)
Log.i("a", "scrolling stopped...");
if (view.getId() == grid.getId()) {
final int currentFirstVisibleItem = grid.getLastVisiblePosition();
mLastFirstVisibleItem = grid.getFirstVisiblePosition();
if (currentFirstVisibleItem > mLastFirstVisibleItem) {
mIsScrollingUp = false;
if(!next.contains("null")){
//Call api to get products from server
}
Log.i("a", "scrolling down...");
} else if (currentFirstVisibleItem < mLastFirstVisibleItem) {
mIsScrollingUp = true;
Log.i("a", "scrolling up...");
}
mLastFirstVisibleItem = currentFirstVisibleItem;
}
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
Log.d("on scroll","");
}
});
Here's what I would try first:
1) Create an interface (let's call it OnScrollTopOrBottomListener) with these methods:
void onScrollTop();
void onScrollBottom();
2) In your list's adapter, add a member instance, typed as the interface you created and supply a setter and getter.
3) In the getView() implementation of your adapter, check if the position parameter is either 0 or getCount() - 1. Also check that your OnScrollTopOrBottomListener instance is not null.
4) If the position is 0, call onScrollTopOrBottomListener.onScrollTop(). If position is getCount() - 1, call onScrollTopOrBottomListener.onScrollBottom().
5) In your OnScrollTopOrBottomListener implementation, call the appropriate methods to get the desired data.
Hope that helps in some way.
-Brandon
I have encountered problems using some example where the cell size of ListView is great. So I have found a solution to my problem which detects the slightest movement of your finger . I've simplified to the minimum possible and is as follows:
private int oldScrolly;
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
View view = absListView.getChildAt(0);
int scrolly = (view == null) ? 0 : -view.getTop() + absListView.getFirstVisiblePosition() * view.getHeight();
int margin = 10;
Log.e(TAG, "Scroll y: " + scrolly + " - Item: " + firstVisibleItem);
if (scrolly > oldScrolly + margin) {
Log.d(TAG, "SCROLL_UP");
oldScrolly = scrolly;
} else if (scrolly < oldScrolly - margin) {
Log.d(TAG, "SCROLL_DOWN");
oldScrolly = scrolly;
}
}
});
PD: I use the MARGIN to not detect the scroll until you meet that margin . This avoids problems when I show or hide views and avoid blinking of them.

How can I detect if an Android MapView has been panned or zoomed?

I'm creating an Android app that searches for items based on the visible area of the MapView. Is there a way to set up a listener on my MapView to detect when a map has been panned or zoomed?
try mapview-overlay-manager, it is a extension for overlayer for android maps,
it has some simplified OnGestureListener, few example:
onSingleTap(MotionEvent, ManagedOverlay, GeoPoint, OverlayItem)
onDoubleTap(MotionEvent, ManagedOverlay, GeoPoint, OverlayItem)
onLongPress(MotionEvent, ManagedOverlay, GeoPoint, OverlayItem)
onZoom(ZoomEvent, ManagedOverlay)
onScrolled(...)
link:http://code.google.com/p/mapview-overlay-manager/ hope it helps
You can create a SimpleMapView that extends MapView.
public class SimpleMapView extends MapView {
private int currentZoomLevel = -1;
private GeoPoint currentCenter;
private List<ZoomChangeListener> zoomEvents = new ArrayList<ZoomChangeListener>();
private List<PanChangeListener> panEvents = new ArrayList<PanChangeListener>();
public SimpleMapView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public SimpleMapView(Context context, String apiKey) {
super(context, apiKey);
}
public SimpleMapView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
*
* #return
*/
public int[][] getBounds() {
GeoPoint center = getMapCenter();
int latitudeSpan = getLatitudeSpan();
int longtitudeSpan = getLongitudeSpan();
int[][] bounds = new int[2][2];
bounds[0][0] = center.getLatitudeE6() - (latitudeSpan / 2);
bounds[0][1] = center.getLongitudeE6() - (longtitudeSpan / 2);
bounds[1][0] = center.getLatitudeE6() + (latitudeSpan / 2);
bounds[1][1] = center.getLongitudeE6() + (longtitudeSpan / 2);
return bounds;
}
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
GeoPoint centerGeoPoint = this.getMapCenter();
if (currentCenter == null ||
(currentCenter.getLatitudeE6() != centerGeoPoint.getLatitudeE6()) ||
(currentCenter.getLongitudeE6() != centerGeoPoint.getLongitudeE6()) ) {
firePanEvent(currentCenter, this.getMapCenter());
}
currentCenter = this.getMapCenter();
}
return super.onTouchEvent(ev);
}
#Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if(getZoomLevel() != currentZoomLevel){
fireZoomLevel(currentZoomLevel, getZoomLevel());
currentZoomLevel = getZoomLevel();
}
}
#Override
public void setSatellite(boolean on){
super.setSatellite(on);
}
#Override
public MapController getController(){
return super.getController();
}
private void fireZoomLevel(int old, int current){
for(ZoomChangeListener event : zoomEvents){
event.onZoom(old, current);
}
}
private void firePanEvent(GeoPoint old, GeoPoint current){
for(PanChangeListener event : panEvents){
event.onPan(old, current);
}
}
public void addZoomChangeListener(ZoomChangeListener listener){
this.zoomEvents.add(listener);
}
public void addPanChangeListener(PanChangeListener listener){
this.panEvents.add(listener);
}
}
You have the Listeners you can put the code for pan or zoom.
Then in your xml:
<com.androidnatic.maps.SimpleMapView android:clickable="true"
android:layout_height="match_parent" android:id="#+id/mapView"
android:layout_width="match_parent"
android:apiKey="xxx">
</com.androidnatic.maps.SimpleMapView>
And then in your code you can specify the Pan Listener:
mapView.addPanChangeListener(new PanChangeListener() {
#Override
public void onPan(GeoPoint old, GeoPoint current) {
//TODO
}
});
Sadly, there is no built-in functionality to do this in the MapView tools (a strange oversight since this functionality is in the JavaScript SDK, as well as the iOS SDK).
You can deal with this easily enough though by using a Runnable and just polling the MapView. I do this by keeping track of the "last" state of:
getLatitudeSpan();
getLongitudeSpan();
getCenter();
getZoomLevel();
And then comparing them to the current values. If the values of changed, you know the map view has moved. If not, you can do nothing.
Either way, reschedule the runnable for another run after 500ms or so and repeat the process. You can use onResume() and onPause() to remove the callback for the Runnable and restart it as necessary.
The MapView class can track changes using the onLayout method.
i.e.
class CustomMapView extends MapView {
protected void onLayout(boolean changed,int left, int right, int top, int bottom){
super.onLayout(changed,left, right, top, bottom);
if(changed){
// do something special
}
}
}
The only way that I can think of is to extends the MapView and override the OnTouchEvent and watch for the Up action. This will tell you that the user has finished moving and you can get the lat/lon span to determine the region you should check out.
Old question, but I wrote a class a while back called ExMapView which fires an event when the map region starts changing (onRegionBeginChange) and when it stops changing (onRegionEndChange). This class is for use with the old MapView, not V2.0. Hope it helps someone.
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
public class ExMapView extends MapView {
private static final String TAG = ExMapView.class.getSimpleName();
private static final int DURATION_DEFAULT = 700;
private OnRegionChangedListener onRegionChangedListener;
private GeoPoint previousMapCenter;
private int previousZoomLevel;
private int changeDuration; // This is the duration between when the user stops moving the map around and when the onRegionEndChange event fires.
private boolean isTouched = false;
private boolean regionChanging = false;
private Runnable onRegionEndChangeTask = new Runnable() {
public void run() {
regionChanging = false;
previousMapCenter = getMapCenter();
previousZoomLevel = getZoomLevel();
if (onRegionChangedListener != null) {
onRegionChangedListener.onRegionEndChange(ExMapView.this, previousMapCenter, previousZoomLevel);
}
}
};
public ExMapView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ExMapView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public ExMapView(Context context, String apiKey) {
super(context, apiKey);
init();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
isTouched = event.getAction() != MotionEvent.ACTION_UP;
return super.onTouchEvent(event);
}
#Override
public void computeScroll() {
super.computeScroll();
// If the map region is still changing (user is still scrolling or zooming), reset timer for onRegionEndChange.
if ((!isTouched && !getMapCenter().equals(previousMapCenter)) || (previousZoomLevel != getZoomLevel())) {
// If the region has just begun changing, fire off onRegionBeginChange event.
if (!regionChanging) {
regionChanging = true;
if (onRegionChangedListener != null) {
onRegionChangedListener.onRegionBeginChange(this, previousMapCenter, previousZoomLevel);
}
}
// Reset timer for onRegionEndChange.
removeCallbacks(onRegionEndChangeTask);
postDelayed(onRegionEndChangeTask, changeDuration);
}
}
private void init() {
changeDuration = DURATION_DEFAULT;
previousMapCenter = getMapCenter();
previousZoomLevel = getZoomLevel();
}
public void setOnRegionChangedListener(OnRegionChangedListener listener) {
onRegionChangedListener = listener;
}
public void setChangeDuration(int duration) {
changeDuration = duration;
}
public interface OnRegionChangedListener {
public abstract void onRegionBeginChange(ExMapView exMapView, GeoPoint geoPoint, int zoomLevel);
public abstract void onRegionEndChange(ExMapView exMapView, GeoPoint geoPoint, int zoomLevel);
}
}

Categories

Resources