Skip to content

Instantly share code, notes, and snippets.

@caseykulm
Last active August 4, 2016 16:08
Show Gist options
  • Save caseykulm/0d51ef4d4979be176e89bcc84220c10d to your computer and use it in GitHub Desktop.
Save caseykulm/0d51ef4d4979be176e89bcc84220c10d to your computer and use it in GitHub Desktop.
New and improved helper class to determine when view enter/exits the visible viewport
public class EventView {
public interface EventViewListener<ViewType extends View> {
/**
* @param onScreen When the view enters/exits the viewport. Not to be confused with {@link View#VISIBLE}.
*/
void onVisibleOnScreenStateChanged(boolean onScreen);
/**
* Verbose method that is called every time the view hierarchy has the potential to
* move this view in/out of the viewport. Useful for debugging, but will be called
* often so make sure not to call anything expensive here.
*
* @param viewType The view that all view state events are listening to
* @param onScreen Whether the view is currently in the viewport or not
*/
void onProcessEvent(ViewType viewType, boolean onScreen);
/**
* Called whenever a view enters the viewport. Will also be called on the same view,
* if the id associated with it is different than the last time the view entered the
* viewport.
*/
void onFirstTimeOnScreen();
}
public static <ViewType extends View> void bind(ViewType view, EventViewListener<ViewType> listener, String id) {
EventView.ViewsMovingListener<ViewType> viewsMovingListener = new EventView.ViewsMovingListener<ViewType>(view, listener, id);
view.getViewTreeObserver().addOnGlobalLayoutListener(viewsMovingListener);
view.getViewTreeObserver().addOnScrollChangedListener(viewsMovingListener);
}
static class Subject<ViewType extends View> {
private ViewType bvView;
private EventViewListener<ViewType> eventViewListener;
private final Rect rect;
private final Point globalOffset;
private boolean lastOnScreenState = false;
private final String id;
Subject(ViewType view, EventViewListener<ViewType> eventViewListener, String id) {
this.bvView = view;
this.eventViewListener = eventViewListener;
this.rect = new Rect();
this.globalOffset = new Point();
this.id = id;
}
void processEvent() {
boolean onScreen = eventViewVisible();
eventViewListener.onProcessEvent(bvView, onScreen);
if (onScreen != lastOnScreenState) {
eventViewListener.onVisibleOnScreenStateChanged(onScreen);
}
if (onScreen && !seenOnScreenWithId(bvView, id)) {
eventViewListener.onFirstTimeOnScreen();
}
lastOnScreenState = onScreen;
}
private boolean eventViewVisible() {
if (bvView == null) {
return false;
}
boolean isVisibleOnScreen = bvView.getGlobalVisibleRect(rect, globalOffset);
return isVisibleOnScreen;
}
}
static class ViewUtil {
/**
* Only call this when the View was seen in the viewport with an associated id
*
* @param view The View that will be tracked
* @param id A unique id to associate with this view
* @param <ViewType> The type of View child class
* @return Whether this View+Id has already been seen in the viewport
*/
static <ViewType extends View> boolean seenOnScreenWithId(ViewType view, String id) {
Object seenIdsForViewObj = view.getTag(R.string.seen_product_onscreen_with_id_set);
if (seenIdsForViewObj == null) {
Set<String> seenIdsForView = new HashSet<String>();
updateSeenId(seenIdsForView, view, id);
return false;
} else if (seenIdsForViewObj instanceof Set) {
Set<String> seenIdsForView = (Set<String>) seenIdsForViewObj;
if (seenIdsForView.contains(id)) {
return true;
} else {
updateSeenId(seenIdsForView, view, id);
return false;
}
} else {
throw new IllegalStateException("ids associated with view are not in expected state");
}
}
static <ViewType extends View> void updateSeenId(Set<String> seenIdsForView, ViewType view, String id) {
Log.d(EventView.class.getSimpleName(), "onProcessEvent() - First time seeing " + ((TextView) view).getText() + " on screen!");
seenIdsForView.add(id);
view.setTag(R.string.seen_product_onscreen_with_id_set, seenIdsForView);
}
}
static class ViewsMovingListener<ViewType extends View> implements ViewTreeObserver.OnScrollChangedListener, ViewTreeObserver.OnGlobalLayoutListener {
private final Subject<ViewType> eventViewSubject;
ViewsMovingListener(ViewType view, EventViewListener<ViewType> eventViewListener, String id) {
this.eventViewSubject = new Subject<ViewType>(view, eventViewListener, id);
}
@Override
public void onScrollChanged() {
eventViewSubject.processEvent();
}
@Override
public void onGlobalLayout() {
eventViewSubject.processEvent();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment