web-dev-qa-db-fra.com

Android: Détecter lorsque ScrollView cesse de défiler

J'utilise un ScrollView dans Android et où la partie visible du ScrollView a la même taille que l'une des cellules du Scrollview. Chaque "cellule" a la même hauteur. Ce que j'essaie de faire est donc de se mettre en place après le défilement du ScrollView.

Actuellement, je détecte quand l'utilisateur a touché le ScrollView et quand il a commencé à faire défiler et à travailler à partir de là, mais c'est tout à fait buggé. Il doit également fonctionner lorsque l'utilisateur le feuillette et le fait défiler, puis décélérer.

Sur l'iPhone, il y a une fonction similaire à didDecelerate et je peux créer le code que je souhaite lorsque le ScrollView a terminé de faire défiler l'écran. Y at-il une telle chose avec Android? Ou y a-t-il un code que je pourrais consulter pour trouver une meilleure façon de le faire?

J'ai regardé la Android docs et n'ai rien trouvé de tel.

82
user1053691

J'ai récemment dû implémenter la fonction que vous avez décrite. Ce que j'ai fait était d'avoir un Runnable vérifiant si le ScrollView avait cessé de défiler en comparant la valeur renvoyée par getScrollY () lorsque le onTouchEvent est déclenché pour la première fois avec la valeur renvoyée après un délai défini par la variable newCheck .

Voir le code ci-dessous (solution de travail):

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

}

Ensuite j'ai:

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

        }
});

BTW j'ai utilisé quelques idées d'autres réponses pour le faire dans mon application. J'espère que cela t'aides. Toutes les questions se sentent libres de demander. À votre santé.

93
tulio84z

Voici encore un autre correctif au bogue d'événement OnEndScroll manquant dans le IMHO dans ScrollView.

Son inspiré par hambonious answer. Il suffit de déposer cette classe dans votre projet (modifier le package pour qu’il corresponde au vôtre) et d’utiliser le code XML ci-dessous.

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

}

changer à nouveau le nom du paquet pour correspondre à votre projet

<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>
22
concept

J'ai sous-classé ScrollView (Horizontal) et fait quelque chose comme ceci:

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

J'ai utilisé une valeur de 1 pour SlowDownThreshold car il semble toujours être la différence du dernier événement onScrollChanged.

Pour que cela se comporte correctement lorsque vous glissez lentement, je devais le faire:

@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;
}
9
Ross Hambrick

Mon approche consiste à déterminer l'état de défilement par un horodatage modifié chaque fois que l'onScrollChanged () est appelé. Il est très facile de déterminer le début et la fin du défilement. Vous pouvez également modifier le seuil (j'utilise 100 ms) pour corriger la sensibilité.

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
    }

}
7
Lin Yu Cheng

Voici encore une autre solution, assez simple et propre à mon avis, naturellement inspirée des réponses ci-dessus. En gros, une fois que l'utilisateur a terminé son geste, vérifiez si getScrollY () est toujours en train de changer, après un bref délai (ici 50 ms).

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;
    }
}
5
Aleksandarf

Mon approche pour cette question consiste à utiliser une minuterie pour vérifier les 2 "événements" suivants.

1) onScrollChanged () a cessé d'appeler

2) Le doigt de l'utilisateur est levé depuis le 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);
    }
}
4
ZeroG

Voici ma solution qui inclut le suivi et la fin du défilement:

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

}
2
ZaBlanc

Ma solution est une variante de l'excellente solution de Lin Yu Cheng et détecte également le début et la fin du défilement.

Étape 1. Définissez un HorizontalScrollView et 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);

Étape 2. Ajoutez la classe CustomHorizontalScrollView:

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();
        }
    }
}
2
D. Clarke

Essayez de regarder cette question ici sur StackOverflow - ce n'est pas exactement la même chose que votre question, mais cela donne une idée de la façon dont vous pouvez gérer l'événement de défilement d'un ScrollView.

En gros, vous devez créer votre propre CustomScrollView en étendant ScrollView et en écrasant onScrollChanged(int x, int y, int oldx, int oldy). Ensuite, vous devez référencer cela dans votre fichier de mise en page au lieu de la norme ScrollView comme com.mypackage.CustomScrollView.

2
kaspermoerch

c'est un vieux fil, mais j'aimerais ajouter une solution plus courte que j'ai proposée:

 buttonsScrollView.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->

            handler.removeCallbacksAndMessages(null)

            handler.postDelayed({
                  //YOUR CODE TO BE EXECUTED HERE
                },1000)
        }

Naturellement, il y a un délai de 1000 millisecondes. Ajustez cela si vous en avez besoin.

2
pepinobr

Pour un cas simple comme celui que vous avez décrit, vous pouvez probablement vous en tirer en remplaçant la méthode fling dans votre vue de défilement personnalisée. La méthode Fling est appelée pour effectuer une "décélération" chaque fois que l'utilisateur lève son doigt de l'écran.

Donc, ce que vous devriez faire est quelque chose comme ceci:

  1. Sous-classe 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
                    }
                }
            };
            //...
        }
    }
    
  2. Méthode de sous-classe fling et NE PAS appeler la mise en œuvre de superclasse.

    @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.
    }
    
  3. Fling ne se déclenchera pas si l'utilisateur arrête de faire défiler le texte avant de lever son doigt (vélocitéY == 0). Si vous souhaitez également intercepter ce type d'événements, remplacez 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 Bien que cela fonctionne, le remplacement de la méthode fling peut être une mauvaise idée. C'est public, mais c'est à peine conçu pour le sous-classement. Actuellement, il fait 3 choses: il lance le lancement de mScroller privé, gère les modifications de focus possibles et affiche des barres de défilement. Cela pourrait changer à l'avenir Android version. Par exemple, l'instance privée de mScroller a changé sa classe de Scroller à OvershootScroller entre la version 2.3 et la version 3.0. Vous devez garder à l'esprit toutes ces petites différences. Dans tous les cas, être prêt pour des conséquences imprévues à l'avenir.

2
Alexey

Je pense que cela a été soulevé dans le passé. Autant que je sache, vous ne pouvez pas facilement le détecter. Ma suggestion est que vous jetiez un oeil à ScrollView.Java (c'est comme ça que nous faisons les choses en Android land :)) et voyez comment vous pouvez étendre la classe pour fournir les fonctionnalités que vous recherchez. C'est ce que j'essayerais d'abord:

 @Override
 protected void onScrollChanged(int l, int t, int oldl, int oldt) {
     if (mScroller.isFinished()) {
         // do something, for example call a listener
     }
 }
2
Felix

J'ai apporté quelques améliorations à la réponse de ZeroG. Il s’agit principalement d’annuler les appels de tâches en trop et d’implémenter le tout en tant que OnTouchListener privé, de sorte que tout le code de détection de défilement se trouve au même endroit.

Collez le code suivant dans votre propre implémentation ScrollView:

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

}

Et puis dans votre implémentation ScrollView:

setOnTouchListener( new ScrollFinishHandler() );
1
Vaiden

Il y a d'excellentes réponses ici, mais mon code peut détecter l'arrêt du défilement sans avoir à étendre la classe ScrollView. chaque instance de vue peut appeler getViewTreeObserver (). en maintenant cette instance de ViewTreeObserver, vous pouvez ajouter un OnScrollChangedListener à l'aide de la fonction addOnScrollChangedListener ().

déclarer ce qui suit:

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

et dans votre onCreate (ou un autre endroit), ajoutez:

    scrollListener = (ScrollView) findViewById(R.id.scroll);

    scrollListener.getViewTreeObserver().addOnScrollChangedListener(new OnScrollChangedListener() {

        @Override
        public void onScrollChanged() {
            milesec = System.currentTimeMillis();
            scrollStopDetector.postDelayed(scrollcalled, 200);
        }
    });

vous voudrez peut-être prendre plus de temps ou plus lentement entre ces vérifications, mais lors du défilement, ce listner est appelé très rapidement et fonctionne donc très vite.

1
Zbun