Android: comment vérifier si une vue à l'intérieur de ScrollView est visible?


168

J'ai un ScrollViewqui détient une série de Views. Je voudrais pouvoir déterminer si une vue est actuellement visible (si une partie de celle-ci est actuellement affichée par le ScrollView). Je m'attendrais à ce que le code ci-dessous fasse cela, étonnamment ce n'est pas le cas:

Rect bounds = new Rect();
view.getDrawingRect(bounds);

Rect scrollBounds = new Rect(scroll.getScrollX(), scroll.getScrollY(), 
        scroll.getScrollX() + scroll.getWidth(), scroll.getScrollY() + scroll.getHeight());

if(Rect.intersects(scrollBounds, bounds))
{
    //is  visible
}

Je suis curieux de savoir comment cela fonctionne. J'essaye de faire la même chose mais un ScrollView ne peut héberger qu'un enfant direct. Votre "série de vues" est-elle enveloppée dans une autre disposition à l'intérieur du ScrollView? C'est ainsi que les miens sont présentés, mais lorsque je fais cela, aucune des réponses données ici ne fonctionne pour moi.
Rooster242

1
Oui, ma série de vues est à l'intérieur d'un LinearLayout, qui est le 1 enfant de ScrollView. La réponse de Qberticus a fonctionné pour moi.
ab11

Réponses:


65

Utilisez View#getHitRectplutôt que View#getDrawingRectsur la vue que vous testez. Vous pouvez utiliser View#getDrawingRectsur au ScrollViewlieu de calculer explicitement.

Code de View#getDrawingRect:

 public void getDrawingRect(Rect outRect) {
        outRect.left = mScrollX;
        outRect.top = mScrollY;
        outRect.right = mScrollX + (mRight - mLeft);
        outRect.bottom = mScrollY + (mBottom - mTop);
 }

Code de View#getHitRect:

public void getHitRect(Rect outRect) {
        outRect.set(mLeft, mTop, mRight, mBottom);
}

35
Où dois-je appeler ces méthodes?
Tooto

3
@Qberticus Comment appeler les méthodes? Je l'utilise et il renvoie toujours false. S'il vous plaît laissez-moi savoir
KK_07k11A0585

2
Exactement où appeler ces méthodes?
zemaitis

193

Cela marche:

Rect scrollBounds = new Rect();
scrollView.getHitRect(scrollBounds);
if (imageView.getLocalVisibleRect(scrollBounds)) {
    // Any portion of the imageView, even a single pixel, is within the visible window
} else {
    // NONE of the imageView is within the visible window
}

1
Fonctionne parfaitement. Pour clarifier les choses: renvoie true si la vue est complètement ou partiellement visible; false signifie que la vue n'est pas complètement visible.
qwertzguy

1
[+1] J'ai utilisé ce code pour obtenir GridView/ ListView/ GridViewWithHeadertravailler avec SwipeRefreshLayout.
Kartik

Quelqu'un pourrait-il expliquer pourquoi cela fonctionne? getHitRectrenvoie un rect dans les coordonnées parent, mais getLocalVisibleRectrenvoie un rect dans les coordonnées locales de la vue de défilement, n'est-ce pas?
Épingler

3
Cela ne couvre pas les chevauchements, si la vue enfant est chevauchée par un autre élément enfant, elle retournera toujours true
Pradeep

1
Oui, nous avons besoin d'une instance de Rect, mais est-il nécessaire de getHitRect. Y a-t-il une différence si j'utilise un Rect (0,0-0,0) .Nous pouvons voir l'appel getLocalVisibleRect getGlobalVisibleRect.And Rect est défini ici r.set (0, 0, width, height);. @ BillMote
chefish

56

Si vous voulez détecter que la vue est ENTIÈREMENT visible:

private boolean isViewVisible(View view) {
    Rect scrollBounds = new Rect();
    mScrollView.getDrawingRect(scrollBounds);

    float top = view.getY();
    float bottom = top + view.getHeight();

    if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
        return true;
    } else {
        return false;
    }
}

6
C'est la bonne réponse =) Dans mon cas, j'ai changé le si comme ceci: scrollBounds.top <= top && scrollBounds.bottom => bottom
Helton Isac

2
+1 Helton si votre vue est poussée contre le haut ou le bas de votre vue de défilement, vous aurez besoin du <= ou> = respectivement
Joe Maher

Avez-vous vraiment testé cela? Il renvoie toujours false dans la mise en page la plus simple ScrollView et TextView en tant qu'enfant.
Farid

1
Quelle est la différence entre getHitRect () et getDrawingRect ()? S'il vous plaît guide
VVB

2
Ce code ne fonctionne que si la vue est ajoutée directement à la racine du conteneur ScrollView. Vérifiez la réponse de Phan Van Linh si vous souhaitez gérer une vue enfant dans une vue enfant, etc.
thijsonline

12

Ma solution est d'utiliser l' NestedScrollViewélément Scroll:

    final Rect scrollBounds = new Rect();
    scroller.getHitRect(scrollBounds);

    scroller.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
        @Override
        public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {

            if (myBtn1 != null) {

                if (myBtn1.getLocalVisibleRect(scrollBounds)) {
                    if (!myBtn1.getLocalVisibleRect(scrollBounds)
                            || scrollBounds.height() < myBtn1.getHeight()) {
                        Log.i(TAG, "BTN APPEAR PARCIALY");
                    } else {
                        Log.i(TAG, "BTN APPEAR FULLY!!!");
                    }
                } else {
                    Log.i(TAG, "No");
                }
            }

        }
    });
}

nécessite l'API 23+
SolidSnake

@SolidSnake, non, vous devez importer une classe différente, cela fonctionne bien
Parth Anjaria

10

Pour développer un peu la réponse de Bill Mote à l'aide de getLocalVisibleRect, vous voudrez peut-être vérifier si la vue n'est que partiellement visible:

Rect scrollBounds = new Rect();
scrollView.getHitRect(scrollBounds);
if (!imageView.getLocalVisibleRect(scrollBounds)
    || scrollBounds.height() < imageView.getHeight()) {
    // imageView is not within or only partially within the visible window
} else {
    // imageView is completely visible
}

6
Cela ne fonctionne pas .. même une vue partiellement visible est catégorisée comme complètement visible
azfar

10

Cette extension aide à détecter la vue entièrement visible.
Cela fonctionne également si vous Viewêtes un enfant de ... de ScrollView(par exemple: ScrollView-> LinearLayout-> ContraintLayout-> ... -> YourView).

fun ScrollView.isViewVisible(view: View): Boolean {
    val scrollBounds = Rect()
    this.getDrawingRect(scrollBounds)
    var top = 0f
    var temp = view
    while (temp !is ScrollView){
        top += (temp).y
        temp = temp.parent as View
    }
    val bottom = top + view.height
    return scrollBounds.top < top && scrollBounds.bottom > bottom
}

Remarque

1) view.getY()et view.getX()renvoyer la valeur x, y à FIRST PARENT .

2) Voici un exemple sur la façon dont getDrawingRectle lien retournera entrez la description de l'image ici


Je voulais une solution où la méthode devrait retourner false si la vue est cachée sous le clavier et cela fait le travail. Merci.
Rahul

8
public static int getVisiblePercent(View v) {
        if (v.isShown()) {
            Rect r = new Rect();
            v.getGlobalVisibleRect(r);
            double sVisible = r.width() * r.height();
            double sTotal = v.getWidth() * v.getHeight();
            return (int) (100 * sVisible / sTotal);
        } else {
            return -1;
        }
    }

2
C'est différent de ce que demandait ab11. isShown () vérifie uniquement l'indicateur de visibilité, pas si la vue est dans la zone visible de l'écran.
Romain Guy

4
@Romain Guy Le code ne couvre pas quand une vue est totalement défilée loin de l'écran. Il devrait être` public static int getVisiblePercent (View v) {if (v.isShown ()) {Rect r = new Rect (); booléen isVisible = v.getGlobalVisibleRect (r); if (isVisible) {double sVisible = r.width () * r.height (); double sTotal = v.getWidth () * v.getHeight (); return (int) (100 * sVisible / sTotal); } else {return -1; }} else {return -1; }} `
chefish

6

J'ai fait face au même problème aujourd'hui. En cherchant sur Google et en lisant la référence Android, j'ai trouvé cet article et une méthode que j'ai fini par utiliser à la place;

public final boolean getLocalVisibleRect (Rect r)

Bien de leur part non seulement de fournir Rect mais aussi de booléen indiquant si View est visible du tout. Du côté négatif, cette méthode n'est pas documentée :(


1
Cela vous indique uniquement si l'élément est défini sur la visibilité (vrai). Il ne vous dit pas si l'élément "visible" est réellement visible dans la fenêtre.
Bill Mote

Le code de getLocalVisibleRect ne prend pas en charge votre revendication: `public final boolean getLocalVisibleRect (Rect r) {final Point offset = mAttachInfo! = Null? mAttachInfo.mPoint: nouveau point (); if (getGlobalVisibleRect (r, offset)) {r.offset (-offset.x, -offset.y); // rend r local return true; } return false; } `
mbafford

6

Si vous voulez détecter si vous êtes Viewpleinement visible, essayez avec cette méthode:

private boolean isViewVisible(View view) {
    Rect scrollBounds = new Rect();
    mScrollView.getDrawingRect(scrollBounds);
    float top = view.getY();
    float bottom = top + view.getHeight();
    if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
        return true; //View is visible.
    } else {
        return false; //View is NOT visible.
    }
}

À proprement parler, vous pouvez obtenir la visibilité d'une vue avec:

if (myView.getVisibility() == View.VISIBLE) {
    //VISIBLE
} else {
    //INVISIBLE
}

Les valeurs constantes possibles de la visibilité dans une vue sont:

VISIBLE Cette vue est visible. À utiliser avec setVisibility (int) et android: visibilité.

INVISIBLE Cette vue est invisible, mais elle occupe toujours de l'espace à des fins de mise en page. À utiliser avec setVisibility (int) et android: visibilité.

DISPARU Cette vue est invisible et ne prend pas de place pour la mise en page. À utiliser avec setVisibility (int) et android: visibilité.


3
Battement lent. Ce que l'OP voulait savoir, c'est qu'en supposant que la visibilité de la vue soit View # VISIBLE, comment savoir si la vue elle-même est visible dans une vue défilante.
Joao Sousa

1
Je viens de vérifier un projet simple. La disposition a ScrollView et TextView comme enfant; renvoie toujours false même si TextView est entièrement visible.
Farid

Il renvoie toujours faux.
Rahul

3

Vous pouvez utiliser le FocusAwareScrollViewqui avertit lorsque la vue devient visible:

FocusAwareScrollView focusAwareScrollView = (FocusAwareScrollView) findViewById(R.id.focusAwareScrollView);
    if (focusAwareScrollView != null) {

        ArrayList<View> viewList = new ArrayList<>();
        viewList.add(yourView1);
        viewList.add(yourView2);

        focusAwareScrollView.registerViewSeenCallBack(viewList, new FocusAwareScrollView.OnViewSeenListener() {

            @Override
            public void onViewSeen(View v, int percentageScrolled) {

                if (v == yourView1) {

                    // user have seen view1

                } else if (v == yourView2) {

                    // user have seen view2
                }
            }
        });

    }

Voici la classe:

import android.content.Context;
import android.graphics.Rect;
import android.support.v4.widget.NestedScrollView;
import android.util.AttributeSet;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

public class FocusAwareScrollView extends NestedScrollView {

    private List<OnScrollViewListener> onScrollViewListeners = new ArrayList<>();

    public FocusAwareScrollView(Context context) {
        super(context);
    }

    public FocusAwareScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FocusAwareScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public interface OnScrollViewListener {
        void onScrollChanged(FocusAwareScrollView v, int l, int t, int oldl, int oldt);
    }

    public interface OnViewSeenListener {
        void onViewSeen(View v, int percentageScrolled);
    }

    public void addOnScrollListener(OnScrollViewListener l) {
        onScrollViewListeners.add(l);
    }

    public void removeOnScrollListener(OnScrollViewListener l) {
        onScrollViewListeners.remove(l);
    }

    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        for (int i = onScrollViewListeners.size() - 1; i >= 0; i--) {
            onScrollViewListeners.get(i).onScrollChanged(this, l, t, oldl, oldt);
        }
        super.onScrollChanged(l, t, oldl, oldt);
    }

    @Override
    public void requestChildFocus(View child, View focused) {
        super.requestChildFocus(child, focused);
    }

    private boolean handleViewSeenEvent(View view, int scrollBoundsBottom, int scrollYOffset,
                                        float minSeenPercentage, OnViewSeenListener onViewSeenListener) {
        int loc[] = new int[2];
        view.getLocationOnScreen(loc);
        int viewBottomPos = loc[1] - scrollYOffset + (int) (minSeenPercentage / 100 * view.getMeasuredHeight());
        if (viewBottomPos <= scrollBoundsBottom) {
            int scrollViewHeight = this.getChildAt(0).getHeight();
            int viewPosition = this.getScrollY() + view.getScrollY() + view.getHeight();
            int percentageSeen = (int) ((double) viewPosition / scrollViewHeight * 100);
            onViewSeenListener.onViewSeen(view, percentageSeen);
            return true;
        }
        return false;
    }

    public void registerViewSeenCallBack(final ArrayList<View> views, final OnViewSeenListener onViewSeenListener) {

        final boolean[] viewSeen = new boolean[views.size()];

        FocusAwareScrollView.this.postDelayed(new Runnable() {
            @Override
            public void run() {

                final Rect scrollBounds = new Rect();
                FocusAwareScrollView.this.getHitRect(scrollBounds);
                final int loc[] = new int[2];
                FocusAwareScrollView.this.getLocationOnScreen(loc);

                FocusAwareScrollView.this.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {

                    boolean allViewsSeen = true;

                    @Override
                    public void onScrollChange(NestedScrollView v, int x, int y, int oldx, int oldy) {

                        for (int index = 0; index < views.size(); index++) {

                            //Change this to adjust criteria
                            float viewSeenPercent = 1;

                            if (!viewSeen[index])
                                viewSeen[index] = handleViewSeenEvent(views.get(index), scrollBounds.bottom, loc[1], viewSeenPercent, onViewSeenListener);

                            if (!viewSeen[index])
                                allViewsSeen = false;
                        }

                        //Remove this if you want continuous callbacks
                        if (allViewsSeen)
                            FocusAwareScrollView.this.setOnScrollChangeListener((NestedScrollView.OnScrollChangeListener) null);
                    }
                });
            }
        }, 500);
    }
}

1

Façon Kotlin;

Une extension pour lister le défilement de la vue de défilement et obtenir une action si la vue enfant est visible à l'écran.

@SuppressLint("ClickableViewAccessibility")
fun View.setChildViewOnScreenListener(view: View, action: () -> Unit) {
    val visibleScreen = Rect()

    this.setOnTouchListener { _, motionEvent ->
        if (motionEvent.action == MotionEvent.ACTION_MOVE) {
            this.getDrawingRect(visibleScreen)

            if (view.getLocalVisibleRect(visibleScreen)) {
                action()
            }
        }

        false
    }
}

Utilisez cette fonction d'extension pour toute vue déroulante

nestedScrollView.setChildViewOnScreenListener(childView) {
               action()
            }

0

Je sais que c'est très tard. Mais j'ai une bonne solution. Vous trouverez ci-dessous l'extrait de code permettant d'obtenir le pourcentage de visibilité de la vue en mode défilement.

Tout d'abord, réglez l'écouteur tactile sur la vue de défilement pour obtenir un rappel pour l'arrêt du défilement.

@Override
public boolean onTouch(View v, MotionEvent event) {
    switch ( event.getAction( ) ) {
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    if(mScrollView == null){
                        mScrollView = (ScrollView) findViewById(R.id.mScrollView);
                    }
                    int childCount = scrollViewRootChild.getChildCount();

                    //Scroll view location on screen
                    int[] scrollViewLocation = {0,0};
                    mScrollView.getLocationOnScreen(scrollViewLocation);

                    //Scroll view height
                    int scrollViewHeight = mScrollView.getHeight();
                    for (int i = 0; i < childCount; i++){
                        View child = scrollViewRootChild.getChildAt(i);
                        if(child != null && child.getVisibility() == View.VISIBLE){
                            int[] viewLocation = new int[2];
                            child.getLocationOnScreen(viewLocation);
                            int viewHeight = child.getHeight();
                            getViewVisibilityOnScrollStopped(scrollViewLocation, scrollViewHeight,
                                    viewLocation, viewHeight, (String) child.getTag(), (childCount - (i+1)));
                        }
                    }
                }
            }, 150);
            break;
    }
    return false;
}

Dans l'extrait de code ci-dessus, nous recevons des rappels pour les événements tactiles de la vue de défilement et publions un exécutable après 150 millisecondes (non obligatoire) après avoir arrêté le rappel pour le défilement. Dans ce runnable, nous obtiendrons l'emplacement de la vue de défilement sur l'écran et la hauteur de la vue de défilement. Ensuite, obtenez l'instance de groupe de vues enfant direct de la vue de défilement et obtenez le nombre d'enfants. Dans mon cas, l'enfant direct de la vue de défilement est LinearLayout nommé scrollViewRootChild . Ensuite, itérez toutes les vues enfants de scrollViewRootChild . Dans l'extrait de code ci-dessus, vous pouvez voir que j'obtiens l'emplacement de l'enfant à l'écran dans un tableau d'entiers nommé viewLocation , obtenez la hauteur de vue dans le nom de la variable viewHeight . Ensuite, j'ai appelé une méthode privée getViewVisibilityOnScrollStopped. Vous pouvez comprendre le fonctionnement interne de cette méthode en lisant la documentation.

/**
 * getViewVisibilityOnScrollStopped
 * @param scrollViewLocation location of scroll view on screen
 * @param scrollViewHeight height of scroll view
 * @param viewLocation location of view on screen, you can use the method of view claas's getLocationOnScreen method.
 * @param viewHeight height of view
 * @param tag tag on view
 * @param childPending number of views pending for iteration.
 */
void getViewVisibilityOnScrollStopped(int[] scrollViewLocation, int scrollViewHeight, int[] viewLocation, int viewHeight, String tag, int childPending) {
    float visiblePercent = 0f;
    int viewBottom = viewHeight + viewLocation[1]; //Get the bottom of view.
    if(viewLocation[1] >= scrollViewLocation[1]) {  //if view's top is inside the scroll view.
        visiblePercent = 100;
        int scrollBottom = scrollViewHeight + scrollViewLocation[1];    //Get the bottom of scroll view 
        if (viewBottom >= scrollBottom) {   //If view's bottom is outside from scroll view
            int visiblePart = scrollBottom - viewLocation[1];  //Find the visible part of view by subtracting view's top from scrollview's bottom  
            visiblePercent = (float) visiblePart / viewHeight * 100;
        }
    }else{      //if view's top is outside the scroll view.
        if(viewBottom > scrollViewLocation[1]){ //if view's bottom is outside the scroll view
            int visiblePart = viewBottom - scrollViewLocation[1]; //Find the visible part of view by subtracting scroll view's top from view's bottom
            visiblePercent = (float) visiblePart / viewHeight * 100;
        }
    }
    if(visiblePercent > 0f){
        visibleWidgets.add(tag);        //List of visible view.
    }
    if(childPending == 0){
        //Do after iterating all children.
    }
}

Si vous ressentez une amélioration de ce code, veuillez contribuer.


0

J'ai fini par implémenter une combinaison de deux des réponses Java (@ bill-mote https://stackoverflow.com/a/12428154/3686125 et @ denys-vasylenko https://stackoverflow.com/a/25528434/3686125 ) dans mon projet sous la forme d'un ensemble d'extensions Kotlin, qui prennent en charge les contrôles ScrollView vertical standard ou HorizontalScrollView.

Je viens de les jeter dans un fichier Kotlin nommé Extensions.kt, pas de classe, juste des méthodes.

Je les ai utilisés pour déterminer sur quel élément effectuer un accrochage lorsqu'un utilisateur arrête de défiler dans diverses vues de défilement de mon projet:

fun View.isPartiallyOrFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean {
    @Suppress("CanBeVal") var scrollBounds = Rect()
    horizontalScrollView.getHitRect(scrollBounds)
    return getLocalVisibleRect(scrollBounds)
}

fun View.isPartiallyOrFullyVisible(scrollView: ScrollView) : Boolean {
    @Suppress("CanBeVal") var scrollBounds = Rect()
    scrollView.getHitRect(scrollBounds)
    return getLocalVisibleRect(scrollBounds)
}

fun View.isFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean {
    @Suppress("CanBeVal") var scrollBounds = Rect()
    horizontalScrollView.getDrawingRect(scrollBounds)
    val left = x
    val right = left + width
    return scrollBounds.left < left && scrollBounds.right > right
}

fun View.isFullyVisible(scrollView: ScrollView) : Boolean {
    @Suppress("CanBeVal") var scrollBounds = Rect()
    scrollView.getDrawingRect(scrollBounds)
    val top = y
    val bottom = top + height
    return scrollBounds.top < top && scrollBounds.bottom > bottom
}

fun View.isPartiallyVisible(horizontalScrollView: HorizontalScrollView) : Boolean = isPartiallyOrFullyVisible(horizontalScrollView) && !isFullyVisible(horizontalScrollView)
fun View.isPartiallyVisible(scrollView: ScrollView) : Boolean = isPartiallyOrFullyVisible(scrollView) && !isFullyVisible(scrollView)

Exemple d'utilisation, itération dans les enfants LinearLayout de scrollview et journalisation des sorties:

val linearLayoutChild: LinearLayout = getChildAt(0) as LinearLayout
val scrollView = findViewById(R.id.scroll_view) //Replace with your scrollview control or synthetic accessor
for (i in 0 until linearLayoutChild.childCount) {
    with (linearLayoutChild.getChildAt(i)) {
        Log.d("ScrollView", "child$i left=$left width=$width isPartiallyOrFullyVisible=${isPartiallyOrFullyVisible(scrollView)} isFullyVisible=${isFullyVisible(scrollView)} isPartiallyVisible=${isPartiallyVisible(scrollView)}")
    }
}

1
pourquoi vous utilisez varet supprimez l'indication ide?
Filipkowicz

-1

En utilisant la réponse @Qberticus qui était au point mais super btw, j'ai compilé un tas de codes pour vérifier si chaque fois qu'un scrollview est appelé et fait défiler, il déclenche la réponse @Qberticus et vous pouvez faire ce que vous voulez, dans mon cas, j'ai un réseau social contenant des vidéos, donc lorsque la vue est dessinée sur l'écran, je joue la vidéo de la même idée que Facebook et Instagram. Voici le code:

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

                    @Override
                    public void onScrollChanged() {
                        //mainscrollview is my scrollview that have inside it a linearlayout containing many child views.
                        Rect bounds = new Rect();
                         for(int xx=1;xx<=postslayoutindex;xx++)
                         {

                          //postslayoutindex is the index of how many posts are read.
                          //postslayoutchild is the main layout for the posts.
                        if(postslayoutchild[xx]!=null){

                            postslayoutchild[xx].getHitRect(bounds);

                        Rect scrollBounds = new Rect();
                        mainscrollview.getDrawingRect(scrollBounds);

                        if(Rect.intersects(scrollBounds, bounds))
                        {
                            vidPreview[xx].startPlaywithoutstoppping();
                         //I made my own custom video player using textureview and initialized it globally in the class as an array so I can access it from anywhere.
                        }
                        else
                        {

                        }


                        }
                    }
                    }
                });
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.