Android: ¿cómo verificar si una vista dentro de ScrollView es visible?


168

Tengo una ScrollViewque contiene una serie de Views. Me gustaría poder determinar si una vista está actualmente visible (si alguna parte de ella está siendo mostrada actualmente por el ScrollView). Esperaría que el siguiente código haga esto, 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
}

Tengo curiosidad de cómo conseguiste que esto funcione. Estoy tratando de hacer lo mismo, pero un ScrollView solo puede alojar 1 hijo directo. ¿Están sus "series de vistas" envueltas en otro diseño dentro de ScrollView? Así es como se presentan los míos, pero cuando hago eso, ninguna de las respuestas dadas aquí funciona para mí.
Gallo242

1
Sí, mi serie de vistas está dentro de un LinearLayout, que es el 1 hijo de ScrollView. La respuesta de Qberticus funcionó para mí.
ab11

Respuestas:


65

Use en View#getHitRectlugar de View#getDrawingRecten la vista que está probando. Puede usar View#getDrawingRecten ScrollViewlugar de calcular explícitamente.

Código de View#getDrawingRect:

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

Código de View#getHitRect:

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

35
¿Dónde debería llamar a estos métodos?
Tooto

3
@Qberticus ¿Cómo llamar a los métodos? Lo estoy usando y siempre devuelve falso. Por favor, hágamelo saber
KK_07k11A0585

2
¿Exactamente dónde llamar a estos métodos?
zemaitis

193

Esto funciona:

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
Funciona perfectamente. Para hacerlo más claro: devuelve verdadero si la vista es total o parcialmente visible; falso significa que la vista no es completamente visible.
qwertzguy

1
[+1] Usé este código para GridView/ ListView/ GridViewWithHeadertrabajar con SwipeRefreshLayout.
Kartik

¿Podría alguien explicar por qué esto funciona? getHitRectdevuelve un rect en las coordenadas principales, pero getLocalVisibleRectdevuelve un rect en las coordenadas locales de la vista de desplazamiento, ¿no?
Pin

3
Esto no cubre las superposiciones, si la vista secundaria está solapada por otro elemento secundario, seguirá devolviéndose verdadero
Pradeep

1
Sí, necesitamos una instancia de Rect. Pero es necesario obtener GetHitRect. ¿Hay alguna diferencia si uso un Rect (0,0-0,0). Podemos ver la llamada getLocalVisibleRect getGlobalVisibleRect. Y Rect se establece aquí r.set (0, 0, ancho, alto);. @ BillMote
chefish

56

Si desea detectar que la vista es TOTALMENTE 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;
    }
}

66
Esta es la respuesta correcta =) En mi caso, cambié el if así: scrollBounds.top <= top && scrollBounds.bottom => bottom
Helton Isac

2
+1 Helton si su vista se empuja contra la parte superior o inferior de su vista de desplazamiento, necesitará el <= o> = respectivamente
Joe Maher

¿Realmente has probado esto? Siempre devuelve falso en el diseño más simple ScrollView y TextView como niño.
Farid

1
¿Cuál es la diferencia entre getHitRect () y getDrawingRect ()? Por favor guía
VVB

2
Este código solo funciona si la vista se agrega directamente a la raíz del contenedor ScrollView. Verifique la respuesta de Phan Van Linh si desea manejar una vista infantil en una vista infantil, etc.
thijsonline

12

Mi solución es usar NestedScrollViewelemento de desplazamiento:

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

        }
    });
}

requiere API 23+
SolidSnake

@SolidSnake, no necesitas importar clases diferentes, funciona bien
Parth Anjaria

10

Para ampliar un poco la respuesta de Bill Mote usando getLocalVisibleRect, es posible que desee verificar si la vista solo es parcialmente 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
}

66
Esto no funciona ... incluso la vista parcialmente visible se clasifica como completamente visible
azfar

10

Esta extensión ayuda a detectar la vista totalmente visible.
También funciona si Viewes hijo de un hijo de ... de ScrollView(por ejemplo: 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()y view.getX()devuelva el valor x, y al PRIMER PADRE .

2) Aquí hay un ejemplo sobre cómo getDrawingRectdevolverá Linkingrese la descripción de la imagen aquí


Quería una solución donde el método debería devolver falso si la vista está oculta debajo del teclado y esto hace el trabajo. Gracias.
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
Esto es diferente de lo que ab11 pidió. isShown () solo verifica la marca de visibilidad, no si la vista está en la región visible de la pantalla.
Romain Guy

44
@Romain Guy El código no cubre cuando una vista se desplaza por completo de la pantalla. Debe ser `public static int getVisiblePercent (View v) {if (v.isShown ()) {Rect r = new Rect (); boolean isVisible = v.getGlobalVisibleRect (r); if (isVisible) {double sVisible = r.width () * r.height (); doble sTotal = v.getWidth () * v.getHeight (); return (int) (100 * sVisible / sTotal); } else {return -1; }} else {return -1; }} `
chefish

6

Me enfrenté al mismo problema hoy. Mientras busqué en Google y leí la referencia de Android, encontré esta publicación y un método que terminé usando;

public final boolean getLocalVisibleRect (Rect r)

Agradable de ellos no solo para proporcionar Rect sino también booleano que indica si View es visible en absoluto. En el lado negativo, este método no está documentado :(


1
Esto solo le indica si el elemento está configurado para visibilidad (verdadero). No le dice si el elemento "visible" es realmente visible dentro de la ventana gráfica.
Bill Mote

El código de getLocalVisibleRect no respalda su afirmación: `public boolean final getLocalVisibleRect (Rect r) {final Point offset = mAttachInfo! = Null? mAttachInfo.mPoint: nuevo punto (); if (getGlobalVisibleRect (r, offset)) {r.offset (-offset.x, -offset.y); // hacer que r local return true; } falso retorno; } `
mbafford

6

Si quieres detectar si tu Viewestá completamente visible, prueba con este método:

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

Estrictamente hablando, puede obtener la visibilidad de una vista con:

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

Los posibles valores constantes de la visibilidad en una Vista son:

VISIBLE Esta vista es visible. Usar con setVisibility (int) y android: visibilidad.

INVISIBLE Esta vista es invisible, pero aún ocupa espacio para propósitos de diseño. Usar con setVisibility (int) y android: visibilidad.

IDA Esta vista es invisible y no ocupa espacio para el diseño. Usar con setVisibility (int) y android: visibilidad.


3
aplauso lento Lo que el OP quería saber es, suponiendo que la visibilidad de la vista es Vista # VISIBLE, cómo saber si la vista en sí es visible dentro de una vista de desplazamiento.
Joao Sousa

1
Acabo de comprobar un proyecto simple. El diseño tiene ScrollView y TextView como niño; siempre devuelve falso a pesar de que TextView es completamente visible.
Farid

Vuelve siempre falso.
Rahul

3

Puede usar el FocusAwareScrollViewque notifica cuando la vista se hace 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
                }
            }
        });

    }

Aquí hay clase:

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

Camino de Kotlin;

Una extensión para enumerar el desplazamiento de la vista de desplazamiento y obtener una acción si la vista secundaria es visible en la pantalla.

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

Use esta función de extensión para cualquier vista desplazable

nestedScrollView.setChildViewOnScreenListener(childView) {
               action()
            }

0

Sé que es muy tarde. Pero tengo una buena solución. A continuación se muestra el fragmento de código para obtener el porcentaje de visibilidad de la vista en la vista de desplazamiento.

En primer lugar, configure el oyente táctil en la vista de desplazamiento para obtener la devolución de llamada para detener el desplazamiento.

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

En el fragmento de código anterior, recibimos devoluciones de llamadas para eventos táctiles de vista de desplazamiento y publicamos un ejecutable después de 150 milis (no obligatorio) después de detener la devolución de llamada para desplazamiento. En ese ejecutable obtendremos la ubicación de la vista de desplazamiento en la pantalla y la altura de la vista de desplazamiento. Luego obtenga la instancia del grupo de vista hijo directo de la vista de desplazamiento y obtenga los recuentos secundarios. En mi caso, el elemento secundario directo de la vista de desplazamiento es LinearLayout llamado scrollViewRootChild . Luego, repita todas las vistas secundarias de scrollViewRootChild . En el fragmento de código anterior, puede ver que obtengo la ubicación del niño en la pantalla en una matriz entera llamada viewLocation , obtenga la altura de la vista en el nombre de la variable viewHeight . Luego llamé a un método privado getViewVisibilityOnScrollStopped. Puede comprender el funcionamiento interno de este método leyendo la documentación.

/**
 * 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 siente alguna mejora en este código, por favor contribuya.


0

Terminé implementando una combinación de dos de las respuestas de Java (@ bill-mote https://stackoverflow.com/a/12428154/3686125 y @ denys-vasylenko https://stackoverflow.com/a/25528434/3686125 ) en mi proyecto como un conjunto de extensiones de Kotlin, que admiten controles ScrollView verticales u HorizontalScrollView estándar.

Acabo de lanzar estos en un archivo Kotlin llamado Extensions.kt, sin clase, solo métodos.

Los utilicé para determinar a qué elemento ajustar cuando un usuario deja de desplazarse en varias vistas de desplazamiento en mi proyecto:

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)

Ejemplo de uso, iterando a través de los elementos secundarios LinearLayout de scrollview y las salidas de registro:

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
¿Por qué estás usando vary suprimiendo la pista de ide?
Filipkowicz

-1

Utilizando la respuesta @Qberticus, que era al punto pero por cierto genial, compilé un montón de códigos para verificar si cada vez que se llama a una vista de desplazamiento y se desplaza, activa la respuesta @Qberticus y puedes hacer lo que quieras, en mi caso tengo un red social que contiene videos, así que cuando la vista se dibuja en la pantalla, reproduzco el video con la misma idea que Facebook e Instagram. Aquí está el código:

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
                        {

                        }


                        }
                    }
                    }
                });
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.