ANDROID: Tracking visibility of a VIEW, How to check programmatically if the VIEW is visible on the screen or not

import android.graphics.Rect;import android.os.SystemClock;import android.view.View;
public class VisibilityChecker {
/**
* A rect to use for hit testing.
* Create this once to avoid excess garbage collection
*/
private final Rect mClipRect = new Rect();
/**
* Whether the visible time has elapsed
* from the startPlayer time. Easily mocked for testing.
*/
boolean hasRequiredTimeElapsed(
final long startTimeMillis,
final int minTimeViewed) {
return SystemClock.uptimeMillis() - startTimeMillis >= minTimeViewed;
}
/**
* Whether the view is at least certain % visible
*/
public boolean isVisible(
final View rootView,
final View view,
final int minPercentageViewed) {
return !(view == null || view.getVisibility() != View.VISIBLE || view.getParent() == null) &&
isViewVisible(view, minPercentageViewed);
}
public boolean isVisibleWithParent(
final View rootView,
final View view,
final int minPercentageViewed) {
if (view == null || view.getVisibility() != View.VISIBLE ||
view.getParent() == null || rootView.getParent() == null) {
return false; }
return isViewVisible(view, minPercentageViewed);
}
private boolean isViewVisible(View view, int minPercentageViewed) { if (!view.getGlobalVisibleRect(mClipRect)) {
/** Not visible */ return false; }

/** visible check - the cast is to avoid int overflow for large views. */
final long visibleViewArea = (long) mClipRect.height() * mClipRect.width(); final long totalViewArea = (long) view.getHeight() * view.getWidth(); return totalViewArea > 0 && 100 * visibleViewArea >= minPercentageViewed * totalViewArea; }
}
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.os.Handler;
import android.os.SystemClock;
import android.view.View;
import android.view.ViewTreeObserver;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
/**
* Tracks views to determine when they become visible or invisible, where visibility is defined as
* having been at least X% on the screen.
*/
public class VisibilityTracker {
// Time interval to use for throttling visibility checks.
private static final int VISIBILITY_THROTTLE_MILLIS = 100;
// Trim the tracked views after this many accesses. This protects us against tracking
// too many views if the developer uses the adapter for multiple ListViews. It also
// limits the memory leak if a developer forgets to call destroy().
static final int NUM_ACCESSES_BEFORE_TRIMMING = 50;
// Temporary array of trimmed views so that we don't allocate this on every trim.
private final ArrayList<View> mTrimmedViews;
// Incrementing access counter. Use a long to support very long-lived apps.
private long mAccessCounter = 0;
// Listener that passes all visible and invisible views when a visibility check occurs
interface VisibilityTrackerListener {
void onVisibilityChanged(List<View> visibleViews, List<View> invisibleViews);
}
ViewTreeObserver.OnScrollChangedListener onScrollChangedListener;
final WeakReference<View> mRootView;
static class TrackingInfo {
int mMinViewablePercent;
// Must be less than mMinVisiblePercent
int mMaxInvisiblePercent;
long mAccessOrder;
View mRootView;
}
// Views that are being tracked, mapped to the min viewable percentage
private final Map<View, TrackingInfo> mTrackedViews;
// Object to check actual visibility
private final VisibilityChecker mVisibilityChecker;
// Callback listener
private VisibilityTrackerListener mVisibilityTrackerListener;
// Runnable to run on each visibility loop
private final VisibilityRunnable mVisibilityRunnable;
// Handler for visibility
private final Handler mVisibilityHandler;
// Whether the visibility runnable is scheduled
private boolean mIsVisibilityScheduled;
public VisibilityTracker(final Context context) {
this(context,
new WeakHashMap<View, TrackingInfo>(10),
new VisibilityChecker(),
new Handler());
}
VisibilityTracker(final Context context,
final Map<View, TrackingInfo> trackedViews,
final VisibilityChecker visibilityChecker,
final Handler visibilityHandler) {
mTrackedViews = trackedViews;
mVisibilityChecker = visibilityChecker;
mVisibilityHandler = visibilityHandler;
mVisibilityRunnable = new VisibilityRunnable();
mTrimmedViews = new ArrayList<View>(NUM_ACCESSES_BEFORE_TRIMMING);
final View rootView = ((Activity) context).getWindow().getDecorView();
mRootView = new WeakReference<View>(rootView);
final ViewTreeObserver viewTreeObserver = rootView.getViewTreeObserver();
if (!viewTreeObserver.isAlive()) {
Log.internal("", "Visibility Tracker was unable to track views because the"
+ " root view tree observer was not alive");
} else {
onScrollChangedListener = new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
scheduleVisibilityCheck();
}
};
viewTreeObserver.addOnScrollChangedListener(onScrollChangedListener);
}
}
void setVisibilityTrackerListener(
final VisibilityTrackerListener visibilityTrackerListener) {
mVisibilityTrackerListener = visibilityTrackerListener;
}
/**
* Tracks the given view for visibility.
*/
void addView(final View view, final int minPercentageViewed) {
addView(view, view, minPercentageViewed);
}
void addView(View rootView, final View view, final int minPercentageViewed) {
addView(rootView, view, minPercentageViewed, minPercentageViewed);
}
void addView(View rootView, final View view, final int minVisiblePercentageViewed, final int maxInvisiblePercentageViewed) {
// Find the view if already tracked
TrackingInfo trackingInfo = mTrackedViews.get(view);
if (trackingInfo == null) {
trackingInfo = new TrackingInfo();
mTrackedViews.put(view, trackingInfo);
scheduleVisibilityCheck();
}
int maxInvisiblePercent = Math.min(maxInvisiblePercentageViewed, minVisiblePercentageViewed); trackingInfo.mRootView = rootView;
trackingInfo.mMinViewablePercent = minVisiblePercentageViewed;
trackingInfo.mMaxInvisiblePercent = maxInvisiblePercent;
trackingInfo.mAccessOrder = mAccessCounter;
// Trim the number of tracked views to a reasonable number
mAccessCounter++;
if (mAccessCounter % NUM_ACCESSES_BEFORE_TRIMMING == 0) {
trimTrackedViews(mAccessCounter - NUM_ACCESSES_BEFORE_TRIMMING);
}
}
private void trimTrackedViews(long minAccessOrder) {
// Clear anything that is below minAccessOrder.
for (final Map.Entry<View, TrackingInfo> entry : mTrackedViews.entrySet()) {
if (entry.getValue().mAccessOrder < minAccessOrder) {
mTrimmedViews.add(entry.getKey());
}
}
for (View view : mTrimmedViews) {
removeView(view);
}
mTrimmedViews.clear();
}
/**
* Stops tracking a view, cleaning any pending tracking
*/
void removeView(final View view) {
try {
mTrackedViews.remove(view);
} catch (ConcurrentModificationException e) {
e.printStackTrace();
}
}
/**
* Immediately clear all views. Useful for when we re-request ads for an ad placer
*/
void clear() {
mTrackedViews.clear();
mVisibilityHandler.removeMessages(0);
mIsVisibilityScheduled = false;
}
/**
* Destroy the visibility tracker, preventing it from future use.
*/
void destroy() {
clear();
final View rootView = mRootView.get();
if (rootView != null && onScrollChangedListener != null) {
final ViewTreeObserver viewTreeObserver = rootView.getViewTreeObserver();
if (viewTreeObserver.isAlive()) {
viewTreeObserver.removeOnScrollChangedListener(onScrollChangedListener);
}
onScrollChangedListener = null;
}
mVisibilityTrackerListener = null;
}
void scheduleVisibilityCheck() {
// Tracking this directly instead of calling hasMessages directly because we measured that
// this led to slightly better performance.
if (mIsVisibilityScheduled) {
return;
}
mIsVisibilityScheduled = true;
mVisibilityHandler.postDelayed(mVisibilityRunnable, VISIBILITY_THROTTLE_MILLIS);
}
class VisibilityRunnable implements Runnable {
// Set of views that are visible or invisible. We create these once to avoid excessive
// garbage collection observed when calculating these on each pass.
private final ArrayList<View> mVisibleViews;
private final ArrayList<View> mInvisibleViews;
VisibilityRunnable() {
mInvisibleViews = new ArrayList<View>();
mVisibleViews = new ArrayList<View>();
}
@Override
public void run() {
try {
mIsVisibilityScheduled = false;
for (final Map.Entry<View, TrackingInfo> entry : mTrackedViews.entrySet()) {
final View view = entry.getKey();
final int minPercentageViewed = entry.getValue().mMinViewablePercent;
final int maxInvisiblePercent = entry.getValue().mMaxInvisiblePercent;
final View rootView = entry.getValue().mRootView;
if (mVisibilityChecker.isVisible(rootView, view, minPercentageViewed)) {
mVisibleViews.add(view);
} else if (!mVisibilityChecker.isVisible(rootView, view, maxInvisiblePercent)) {
mInvisibleViews.add(view);
}
}
if (mVisibilityTrackerListener != null) {
mVisibilityTrackerListener.onVisibilityChanged(mVisibleViews, mInvisibleViews);
}
// Clear these immediately so that we don't leak memory
mVisibleViews.clear();
mInvisibleViews.clear();
} catch (Exception e) {
e.printStackTrace();// added due to concurrent modification exception in weakhashmap
}
}
}
public static class VisibilityChecker {
// A rect to use for hit testing. Create this once to avoid excess garbage collection
private final Rect mClipRect = new Rect();
/**
* Whether the visible time has elapsed from the startPlayer time. Easily mocked for testing.
*/
boolean hasRequiredTimeElapsed(final long startTimeMillis, final int minTimeViewed) {
return SystemClock.uptimeMillis() - startTimeMillis >= minTimeViewed;
}
/**
* Whether the view is at least certain % visible
*/
public boolean isVisible(final View rootView, final View view, final int minPercentageViewed) {
return !(view == null || view.getVisibility() != View.VISIBLE ||
view.getParent() == null) && isViewVisible(view, minPercentageViewed);
}
public boolean isVisibleWithParent(final View rootView, final View view, final int minPercentageViewed) {
if (view == null || view.getVisibility() != View.VISIBLE ||
view.getParent() == null || rootView.getParent() == null) {
return false;
}
return isViewVisible(view, minPercentageViewed);
}
private boolean isViewVisible(View view, int minPercentageViewed) {
if (!view.getGlobalVisibleRect(mClipRect)) {
// Not visible
return false;
}
// % visible check - the cast is to avoid int overflow for large views.
final long visibleViewArea = (long) mClipRect.height() * mClipRect.width();
final long totalViewArea = (long) view.getHeight() * view.getWidth();
return totalViewArea > 0 && 100 * visibleViewArea >= minPercentageViewed * totalViewArea; }
}
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store