android developer
android developer

Reputation: 116322

android - way to be informed when a view is shown within a scrollview?

i have a simple question:

suppose i have some views on a scrollView (or a horizontalScrollView) . is there any way to add a listener that will tell me when such a view is getting inside and outside the visible area ?

the only similar question i've seen is this: Android: how to check if a View inside of ScrollView is visible? but i want to be informed when such an event occurs (becoming hidden/visible) .

Upvotes: 3

Views: 5424

Answers (3)

android developer
android developer

Reputation: 116322

I've found a nice way to be notified of what i've asked about here.

it works for scrollView with vertical LinearLayout, but if you wish you can make it work for other cases too, depending on the case.

i'm not sure if i should handle onSizeChanged() method too, and if so, what to do there, but in all other cases, this code works fine.

here's the code:

MainActivity.java (for testing) :

public class MainActivity extends Activity
  {
  @Override
  protected void onCreate(final Bundle savedInstanceState)
    {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final CustomScrollView scrollView=(CustomScrollView)findViewById(R.id.scrollView1);
    scrollView.setOnChildViewVisibilityChangedListener(new onChildViewVisibilityChangedListener()
      {
        @Override
        public void onChildViewVisibilityChanged(final int index,final View v,final boolean becameVisible)
          {
          Log.d("Applog","index:"+index+" visible:"+becameVisible);
          }
      });
    final ViewGroup container=(ViewGroup)findViewById(R.id.linearLayout);
    for(int i=0;i<20;++i)
      {
      final TextView tv=new TextView(this);
      tv.setText("item "+i);
      tv.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,300));
      container.addView(tv);
      }
    }
  }

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.example.scrollviewvisibilitydetector.CustomScrollView
        android:id="@+id/scrollView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"  >

        <LinearLayout android:id="@+id/linearLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >
        </LinearLayout>
    </com.example.scrollviewvisibilitydetector.CustomScrollView>

</RelativeLayout>

CustomScrollView.java (the real deal...) :

public class CustomScrollView extends ScrollView
  {
  Set<Integer>                         _shownViewsIndices =new HashSet<Integer>();
  onChildViewVisibilityChangedListener _onChildViewVisibilityChangedListener;

  public interface onChildViewVisibilityChangedListener
    {
    public void onChildViewVisibilityChanged(int index,View v,boolean becameVisible);
    }

  public CustomScrollView(final Context context)
    {
    super(context);
    }

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

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

  public void setOnChildViewVisibilityChangedListener(final onChildViewVisibilityChangedListener onChildViewVisibilityChangedListener)
    {
    _onChildViewVisibilityChangedListener=onChildViewVisibilityChangedListener;
    }

  @Override
  protected void onLayout(final boolean changed,final int l,final int t,final int r,final int b)
    {
    super.onLayout(changed,l,t,r,b);
    checkViewsVisibility(l,t);
    }

  private void checkViewsVisibility(final int l,final int t)
    {
    final ViewGroup viewGroup=(ViewGroup)getChildAt(0);
    final int childCount=viewGroup.getChildCount();
    if(childCount==0)
      return;
    final int parentBottom=t+getHeight();
    // prepare to use binary search to find a view that is inside the bounds
    int min=0,max=childCount-1,piv=-1;
    int childTop,childBottom;
    View v;
    // check previously shown views
    for(final Iterator<Integer> iterator=_shownViewsIndices.iterator();iterator.hasNext();)
      {
      final Integer cur=iterator.next();
      v=viewGroup.getChildAt(cur);
      childTop=v.getTop();
      childBottom=v.getBottom();
      if(childTop<=parentBottom&&childBottom>=t)
        {
        if(piv==-1)
          piv=cur;
        }
      else
        {
        if(_onChildViewVisibilityChangedListener!=null)
          _onChildViewVisibilityChangedListener.onChildViewVisibilityChanged(cur,v,false);
        iterator.remove();
        }
      }
    if(piv==-1)
      {
      // check first view
      v=viewGroup.getChildAt(min);
      childTop=v.getTop();
      childBottom=v.getBottom();
      if(childTop<=parentBottom&&childBottom>=t)
        piv=min;
      else
        {
        // check last view
        v=viewGroup.getChildAt(max);
        childTop=v.getTop();
        childBottom=v.getBottom();
        if(childTop<=parentBottom&&childBottom>=t)
          piv=min;
        }
      if(piv==-1)
        while(true)
          {
          piv=(min+max)/2;
          v=viewGroup.getChildAt(piv);
          childTop=v.getTop();
          childBottom=v.getBottom();
          if(childTop<=parentBottom&&childBottom>=t)
            break;
          if(max-min==1)
            return;
          if(childBottom<t)
            // view above bounds
            min=piv;
          else max=piv;
          }
      }
    //
    for(int i=piv;i<childCount;++i)
      {
      v=viewGroup.getChildAt(i);
      childTop=v.getTop();
      childBottom=v.getBottom();
      // _shownViewsIndices.
      if(childTop<=parentBottom&&childBottom>=t&&!_shownViewsIndices.contains(i))
        {
        _shownViewsIndices.add(i);
        if(_onChildViewVisibilityChangedListener!=null)
          _onChildViewVisibilityChangedListener.onChildViewVisibilityChanged(i,v,true);
        }
      }
    for(int i=piv-1;i>=0;--i)
      {
      v=viewGroup.getChildAt(i);
      childTop=v.getTop();
      childBottom=v.getBottom();
      if(childTop<=parentBottom&&childBottom>=t&&!_shownViewsIndices.contains(i))
        {
        _shownViewsIndices.add(i);
        if(_onChildViewVisibilityChangedListener!=null)
          _onChildViewVisibilityChangedListener.onChildViewVisibilityChanged(i,v,true);
        }
      }
    }

  @Override
  protected void onScrollChanged(final int l,final int t,final int oldl,final int oldt)
    {
    super.onScrollChanged(l,t,oldl,oldt);
    checkViewsVisibility(l,t);
    }
  }

Upvotes: 0

Risadinha
Risadinha

Reputation: 16666

Subclass the view classes you are using (I did this for ImageView as I was only adding those to my scroll view):

public class PeekImageView extends ImageView implements ViewTreeObserver.OnScrollChangedListener {
    private static final String LOG_TAG = "PeekImageView";
    private InViewportListener inViewportListener;
    private boolean isInViewport = false;

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

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

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

    public interface InViewportListener {
        void onViewportEnter(PeekImageView view);
        void onViewportExit(PeekImageView view);
    }

    public void setInViewportListener(InViewportListener listener) {
        this.inViewportListener = listener;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        ViewTreeObserver vto = getViewTreeObserver();
        if (vto != null) {
            vto.addOnScrollChangedListener(this);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        ViewTreeObserver vto = getViewTreeObserver();
        if (vto != null) {
            vto.removeOnScrollChangedListener(this);
        }
    }

    @Override
    public void onScrollChanged() {
        Rect bounds = new Rect();
        boolean inViewport = getLocalVisibleRect(bounds);
        Log.d(LOG_TAG, "is in view " + bounds + " : " + inViewport + " ; " + bounds);
        if (inViewportListener != null && isInViewport != inViewport) {
            if (inViewport) {
                inViewportListener.onViewportEnter(this);
            } else {
                inViewportListener.onViewportExit(this);
            }
        }
        isInViewport = inViewport;
    }
}

Attaching an InViewportListener to an instance of this PeekImageView will get you notified whenever the view enters or leaves the visible part of the window (the viewport).

Upvotes: 5

Blundell
Blundell

Reputation: 76458

You could do something like:

1) keep a list/array of views that are contained in your ScrollView.

2) Set a listener on the scroll view for when the scroll is changed: Synchronise ScrollView scroll positions - android

3) In the listener loop through these views using the Android: how to check if a View inside of ScrollView is visible? method to see if they have gone of the screen

This is a basic method but it'll work, how fast it is depends on whats on your screen etc, but it starts you in the right direction

Upvotes: 1

Related Questions