Android: come verificare se è visibile una vista all'interno di ScrollView?


168

Ne ho uno ScrollViewche contiene una serie di Views. Vorrei essere in grado di determinare se una vista è attualmente visibile (se una parte di essa è attualmente visualizzata dal ScrollView). Mi aspetto che il codice seguente esegua questa operazione, sorprendentemente no:

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
}

Sono curioso di sapere come hai fatto a farlo funzionare. Sto cercando di fare la stessa cosa, ma ScrollView può ospitare solo 1 figlio diretto. Le tue "serie di viste" sono inserite in un altro layout all'interno di ScrollView? È così che sono disposte le mie, ma quando lo faccio, nessuna delle risposte fornite qui funziona per me.
Rooster242,

1
Sì, la mia serie di viste è all'interno di un LinearLayout, che è il figlio 1 di ScrollView. La risposta di Qberticus ha funzionato per me.
ab11,

Risposte:


65

Usa View#getHitRectinvece che View#getDrawingRectsulla vista che stai testando. È possibile utilizzare View#getDrawingRectsul ScrollViewinvece di calcolare esplicitamente.

Codice da View#getDrawingRect:

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

Codice da View#getHitRect:

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

35
Dove dovrei chiamare questo metodo?
Tooto,

3
@Qberticus Come chiamare i metodi? Lo sto usando e ritorna sempre falso. Per favore fatemi sapere
KK_07k11A0585

2
Esattamente dove chiamare questi metodi?
Zemaite,

193

Questo funziona:

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
Funziona perfettamente. Per renderlo più chiaro: restituisce vero se la vista è completamente o parzialmente visibile; false significa che la vista non è completamente visibile.
qwertzguy,

1
[+1] Ho usato questo codice per ottenere GridView/ ListView/ GridViewWithHeaderlavorare con SwipeRefreshLayout.
Kartik,

Qualcuno potrebbe spiegare perché questo funziona? getHitRectrestituisce un rettangolo nelle coordinate principali, ma getLocalVisibleRectrestituisce un rettangolo nelle coordinate locali della vista di scorrimento, non è vero?
Pin

3
Questo non copre le sovrapposizioni, se Child View è sovrapposto da un altro elemento figlio, tornerà comunque vero
Pradeep

1
Sì, abbiamo bisogno di un'istanza di Rect, ma è necessario getHitRect. C'è qualcosa di diverso se uso un Rect (0,0-0,0). Possiamo vedere getLocalVisibleRect chiamare getGlobalVisibleRect. E Rect è impostato qui r.set (0, 0, larghezza, altezza);. @ BillMote
chefish

56

Se si desidera rilevare che la vista è COMPLETAMENTE visibile:

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
Questa è la risposta corretta =) Nel mio caso ho cambiato il if in questo modo: scrollBounds.top <= top && scrollBounds.bottom => bottom
Helton Isac

2
+1 Helton se la tua vista è premuta contro la parte superiore o inferiore della vista di scorrimento, avrai bisogno rispettivamente di <= o> =
Joe Maher,

Hai davvero provato questo? Restituisce sempre falso nel layout più semplice ScrollView e TextView da bambino.
Farid,

1
Qual è la differenza tra getHitRect () e getDrawingRect ()? Si prega di guidare
VVB il

2
Questo codice funziona solo se la vista viene aggiunta direttamente alla radice del contenitore ScrollView. Controlla la risposta di Phan Van Linh se vuoi gestire una vista bambino in una vista bambino ecc.
thijsonline

12

La mia soluzione è utilizzare l' NestedScrollViewelemento 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");
                }
            }

        }
    });
}

richiede API 23+
SolidSnake

@SolidSnake, no devi importare classi diverse, funziona benissimo
Parth Anjaria

10

Per espandere un po 'la risposta di Bill Mote usando getLocalVisibleRect, potresti voler controllare se la vista è solo parzialmente visibile:

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
Questo non funziona .. anche la vista parzialmente visibile è classificata come completamente visibile
azfar

10

Questa estensione aiuta a rilevare la vista completamente visibile.
Funziona anche se sei Viewfiglio di un figlio di ... di ScrollView(ad esempio: 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
}

Nota

1) view.getY()e view.getX()restituisce il valore x, y al PRIMO GENITORE .

2) Ecco un esempio di come getDrawingRectrestituirà Linkinserisci qui la descrizione dell'immagine


Volevo una soluzione in cui il metodo dovesse restituire false se la vista è nascosta sotto la tastiera e questo fa il suo lavoro. Grazie.
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
Questo è diverso da quello che ha richiesto ab11. isShown () controlla solo l'indicatore di visibilità, non se la vista si trova nell'area visibile dello schermo.
Romain Guy,

4
@Romain Guy Il codice non copre quando una vista è completamente spostata dallo schermo. Dovrebbe essere `public static int getVisiblePercent (View v) {if (v.isShown ()) {Rect r = new Rect (); booleano 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

Ho affrontato lo stesso problema oggi. Mentre cercavo su Google e leggevo i riferimenti Android ho trovato questo post e un metodo che ho finito per usare invece;

public final boolean getLocalVisibleRect (Rect r)

Bello da parte loro non solo per fornire Rect ma anche booleano che indica se View è visibile. Sul lato negativo questo metodo non è documentato :(


1
Questo ti dice solo se l'elemento è impostato su visibilità (vero). Non ti dice se l'elemento "visibile" è effettivamente visibile nella finestra.
Bill Mote,

Il codice per getLocalVisibleRect non supporta il tuo reclamo: `getLocalVisibleRect pubblico finale booleano (Rect r) {offset punto finale = mAttachInfo! = Null? mAttachInfo.mPoint: new Point (); if (getGlobalVisibleRect (r, offset)) {r.offset (-offset.x, -offset.y); // rende vero il ritorno locale; } return false; } `
mbafford,

6

Voglio rilevare se Viewè completamente visible, provare con questo metodo:

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

A rigor di termini è possibile ottenere la visibilità di una vista con:

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

I valori costanti possibili della visibilità in una vista sono:

VISIBILE Questa vista è visibile. Utilizzare con setVisibility (int) e Android: visibilità.

INVISIBILE Questa vista è invisibile, ma occupa ancora spazio per scopi di layout. Utilizzare con setVisibility (int) e Android: visibilità.

ANDATO Questa vista è invisibile e non occupa spazio ai fini del layout. Utilizzare con setVisibility (int) e Android: visibilità.


3
applauso lento. Ciò che l'OP voleva sapere è, supponendo che la visibilità della vista sia Visualizza # VISIBILE, come sapere se la vista stessa è visibile all'interno di una vista di scorrimento.
Joao Sousa,

1
Ho appena verificato un semplice progetto. Il layout ha ScrollView e TextView da bambino; restituisce sempre false anche se TextView è completamente visibile.
Farid

Restituisce sempre falso.
Rahul,

3

È possibile utilizzare il FocusAwareScrollViewquale notifica quando la vista diventa visibile:

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

    }

Ecco 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

Via di Kotlin;

Un'estensione per elencare lo scorrimento della vista di scorrimento e ottenere un'azione se la vista figlio è visibile sullo schermo.

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

Utilizzare questa funzione di estensione per qualsiasi vista scorrevole

nestedScrollView.setChildViewOnScreenListener(childView) {
               action()
            }

0

So che è molto tardi. Ma ho una buona soluzione. Di seguito è riportato lo snippet di codice per ottenere la percentuale di visibilità della vista nella vista a scorrimento.

Prima di tutto imposta l'ascoltatore touch sulla vista di scorrimento per ottenere la richiamata per l'arresto dello scorrimento.

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

Nello snippet di codice sopra riportato, riceviamo richiamate per gli eventi touch della vista di scorrimento e pubblichiamo un eseguibile dopo 150 millis (Non obbligatorio) dopo aver arrestato la richiamata per lo scorrimento. In quel runnable avremo la posizione della vista di scorrimento sullo schermo e l'altezza della vista di scorrimento. Quindi ottenere l'istanza di viewgroup figlio diretta della vista di scorrimento e ottenere i conteggi figlio. Nel mio caso, il figlio diretto della vista scroll è LinearLayout chiamato scrollViewRootChild . Quindi ripetere tutte le visualizzazioni figlio di scrollViewRootChild . Nel frammento di codice sopra puoi vedere che sto ottenendo la posizione del bambino sullo schermo in un array intero chiamato viewLocation , ottieni l'altezza della vista in nome variabile viewHeight . Quindi ho chiamato un metodo privato getViewVisibilityOnScrollStopped. È possibile ottenere la comprensione del funzionamento interno di questo metodo leggendo la documentazione.

/**
 * 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.
    }
}

Se riscontri miglioramenti in questo codice, ti preghiamo di contribuire.


0

Ho finito per implementare una combinazione di due delle risposte Java (@ bill-mote https://stackoverflow.com/a/12428154/3686125 e @ denys-vasylenko https://stackoverflow.com/a/25528434/3686125 ) in il mio progetto come un insieme di estensioni di Kotlin, che supportano i controlli standard ScrollView verticale o HorizontalScrollView.

Ho appena lanciato questi file in un file Kotlin chiamato Extensions.kt, nessuna classe, solo metodi.

Ho usato questi per determinare a quale elemento agganciare quando un utente smette di scorrere in varie visualizzazioni di scorrimento nel mio progetto:

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)

Esempio di utilizzo, ripetendo i figli LinearLayout di scrollview e registrando gli output:

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
perché stai usando vare sopprimendo il suggerimento ide?
Filipkowicz,

-1

Usando la risposta di @Qberticus che era al punto, ma a proposito, ho messo insieme un sacco di codici per verificare se ogni volta che viene chiamata una scrollview e ho fatto scorrere, innesca la risposta di @Qberticus e puoi fare quello che vuoi, nel mio caso ho un social network contenente video, quindi quando la vista viene disegnata sullo schermo riproduco il video stessa idea come Facebook e Instagram. Ecco il codice:

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
                        {

                        }


                        }
                    }
                    }
                });
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.