Android: como verificar se uma View dentro do ScrollView está visível?


168

Eu tenho um ScrollViewque contém uma série de Views. Eu gostaria de poder determinar se uma visualização está atualmente visível (se alguma parte dela é exibida no momento ScrollView). Eu esperaria que o código abaixo faça isso, surpreendentemente não:

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
}

Estou curioso para saber como você conseguiu isso. Estou tentando fazer a mesma coisa, mas um ScrollView pode hospedar apenas 1 filho direto. Suas "séries de visualizações" estão agrupadas em outro layout dentro do ScrollView? É assim que as minhas são dispostas, mas quando faço isso, nenhuma das respostas dadas aqui funciona para mim.
Rooster242

1
Sim, minhas séries de visualizações estão dentro de um LinearLayout, que é o 1 filho do ScrollView. A resposta do Qberticus funcionou para mim.
AB11

Respostas:


65

Use em View#getHitRectvez de View#getDrawingRectna exibição que você está testando. Você pode usar o View#getDrawingRectem ScrollViewvez de calcular explicitamente.

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
Onde devo chamar esses métodos?
Tooto

3
@Berberus Como chamar os métodos? Estou usando e está sempre retornando false. Por favor, avise-me #
KK_07k11A0585

2
Exatamente onde chamar esses métodos?
Zemaitis 18/09/19

193

Isso 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 perfeitamente. Para tornar mais claro: retorna true se a visualização for total ou parcialmente visível; false significa que a visualização não está completamente visível.
Qwertzguy

1
[+1] Eu usei esse código para começar GridView/ ListView/ GridViewWithHeadertrabalhar SwipeRefreshLayout.
Kartik

Alguém poderia explicar por que isso funciona? getHitRectretorna um reto nas coordenadas pai, mas getLocalVisibleRectretorna um reto nas coordenadas locais da visualização de rolagem, não é?
Pin

3
Esta não cobre sobreposições, se crianças Ver é sobreposto por um outro elemento filho, ele ainda retornar true
Pradeep

1
Sim, precisamos de uma instância do Rect. Mas é necessário getHitRect. Existe alguma diferença se eu usar um Rect (0,0-0,0). Podemos ver a chamada getLocalVisibleRect getGlobalVisibleRect.And Rect é definido aqui r.set (0, 0, width, height);. @ BillMote
chefish

56

Se você deseja detectar que a visualização é totalmente visível:

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
Esta é a resposta correta =) No meu caso, alterei o if assim: scrollBounds.top <= top && scrollBounds.bottom => bottom #
Helton Isac

2
+1 Helton se o seu ponto de vista é tanto empurrado contra a parte superior ou inferior fo seu ponto de vista de rolagem será necessário o <= ou> = respectivamente
Joe Maher

Você realmente testou isso? Ele sempre retorna false no layout mais simples, ScrollView e TextView, quando filho.
Farid

1
Qual é a diferença entre getHitRect () e getDrawingRect ()? Guia
VVB 17/06/19

2
Esse código funciona apenas se a exibição for adicionada diretamente à raiz do container ScrollView. Verifique a resposta de Phan Van Linh se você quiser lidar com uma vista criança em uma exibição de criança etc.
thijsonline

12

Minha solução é usar o 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");
                }
            }

        }
    });
}

requer API 23+
SolidSnake

@SolidSnake, não necessita de importar classe diferente, ele funciona bem
Parth Anjaria

10

Para expandir um pouco a resposta de Bill Mote usando getLocalVisibleRect, convém verificar se a exibição é apenas parcialmente visível:

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
Isto não funciona .. mesmo parcialmente vista visível é categorizado como completamente visível
Azfar

10

Esta extensão ajuda a detectar a vista totalmente visível.
Também funciona se você Viewé filho de filho de ... de ScrollView(por exemplo: 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()retorne o valor x, y para PRIMEIRO PAI .

2) Aqui está um exemplo sobre como getDrawingRectretornará o Linkinsira a descrição da imagem aqui


Eu queria uma solução em que o método retornasse false se o modo de exibição estiver oculto no teclado e isso funcionasse. Obrigado.
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
Isso é diferente do que o ab11 pediu. isShown () verifica apenas o sinalizador de visibilidade, não se a exibição está na região visível da tela.
Romain Guy

4
@Romain Guy O código não cobre quando uma exibição é totalmente rolada para fora da tela. isVisible booleano = v.getGlobalVisibleRect (r); if (isVisible) {double sVisible = r.width () * r.height (); sTotal duplo = v.getWidth () * v.getHeight (); return (int) (100 * sVisible / sTotal); } else {return -1; }} else {return -1; }} `
chefish

6

Eu enfrentei o mesmo problema hoje. Ao pesquisar no Google e ler a referência do Android, encontrei este post e um método que acabei usando;

public final boolean getLocalVisibleRect (Rect r)

Agradável não apenas fornecer Rect, mas também booleano, indicando se o View está visível. No lado negativo, este método não está documentado :(


1
Isso informa apenas se o item está definido como visibilidade (verdadeiro). Não informa se o item "visível" está realmente visível na janela de exibição.
Bill Mote

O código para getLocalVisibleRect não suporta sua reivindicação: `público final booleano getLocalVisibleRect (Rect r) {final Point offset = mAttachInfo! = Null? mAttachInfo.mPoint: new Point (); if (getGlobalVisibleRect (r, offset)) {r.offset (-offset.x, -offset.y); // tornar r local return true; } retorna falso; } `
mbafford

6

Se você deseja detectar se o seu Viewestá totalmente visible, tente com 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.
    }
}

A rigor, você pode obter a visibilidade de uma visualização com:

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

Os valores constantes possíveis da visibilidade em uma Visualização são:

VISÍVEL Esta visão é visível. Use com setVisibility (int) e android: visibilidade.

INVISÍVEL Essa visualização é invisível, mas ainda ocupa espaço para fins de layout. Use com setVisibility (int) e android: visibilidade.

IDO Esta visão é invisível, e não tomar qualquer espaço para fins de layout. Use com setVisibility (int) e android: visibilidade.


3
aplauso lento. O que o OP queria saber é, assumindo que a visibilidade da visualização é View # VISIBLE, como saber se a visualização em si é visível em uma view de rolagem.
Joao Sousa

1
Acabei de verificar um projeto simples. O layout tem ScrollView e TextView como filho; sempre retorna false, mesmo que o TextView seja totalmente visível.
Farid

Retorna sempre falso.
Rahul

3

Você pode usar o FocusAwareScrollViewque notifica quando a visualização se torna visível:

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

    }

Aqui está a 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

Caminho Kotlin;

Uma extensão para listar a rolagem da exibição de rolagem e executar uma ação se a exibição infantil estiver visível na tela.

@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 função de extensão para qualquer exibição rolável

nestedScrollView.setChildViewOnScreenListener(childView) {
               action()
            }

0

Eu sei que é muito tarde. Mas eu tenho uma boa solução. Abaixo está o snippet de código para obter a porcentagem de visibilidade da visualização na visualização por rolagem.

Antes de tudo, configure o ouvinte de toque na exibição de rolagem para obter retorno de chamada para parada de rolagem.

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

No snippet de código acima, estamos recebendo retornos de chamada para eventos de toque de exibição de rolagem e publicamos uma executável após 150 milis (não obrigatório) após a interrupção do retorno de chamada para rolagem. Nesse executável, obteremos a localização da visualização de rolagem na tela e a altura da visualização de rolagem. Em seguida, obtenha a instância direta do grupo de exibição filho da exibição de rolagem e faça a contagem de filhos. No meu caso, o filho direto da exibição de rolagem é LinearLayout chamado scrollViewRootChild . Em seguida, itere todas as visualizações filho de scrollViewRootChild . No trecho de código acima, você pode ver que estou obtendo a localização do filho na tela em uma matriz inteira chamada viewLocation , obtendo a altura de exibição no nome da variável viewHeight . Então eu chamei um método privado getViewVisibilityOnScrollStopped. Você pode entender o funcionamento interno desse método lendo a documentação.

/**
 * 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 você sentir alguma melhoria neste código, contribua.


0

Acabei implementando uma combinação de duas respostas Java (@ bill-mote https://stackoverflow.com/a/12428154/3686125 e @ denys-vasylenko https://stackoverflow.com/a/25528434/3686125 ) em meu projeto como um conjunto de extensões Kotlin, que oferecem suporte aos controles ScrollView vertical Vertical ou HorizontalScrollView.

Acabei de jogá-los em um arquivo Kotlin chamado Extensions.kt, sem classe, apenas métodos.

Usei-os para determinar a qual item ajustar quando um usuário parar de rolar em várias visualizações de rolagem no meu projeto:

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)

Exemplo de uso, iterando através dos filhos LinearLayout do scrollview e saídas de log:

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 que você está usando vare suprimindo a dica ide?
Filipkowicz 23/07/19

-1

Usando a resposta @Qberticus, que foi ao ponto, mas ótima, compilei um monte de códigos para verificar se sempre que uma visualização de rolagem é chamada e rolada, ela aciona a resposta @Qberticus e você pode fazer o que quiser, no meu caso, tenho um rede social contendo vídeos, então, quando a visualização é desenhada na tela, reproduzo o mesmo vídeo do facebook e do instagram. Aqui está o 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
                        {

                        }


                        }
                    }
                    }
                });
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.