android – What's the best way to check if the view is visible on the window?-ThrowExceptions

Exception or error:

What’s the best way to check if the view is visible on the window?

I have a CustomView which is part of my SDK and anybody can add CustomView to their layouts. My CustomView is taking some actions when it is visible to the user periodically. So if view becomes invisible to the user then it needs to stop the timer and when it becomes visible again it should restart its course.

But unfortunately there is no certain way of checking if my CustomView becomes visible or invisible to the user. There are few things that I can check and listen to:

onVisibilityChange //it is for view's visibility change, and is introduced in new API 8 version so has backward compatibility issue
onWindowVisibilityChange //but my CustomView can be part of a ViewFlipper's Views so it can pose issues
onDetachedFromWindows //this not as useful
onWindowFocusChanged //Again my CustomView can be part of ViewFlipper's views.

So if anybody has faced this kind of issues please throw some light.

How to solve:

In my case the following code works the best to listen if the View is visible or not:

@Override
protected void onWindowVisibilityChanged(int visibility) {
    super.onWindowVisibilityChanged(visibility);
    Log.e(TAG, "is view visible?: " + (visibility == View.VISIBLE));
}

###

onDraw() is called each time the view needs to be drawn. When the view is off screen then onDraw() is never called. When a tiny bit of the view is becomes visible to the user then onDraw() is called. This is not ideal but I cannot see another call to use as I want to do the same thing. Remember to call the super.onDraw or the view won’t get drawn. Be careful of changing anything in onDraw that causes the view to be invalidate as that will cause another call to onDraw.

If you are using a listview then getView can be used whenever your listview becomes shown to the user.

obviously the activity onPause() is called all your views are all covered up and are not visible to the user. perhaps calling invalidate() on the parent and if ondraw() is not called then it is not visible.

###

This is a method that I have used quite a bit in my apps and have had work out quite well for me:

static private int screenW = 0, screenH = 0;

@SuppressWarnings("deprecation") static public boolean onScreen(View view) {
    int coordinates[] = { -1, -1 };
    view.getLocationOnScreen(coordinates);

    // Check if view is outside left or top
    if (coordinates[0] + view.getWidth() < 0) return false;
    if (coordinates[1] + view.getHeight() < 0) return false;

    // Lazy get screen size. Only the first time.
    if (screenW == 0 || screenH == 0) {
        if (MyApplication.getSharedContext() == null) return false;
        Display display = ((WindowManager)MyApplication.getSharedContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        try {
            Point screenSize = new Point();
            display.getSize(screenSize); // Only available on API 13+
            screenW = screenSize.x;
            screenH = screenSize.y;
        } catch (NoSuchMethodError e) { // The backup methods will only be used if the device is running pre-13, so it's fine that they were deprecated in API 13, thus the suppress warnings annotation at the start of the method.
            screenW = display.getWidth();
            screenH = display.getHeight();
        }
    }

    // Check if view is outside right and bottom
    if (coordinates[0] > screenW) return false;
    if (coordinates[1] > screenH) return false;

    // Else, view is (at least partially) in the screen bounds
    return true;
}

To use it, just pass in any view or subclass of view (IE, just about anything that draws on screen in Android.) It’ll return true if it’s on screen or false if it’s not… pretty intuitive, I think.

If you’re not using the above method as a static, then you can probably get a context some other way, but in order to get the Application context from a static method, you need to do these two things:

1 – Add the following attribute to your application tag in your manifest:

android:name="com.package.MyApplication"

2 – Add in a class that extends Application, like so:

public class MyApplication extends Application {
    // MyApplication exists solely to provide a context accessible from static methods.
    private static Context context;

    @Override public void onCreate() {
        super.onCreate();
        MyApplication.context = getApplicationContext();
    }

    public static Context getSharedContext() {
        return MyApplication.context;
    }
}

###

In addition to the view.getVisibility() there is view.isShown().
isShown checks the view tree to determine if all ancestors are also visible.

Although, this doesn’t handle obstructed views, only views that are hidden or gone in either themselves or one of its parents.

###

In dealing with a similar issue, where I needed to know if the view has some other window on top of it, I used this in my custom View:

@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
   super.onWindowFocusChanged(hasWindowFocus);
   if (!hasWindowFocus) {

   } else {

   }
}

###

This can be checked using getGlobalVisibleRect method. If rectangle returned by this method has exactly the same size as View has, then current View is completely visible on the Screen.

/**
 * Returns whether this View is completely visible on the screen
 *
 * @param view view to check
 * @return True if this view is completely visible on the screen, or false otherwise.
 */
public static boolean onScreen(@NonNull View view) {
    Rect visibleRect = new Rect();
    view.getGlobalVisibleRect(visibleRect);
    return visibleRect.height() == view.getHeight() && visibleRect.width() == view.getWidth();
}

If you need to calculate visibility percentage you can do it using square calculation:

float visiblePercentage = (visibleRect.height() * visibleRect.width()) / (float)(view.getHeight() * view.getWidth()) 

###

you can add to your CustomView’s constractor a an onScrollChangedListener from ViewTreeObserver

so if your View is scrolled of screen you can call view.getLocalVisibleRect() and determine if your view is partly offscreen …

you can take a look to the code of my library : PercentVisibleLayout

Hope it helps!

###

in your custom view, set the listeners:

 getViewTreeObserver().addOnScrollChangedListener(this);
 getViewTreeObserver().addOnGlobalLayoutListener(this);

I am using this code to animate a view once when it is visible to user.

2 cases should be considered.

  1. Your view is not in the screen. But it will be visible if user scrolled it

    public void onScrollChanged() {
        final int i[] = new int[2];
        this.getLocationOnScreen(i);
        if (i[1] <= mScreenHeight - 50) {
            this.post(new Runnable() {
                @Override
                public void run() {
                    Log.d("ITEM", "animate");
                    //animate once
                    showValues();
                }
            });
            getViewTreeObserver().removeOnScrollChangedListener(this);
            getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }
    }
    
  2. Your view is initially in screen.(Not in somewhere else invisible to user in scrollview, it is in initially on screen and visible to user)

    public void onGlobalLayout() {
     final int i[] = new int[2];
     this.getLocationOnScreen(i);
     if (i[1] <= mScreenHeight) {
        this.post(new Runnable() {
            @Override
            public void run() {
                Log.d("ITEM", "animate");
                //animate once
                showValues();
            }
        });
        getViewTreeObserver().removeOnGlobalLayoutListener(this);
        getViewTreeObserver().removeOnScrollChangedListener(this);
     }
    }
    

###

This solution takes into account view obstructed by statusbar and toolbar, also as view outside the window (e.g. scrolled out of screen)

/**
 * Test, if given {@code view} is FULLY visible in window. Takes into accout window decorations
 * (statusbar and toolbar)
 *
 * @param view
 * @return true, only if the WHOLE view is visible in window
 */
public static boolean isViewFullyVisible(View view) {
    if (view == null || !view.isShown())
        return false;

    //windowRect - will hold available area where content remain visible to users
    //Takes into account screen decorations (e.g. statusbar)
    Rect windowRect = new Rect();
    view.getWindowVisibleDisplayFrame(windowRect);

    //if there is toolBar, get his height
    int actionBarHeight = 0;
    Context context = view.getContext();
    if (context instanceof AppCompatActivity && ((AppCompatActivity) context).getSupportActionBar() != null)
        actionBarHeight = ((AppCompatActivity) context).getSupportActionBar().getHeight();
    else if (context instanceof Activity && ((Activity) context).getActionBar() != null)
        actionBarHeight = ((Activity) context).getActionBar().getHeight();

    //windowAvailableRect - takes into account toolbar height and statusbar height
    Rect windowAvailableRect = new Rect(windowRect.left, windowRect.top + actionBarHeight, windowRect.right, windowRect.bottom);

    //viewRect - holds position of the view in window
    //(methods as getGlobalVisibleRect, getHitRect, getDrawingRect can return different result,
    // when partialy visible)
    Rect viewRect;
    final int[] viewsLocationInWindow = new int[2];
    view.getLocationInWindow(viewsLocationInWindow);
    int viewLeft = viewsLocationInWindow[0];
    int viewTop = viewsLocationInWindow[1];
    int viewRight = viewLeft + view.getWidth();
    int viewBottom = viewTop + view.getHeight();
    viewRect = new Rect(viewLeft, viewTop, viewRight, viewBottom);

    //return true, only if the WHOLE view is visible in window
    return windowAvailableRect.contains(viewRect);
}

Leave a Reply

Your email address will not be published. Required fields are marked *