Skip to content

Commit

Permalink
Implements visibility events for EpoxyRecyclerView/EpoxyViewHolder. (#…
Browse files Browse the repository at this point in the history
…560)

* Implements visibility events for EpoxyRecyclerView/EpoxyViewHolder.

* checkstyle failures

* Fix existing tests

* Send visibleSize instead of size, prevent sending duplicate events.

* Remove useless private methods

* Add visibility events in sample app.

* Replace `@OnVisibilityEvent(event = ...)` by `@OnVisibilityChanged` and `@OnVisibilityStateChanged`
Add `OnModelVisibilityStateChangedListener` and `OnModelVisibilityChangedListener` interfaces

* Add the visibility listener to the model code generation

* Update test resources (not sure why but the ruby script reformatted some non-related lines)

* Send unfocused event on detach event

* Add float ranges

* Rename to sizeInScrollingDirection;

* Can be private

* typo

* Remove dependency to LinearLayoutManager

* Remove dependency to EpoxyRecyclerView. Make visibility tracker public.

* parameter names inverted

* Add toto regarding nested list (carousel)

* visibility...() -> onVisibility...()

* visibilityStateParam -> visibilityObjectParam

* Add optional checkTypeParameters in validateExecutableElement, use it for visibility annotations checking.

* throws if not instanceof EpoxyViewHolder

* javadoc + renaming on EpoxyVisibilityItem

* EpoxyModel<?> -> EpoxyModel<V>

* Javadoc, links to the epoxy model methods

* Remove mention to "You may clear the listener by..." in javadoc

* More javadoc

* Move state constants to top level class VisibilityState

* Unused import

* Add 3 tests : load RV, scroll to, scroll by.

* Fix bugs (thanks to unit tests)

* Missing annotations (FloatRange, Px)

* Tests for @ModelView's @OnVisibilityChanged @OnVisibilityStateChanged codegen

* Rename `is...()` to `checkAndUpdate...()`

* Use constant

* Add basic javadoc to EpoxyModel + todo

* Prepare unit test for insert/delete case (disabled as failing)

* remove dup comment
  • Loading branch information
eboudrant authored and elihart committed Oct 12, 2018
1 parent 9eb4fc6 commit bb634e8
Show file tree
Hide file tree
Showing 126 changed files with 8,717 additions and 332 deletions.
2 changes: 2 additions & 0 deletions epoxy-adapter/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdkVersion rootProject.COMPILE_SDK_VERSION
Expand All @@ -25,6 +26,7 @@ dependencies {

annotationProcessor project(':epoxy-processor')

testImplementation rootProject.deps.kotlin
testCompile rootProject.deps.junit
testCompile rootProject.deps.robolectric
testCompile rootProject.deps.mockito
Expand Down
23 changes: 23 additions & 0 deletions epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModel.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.airbnb.epoxy;

import android.support.annotation.FloatRange;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.airbnb.epoxy.EpoxyController.ModelInterceptorCallback;
import com.airbnb.epoxy.VisibilityState.Visibility;

import java.util.List;

Expand Down Expand Up @@ -155,6 +158,26 @@ public void bind(@NonNull T view, @NonNull EpoxyModel<?> previouslyBoundModel) {
public void unbind(@NonNull T view) {
}

/**
* TODO link to the wiki
* @see OnVisibilityStateChanged annotation
*/
public void onVisibilityStateChanged(@Visibility int visibilityState, @NonNull T view) {
}

/**
* TODO link to the wiki
* @see OnVisibilityChanged annotation
*/
public void onVisibilityChanged(
@FloatRange(from = 0.0f, to = 100.0f) float percentVisibleHeight,
@FloatRange(from = 0.0f, to = 100.0f) float percentVisibleWidth,
@Px int visibleHeight,
@Px int visibleWidth,
@NonNull T view
) {
}

public long id() {
return id;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.airbnb.epoxy;

import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;
import android.support.annotation.Px;

import com.airbnb.epoxy.VisibilityState.Visibility;

import java.util.List;

Expand Down Expand Up @@ -40,6 +44,24 @@ public void unbind(@NonNull T holder) {
super.unbind(holder);
}


@Override
public void onVisibilityStateChanged(@Visibility int visibilityState, @NonNull T view) {
super.onVisibilityStateChanged(visibilityState, view);
}

@Override
public void onVisibilityChanged(
@FloatRange(from = 0, to = 100) float percentVisibleHeight,
@FloatRange(from = 0, to = 100) float percentVisibleWidth,
@Px int visibleHeight, @Px int visibleWidth,
@NonNull T view) {
super.onVisibilityChanged(
percentVisibleHeight, percentVisibleWidth,
visibleHeight, visibleWidth,
view);
}

@Override
public boolean onFailedToRecycleView(T holder) {
return super.onFailedToRecycleView(holder);
Expand Down
22 changes: 22 additions & 0 deletions epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyViewHolder.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@

package com.airbnb.epoxy;

import android.support.annotation.FloatRange;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.support.v7.widget.RecyclerView;
import android.view.View;

import com.airbnb.epoxy.ViewHolderState.ViewState;
import com.airbnb.epoxy.VisibilityState.Visibility;

import java.util.List;

Expand Down Expand Up @@ -82,6 +85,25 @@ public void unbind() {
payloads = null;
}

public void visibilityStateChanged(@Visibility int visibilityState) {
assertBound();
// noinspection unchecked
epoxyModel.onVisibilityStateChanged(visibilityState, objectToBind());
}

public void visibilityChanged(
@FloatRange(from = 0.0f, to = 100.0f) float percentVisibleHeight,
@FloatRange(from = 0.0f, to = 100.0f) float percentVisibleWidth,
@Px int visibleHeight,
@Px int visibleWidth
) {
assertBound();
// noinspection unchecked
epoxyModel.onVisibilityChanged(percentVisibleHeight, percentVisibleWidth, visibleHeight,
visibleWidth, objectToBind());
}


public List<Object> getPayloads() {
assertBound();
return payloads;
Expand Down
177 changes: 177 additions & 0 deletions epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityItem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.airbnb.epoxy;

import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.annotation.Px;
import android.support.v7.widget.RecyclerView;
import android.view.View;

/**
* This class represent an item in the {@link android.support.v7.widget.RecyclerView} and it is
* being reused with multiple model via the update method. There is 1:1 relationship between an
* EpoxyVisibilityItem and a child within the {@link android.support.v7.widget.RecyclerView}.
*
* It contains the logic to compute the visibility state of an item. It will also invoke the
* visibility callbacks on {@link com.airbnb.epoxy.EpoxyViewHolder}
*
* This class should remain non-public and is intended to be used by {@link EpoxyVisibilityTracker}
* only.
*/
class EpoxyVisibilityItem {

private static final int NOT_NOTIFIED = -1;

private final Rect localVisibleRect = new Rect();

private int adapterPosition = RecyclerView.NO_POSITION;

@Px
private int sizeInScrollingDirection;

private int sizeNotInScrollingDirection;

private boolean verticalScrolling;

private float percentVisibleSize = 0.f;

private int visibleSize;

private int viewportSize;

private boolean fullyVisible = false;
private boolean visible = false;
private boolean focusedVisible = false;

/** Store last value for de-duping */
private int lastVisibleSizeNotified = NOT_NOTIFIED;

/**
* Update the visibility item according the current layout.
*
* @param view the current {@link com.airbnb.epoxy.EpoxyViewHolder}'s itemView
* @param parent the {@link android.support.v7.widget.RecyclerView}
* @param vertical true if it scroll vertically
* @return true if the view has been measured
*/
boolean update(@NonNull View view, @NonNull RecyclerView parent,
boolean vertical, boolean detachEvent) {
view.getLocalVisibleRect(localVisibleRect);
this.verticalScrolling = vertical;
if (vertical) {
sizeInScrollingDirection = view.getMeasuredHeight();
sizeNotInScrollingDirection = view.getMeasuredWidth();
viewportSize = parent.getMeasuredHeight();
visibleSize = detachEvent ? 0 : localVisibleRect.height();
} else {
sizeNotInScrollingDirection = view.getMeasuredHeight();
sizeInScrollingDirection = view.getMeasuredWidth();
viewportSize = parent.getMeasuredWidth();
visibleSize = detachEvent ? 0 : localVisibleRect.width();
}
percentVisibleSize = detachEvent ? 0 : 100.f / sizeInScrollingDirection * visibleSize;
if (visibleSize != sizeInScrollingDirection) {
fullyVisible = false;
}
return sizeInScrollingDirection > 0;
}

int getAdapterPosition() {
return adapterPosition;
}

void reset(int newAdapterPosition) {
fullyVisible = false;
visible = false;
focusedVisible = false;
adapterPosition = newAdapterPosition;
lastVisibleSizeNotified = NOT_NOTIFIED;
}

void handleVisible(@NonNull EpoxyViewHolder epoxyHolder, boolean detachEvent) {
if (visible && checkAndUpdateInvisible(detachEvent)) {
epoxyHolder.visibilityStateChanged(VisibilityState.INVISIBLE);
} else if (!visible && checkAndUpdateVisible()) {
epoxyHolder.visibilityStateChanged(VisibilityState.VISIBLE);
}
}

void handleFocus(EpoxyViewHolder epoxyHolder, boolean detachEvent) {
if (focusedVisible && checkAndUpdateUnfocusedVisible(detachEvent)) {
epoxyHolder.visibilityStateChanged(VisibilityState.UNFOCUSED_VISIBLE);
} else if (!focusedVisible && checkAndUpdateFocusedVisible()) {
epoxyHolder.visibilityStateChanged(VisibilityState.FOCUSED_VISIBLE);
}
}

void handleFullImpressionVisible(EpoxyViewHolder epoxyHolder, boolean detachEvent) {
if (!fullyVisible && checkAndUpdateFullImpressionVisible()) {
epoxyHolder
.visibilityStateChanged(VisibilityState.FULL_IMPRESSION_VISIBLE);
}
}

void handleChanged(EpoxyViewHolder epoxyHolder) {
if (visibleSize != lastVisibleSizeNotified) {
if (verticalScrolling) {
epoxyHolder.visibilityChanged(percentVisibleSize, 100.f, visibleSize,
sizeNotInScrollingDirection);
} else {
epoxyHolder.visibilityChanged(100.f, percentVisibleSize,
sizeNotInScrollingDirection, visibleSize);
}
lastVisibleSizeNotified = visibleSize;
}
}

/**
* @return true when at least one pixel of the component is visible
*/
private boolean checkAndUpdateVisible() {
return visible = visibleSize > 0;
}

/**
* @param detachEvent true if initiated from detach event
* @return true when when the component no longer has any pixels on the screen
*/
private boolean checkAndUpdateInvisible(boolean detachEvent) {
boolean invisible = visibleSize <= 0 || detachEvent;
if (invisible) {
visible = false;
}
return !visible;
}

/**
* @return true when either the component occupies at least half of the viewport, or, if the
* component is smaller than half the viewport, when it is fully visible.
*/
private boolean checkAndUpdateFocusedVisible() {
return focusedVisible =
sizeInScrollingDirection >= viewportSize / 2 || (visibleSize == sizeInScrollingDirection
&& sizeInScrollingDirection < viewportSize / 2);
}

/**
* @param detachEvent true if initiated from detach event
* @return true when the component is no longer focused, i.e. it is not fully visible and does
* not occupy at least half the viewport.
*/
private boolean checkAndUpdateUnfocusedVisible(boolean detachEvent) {
boolean unfocusedVisible = detachEvent
|| !(sizeInScrollingDirection >= viewportSize / 2 || (
visibleSize == sizeInScrollingDirection && sizeInScrollingDirection < viewportSize / 2));
if (unfocusedVisible) {
focusedVisible = false;
}
return !focusedVisible;
}

/**
* @return true when the entire component has passed through the viewport at some point.
*/
private boolean checkAndUpdateFullImpressionVisible() {
return fullyVisible = visibleSize == sizeInScrollingDirection;
}
}

Loading

0 comments on commit bb634e8

Please sign in to comment.