diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityItem.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityItem.java
index 0bcb101ebf..cb368d4b0f 100644
--- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityItem.java
+++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityItem.java
@@ -45,6 +45,10 @@ class EpoxyVisibilityItem {
/** Store last value for de-duping */
private int lastVisibleSizeNotified = NOT_NOTIFIED;
+ EpoxyVisibilityItem(int adapterPosition) {
+ reset(adapterPosition);
+ }
+
/**
* Update the visibility item according the current layout.
*
@@ -173,5 +177,9 @@ private boolean checkAndUpdateUnfocusedVisible(boolean detachEvent) {
private boolean checkAndUpdateFullImpressionVisible() {
return fullyVisible = visibleSize == sizeInScrollingDirection;
}
+
+ void shiftBy(int offsetPosition) {
+ adapterPosition += offsetPosition;
+ }
}
diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.java
index f2d325e322..79cd33d2f3 100644
--- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.java
+++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyVisibilityTracker.java
@@ -3,13 +3,19 @@
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.support.v7.widget.RecyclerView.AdapterDataObserver;
import android.support.v7.widget.RecyclerView.OnChildAttachStateChangeListener;
import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* A simple way to track visibility events on {@link com.airbnb.epoxy.EpoxyModel} within a {@link
* android.support.v7.widget.RecyclerView}.
@@ -21,6 +27,7 @@
* Note regarding nested lists: The visibility event tracking is not properly handled yet. This is
* on the todo.
*
+ *
* @see OnVisibilityChanged
* @see OnVisibilityStateChanged
* @see OnModelVisibilityChangedListener
@@ -28,17 +35,32 @@
*/
public class EpoxyVisibilityTracker {
+ private static final String TAG = "EpoxyVisibilityTracker";
+
+ // Not actionable at runtime. It is only useful for internal test-troubleshooting.
+ static final boolean DEBUG_LOG = false;
+
/** Maintain visibility item indexed by view id (identity hashcode) */
private final SparseArray visibilityIdToItemMap = new SparseArray<>();
+ private final List visibilityIdToItems = new ArrayList<>();
/** listener used to process scroll, layout and attach events */
private final Listener listener = new Listener();
+ /** listener used to process data events */
+ private final DataObserver observer = new DataObserver();
+
@Nullable
private RecyclerView attachedRecyclerView = null;
+ @Nullable
+ private Adapter lastAdapterSeen = null;
private boolean onChangedEnabled = true;
+ /** This flag is for optimizing the process on detach. If detach is from data changed then it
+ * need to re-process all views, else no need (ex: scroll). */
+ private boolean visibleDataChanged = false;
+
/**
* Enable or disable visibility changed event. Default is `true`, disable it if you don't need
* (triggered by every pixel scrolled).
@@ -65,7 +87,7 @@ public void attach(@NonNull RecyclerView recyclerView) {
/**
* Detach the tracker
*
- * @param recyclerView The recyclerview that the EpoxyController has its adapter added to.
+ * @param recyclerView The recycler view that the EpoxyController has its adapter added to.
*/
public void detach(@NonNull RecyclerView recyclerView) {
recyclerView.removeOnScrollListener(this.listener);
@@ -74,47 +96,118 @@ public void detach(@NonNull RecyclerView recyclerView) {
attachedRecyclerView = null;
}
- private void processChildren() {
+ /**
+ * The tracker is storing visibility states internally and is using if to send events, only the
+ * difference is sent. Use this method to clear the states and thus regenerate the visibility
+ * events. This may be useful when you change the adapter on the {@link
+ * android.support.v7.widget.RecyclerView}
+ */
+ public void clearVisibilityStates() {
+ // Clear our visibility items
+ visibilityIdToItemMap.clear();
+ visibilityIdToItems.clear();
+ }
+
+ private void processChangeEvent(String debug) {
+ processChangeEventWithDetachedView(null, debug);
+ }
+
+ private void processChangeEventWithDetachedView(@Nullable View detachedView, String debug) {
final RecyclerView recyclerView = attachedRecyclerView;
if (recyclerView != null) {
+
+ // On every every events lookup for a new adapter
+ processNewAdapterIfNecessary();
+
+ // Process the detached child if any
+ if (detachedView != null) {
+ processChild(detachedView, true, debug);
+ }
+
+ // Process all attached children
+
for (int i = 0; i < recyclerView.getChildCount(); i++) {
final View child = recyclerView.getChildAt(i);
- if (child != null) {
- processChild(child);
+ if (child != null && child != detachedView) {
+ // Is some case the detached child is still in the recycler view. Don't process it as it
+ // was already processed.
+ processChild(child, false, debug);
}
}
}
}
- private void processChild(@NonNull View child) {
- processChild(child, false);
+ /**
+ * If there is a new adapter on the attached RecyclerView it will resister the data observer and
+ * clear the current visibility states
+ */
+ private void processNewAdapterIfNecessary() {
+ if (attachedRecyclerView != null && attachedRecyclerView.getAdapter() != null) {
+ if (lastAdapterSeen != attachedRecyclerView.getAdapter()) {
+ if (lastAdapterSeen != null) {
+ // Unregister the old adapter
+ lastAdapterSeen.unregisterAdapterDataObserver(this.observer);
+ }
+ // Register the new adapter
+ attachedRecyclerView.getAdapter().registerAdapterDataObserver(this.observer);
+ lastAdapterSeen = attachedRecyclerView.getAdapter();
+ }
+ }
}
- private void processChild(@NonNull View child, boolean detachEvent) {
+ /**
+ * Don't call this method directly, it is called from
+ * {@link EpoxyVisibilityTracker#processVisibilityEvents}
+ *
+ * @param child the view to process for visibility event
+ * @param detachEvent true if the child was just detached
+ * @param eventOriginForDebug a debug strings used for logs
+ */
+ private void processChild(@NonNull View child, boolean detachEvent, String eventOriginForDebug) {
final RecyclerView recyclerView = attachedRecyclerView;
if (recyclerView != null) {
- recyclerView.getChildViewHolder(child);
final ViewHolder holder = recyclerView.getChildViewHolder(child);
if (holder instanceof EpoxyViewHolder) {
- processVisibilityEvents(recyclerView, (EpoxyViewHolder) holder,
- recyclerView.getLayoutManager().canScrollVertically(), detachEvent);
- } else throw new IllegalEpoxyUsage(
- "`EpoxyVisibilityTracker` cannot be used with non-epoxy view holders."
- );
+ processVisibilityEvents(
+ recyclerView,
+ (EpoxyViewHolder) holder,
+ recyclerView.getLayoutManager().canScrollVertically(),
+ detachEvent,
+ eventOriginForDebug
+ );
+ } else {
+ throw new IllegalEpoxyUsage(
+ "`EpoxyVisibilityTracker` cannot be used with non-epoxy view holders."
+ );
+ }
}
}
+ /**
+ * Call this methods every time something related to ui (scroll, layout, ...) or something related
+ * to data changed.
+ *
+ * @param recyclerView the recycler view
+ * @param epoxyHolder the {@link RecyclerView}
+ * @param vertical true if the scrolling is vertical
+ * @param detachEvent true if the event originated from a view detached from the
+ * recycler view
+ * @param eventOriginForDebug a debug strings used for logs
+ */
private void processVisibilityEvents(
@NonNull RecyclerView recyclerView,
@NonNull EpoxyViewHolder epoxyHolder,
- boolean vertical, boolean detachEvent
+ boolean vertical, boolean detachEvent,
+ String eventOriginForDebug
) {
- // TODO EpoxyVisibilityTrackerTest testInsertData / testInsertData are disabled as they fail as
- // insert/delete not properly handled in the tracker
-
- if (epoxyHolder.getAdapterPosition() == RecyclerView.NO_POSITION) {
- return;
+ if (DEBUG_LOG) {
+ Log.d(TAG, String.format("%s.processVisibilityEvents %s, %s, %s",
+ eventOriginForDebug,
+ System.identityHashCode(epoxyHolder),
+ detachEvent,
+ epoxyHolder.getAdapterPosition()
+ ));
}
final View itemView = epoxyHolder.itemView;
@@ -122,12 +215,13 @@ private void processVisibilityEvents(
EpoxyVisibilityItem vi = visibilityIdToItemMap.get(id);
if (vi == null) {
- vi = new EpoxyVisibilityItem();
+ // New view discovered, assign an EpoxyVisibilityItem
+ vi = new EpoxyVisibilityItem(epoxyHolder.getAdapterPosition());
visibilityIdToItemMap.put(id, vi);
- }
-
- if (vi.getAdapterPosition() != epoxyHolder.getAdapterPosition()) {
- // EpoxyVisibilityItem being re-used for a different position
+ visibilityIdToItems.add(vi);
+ } else if (epoxyHolder.getAdapterPosition() != RecyclerView.NO_POSITION
+ && vi.getAdapterPosition() != epoxyHolder.getAdapterPosition()) {
+ // EpoxyVisibilityItem being re-used for a different adapter position
vi.reset(epoxyHolder.getAdapterPosition());
}
@@ -155,22 +249,126 @@ public void onLayoutChange(
int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom
) {
- processChildren();
+ processChangeEvent("onLayoutChange");
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
- processChildren();
+ processChangeEvent("onScrolled");
}
@Override
public void onChildViewAttachedToWindow(View child) {
- processChild(child);
+ processChild(child, false, "onChildViewAttachedToWindow");
}
@Override
public void onChildViewDetachedFromWindow(View child) {
- processChild(child, true);
+ if (visibleDataChanged) {
+ // On detach event caused by data set changed we need to re-process all children because
+ // the removal caused the others views to changes.
+ processChangeEventWithDetachedView(child, "onChildViewDetachedFromWindow");
+ visibleDataChanged = false;
+ } else {
+ processChild(child, true, "onChildViewDetachedFromWindow");
+ }
+ }
+ }
+
+ /**
+ * The layout/scroll events are not enough to detect all sort of visibility changes. We also
+ * need to look at the data events from the adapter.
+ */
+ class DataObserver extends AdapterDataObserver {
+
+ /**
+ * Clear the current visibility statues
+ */
+ @Override
+ public void onChanged() {
+ if (DEBUG_LOG) {
+ Log.d(TAG, "onChanged()");
+ }
+ visibilityIdToItemMap.clear();
+ visibilityIdToItems.clear();
+ visibleDataChanged = true;
+ }
+
+ /**
+ * For all items after the inserted range shift each {@link EpoxyVisibilityTracker} adapter
+ * position by inserted item count.
+ */
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ if (DEBUG_LOG) {
+ Log.d(TAG, String.format("onItemRangeInserted(%d, %d)", positionStart, itemCount));
+ }
+ for (EpoxyVisibilityItem item : visibilityIdToItems) {
+ if (item.getAdapterPosition() >= positionStart) {
+ visibleDataChanged = true;
+ item.shiftBy(itemCount);
+ }
+ }
+ }
+
+ /**
+ * For all items after the removed range reverse-shift each {@link EpoxyVisibilityTracker}
+ * adapter position by removed item count
+ */
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ if (DEBUG_LOG) {
+ Log.d(TAG, String.format("onItemRangeRemoved(%d, %d)", positionStart, itemCount));
+ }
+ for (EpoxyVisibilityItem item : visibilityIdToItems) {
+ if (item.getAdapterPosition() >= positionStart) {
+ visibleDataChanged = true;
+ item.shiftBy(-itemCount);
+ }
+ }
+ }
+
+ /**
+ * This is a bit more complex, for move we need to first swap the moved position then shift the
+ * items between the swap. To simplify we split any range passed to individual item moved.
+ *
+ * ps: anyway {@link android.support.v7.util.AdapterListUpdateCallback} does not seem to use
+ * range for moved items.
+ */
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ for (int i = 0; i < itemCount; i++) {
+ onItemMoved(fromPosition + i, toPosition + i);
+ }
+ }
+
+ private void onItemMoved(int fromPosition, int toPosition) {
+ if (DEBUG_LOG) {
+ Log.d(TAG,
+ String.format("onItemRangeMoved(%d, %d, %d)", fromPosition, toPosition, 1));
+ }
+ for (EpoxyVisibilityItem item : visibilityIdToItems) {
+ int position = item.getAdapterPosition();
+ if (position == fromPosition) {
+ // We found the item to be moved, just swap the position.
+ item.shiftBy(toPosition - fromPosition);
+ visibleDataChanged = true;
+ } else if (fromPosition < toPosition) {
+ // Item will be moved down in the list
+ if (position > fromPosition && position <= toPosition) {
+ // Item is between the moved from and to indexes, it should move up
+ item.shiftBy(-1);
+ visibleDataChanged = true;
+ }
+ } else if (fromPosition > toPosition) {
+ // Item will be moved up in the list
+ if (position >= toPosition && position < fromPosition) {
+ // Item is between the moved to and from indexes, it should move down
+ item.shiftBy(1);
+ visibleDataChanged = true;
+ }
+ }
+ }
}
}
}
diff --git a/epoxy-adapter/src/test/java/com/airbnb/epoxy/EpoxyVisibilityTrackerTest.kt b/epoxy-adapter/src/test/java/com/airbnb/epoxy/EpoxyVisibilityTrackerTest.kt
index 8db083b28e..3a7455feab 100644
--- a/epoxy-adapter/src/test/java/com/airbnb/epoxy/EpoxyVisibilityTrackerTest.kt
+++ b/epoxy-adapter/src/test/java/com/airbnb/epoxy/EpoxyVisibilityTrackerTest.kt
@@ -3,10 +3,12 @@ package com.airbnb.epoxy
import android.app.Activity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
+import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
+import com.airbnb.epoxy.EpoxyVisibilityTracker.DEBUG_LOG
import com.airbnb.epoxy.VisibilityState.FOCUSED_VISIBLE
import com.airbnb.epoxy.VisibilityState.FULL_IMPRESSION_VISIBLE
import com.airbnb.epoxy.VisibilityState.INVISIBLE
@@ -19,6 +21,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowLog
import java.lang.StringBuilder
/**
@@ -33,13 +36,15 @@ class EpoxyVisibilityTrackerTest {
companion object {
+ private const val TAG = "EpoxyVisibilityTrackerTest"
+
/**
* Make sure the RecyclerView display:
* - 2 full items
* - 50% of the next item.
*/
- private const val VISIBLE_ITEMS = 2.5
- private val FULLY_VISIBLE_ITEMS = Math.floor(VISIBLE_ITEMS).toInt()
+ private const val TWO_AND_HALF_VISIBLE = 2.5f
+
private val ALL_STATES = intArrayOf(
VISIBLE,
INVISIBLE,
@@ -48,9 +53,10 @@ class EpoxyVisibilityTrackerTest {
FULL_IMPRESSION_VISIBLE
)
- private const val DEBUG_LOG = true
private fun log(message: String) {
- if (DEBUG_LOG) System.out.println(message)
+ if (DEBUG_LOG) {
+ Log.d(TAG, message)
+ }
}
private var ids = 0
@@ -69,9 +75,9 @@ class EpoxyVisibilityTrackerTest {
*/
@Test
fun testDataAttachedToRecyclerView() {
- val testHelper = buildTestData(10)
+ val testHelper = buildTestData(10, TWO_AND_HALF_VISIBLE)
- val firstHalfVisibleItem = FULLY_VISIBLE_ITEMS
+ val firstHalfVisibleItem = 2
val firstInvisibleItem = firstHalfVisibleItem + 1
// Verify visibility event
@@ -135,13 +141,13 @@ class EpoxyVisibilityTrackerTest {
}
/**
- * Test visibility events when loading a recycler view
+ * Test visibility events when adding data to a recycler view (item inserted from adapter)
*/
- // @Test TODO make insert works
+ @Test
fun testInsertData() {
// Build initial list
- val testHelper = buildTestData(10)
+ val testHelper = buildTestData(10, TWO_AND_HALF_VISIBLE)
val secondFullyVisibleItemBeforeInsert = testHelper[1]
val halfVisibleItemBeforeInsert = testHelper[2]
@@ -173,6 +179,7 @@ class EpoxyVisibilityTrackerTest {
visitedStates = intArrayOf(
VISIBLE,
FOCUSED_VISIBLE,
+ UNFOCUSED_VISIBLE,
FULL_IMPRESSION_VISIBLE
)
)
@@ -193,13 +200,13 @@ class EpoxyVisibilityTrackerTest {
}
/**
- * Test visibility events when loading a recycler view
+ * Test visibility events when removing data from a recycler view (item removed from adapter)
*/
- // @Test TODO make delete works
+ @Test
fun testDeleteData() {
// Build initial list
- val testHelper = buildTestData(10)
+ val testHelper = buildTestData(10, TWO_AND_HALF_VISIBLE)
val halfVisibleItemBeforeDelete = testHelper[2]
val firstNonVisibleItemBeforeDelete = testHelper[3]
@@ -242,18 +249,173 @@ class EpoxyVisibilityTrackerTest {
}
}
+ /**
+ * Test visibility events when moving data from a recycler view (item moved within adapter)
+ *
+ * This test is a bit more complex so we will add more data in the sample size so we can test
+ * moving a range.
+ *
+ * What is done :
+ * - build a test adapter with a larger sample : 100 items (20 items per screen)
+ * - make sure first item is in focus
+ * - move the 2 first items to the position 14
+ * - make sure recycler view is still displaying the focused item (scrolled to ~14)
+ * - make sure the 3rd item is not visible
+ */
+ @Test
+ fun testMoveDataUp() {
+
+ val llm = recyclerView.layoutManager as LinearLayoutManager
+
+ // Build initial list
+ val itemsPerScreen = 20
+ val testHelper = buildTestData(100, itemsPerScreen.toFloat())
+
+ // First item should be visible and in focus
+ Assert.assertEquals(0, llm.findFirstCompletelyVisibleItemPosition())
+ Assert.assertEquals(20, llm.findLastVisibleItemPosition())
+
+ // Move the 2 first items to the position 24
+ val moved1 = testHelper[0]
+ val moved2 = testHelper[1]
+ val item3 = testHelper[2]
+ moveTwoItems(testHelper, from = 0, to = 14)
+
+ // Because we moved the item in focus (item 0) and the layout manager will maintain the
+ // focus the recycler view should scroll to end
+
+ Assert.assertEquals(14, llm.findFirstVisibleItemPosition())
+ Assert.assertEquals(14 + itemsPerScreen - 1, llm.findLastCompletelyVisibleItemPosition())
+
+ with(moved1) {
+ // moved 1 should still be in focus so still 100% visible
+ assert(
+ visibleHeight = itemHeight,
+ percentVisibleHeight = 100.0f,
+ visible = true,
+ fullImpression = true,
+ visitedStates = intArrayOf(
+ VISIBLE,
+ FOCUSED_VISIBLE,
+ FULL_IMPRESSION_VISIBLE
+ )
+ )
+ }
+
+ with(moved2) {
+ // moved 2 should still be in focus so still 100% visible
+ assert(
+ visibleHeight = itemHeight,
+ percentVisibleHeight = 100.0f,
+ visible = true,
+ fullImpression = true,
+ visitedStates = intArrayOf(
+ VISIBLE,
+ FOCUSED_VISIBLE,
+ FULL_IMPRESSION_VISIBLE
+ )
+ )
+ }
+
+ with(item3) {
+ // the item after moved2 should not be visible now
+ assert(
+ visibleHeight = 0,
+ percentVisibleHeight = 0.0f,
+ visible = false,
+ fullImpression = false,
+ visitedStates = ALL_STATES
+ )
+ }
+ }
+
+ /**
+ * Same kind of test than `testMoveDataUp()` but we move from 24 to 0.
+ */
+ @Test
+ fun testMoveDataDown() {
+
+ val llm = recyclerView.layoutManager as LinearLayoutManager
+
+ // Build initial list
+ val itemsPerScreen = 20
+ val testHelper = buildTestData(100, itemsPerScreen.toFloat())
+
+ // Scroll to item 24, sharp
+ (recyclerView.layoutManager as LinearLayoutManager)
+ .scrollToPositionWithOffset(24, 0)
+
+ // First item should be visible and in focus
+ Assert.assertEquals(24, llm.findFirstCompletelyVisibleItemPosition())
+ Assert.assertEquals(44, llm.findLastVisibleItemPosition())
+
+ // Move the 2 first items to the position 24
+ val moved1 = testHelper[24]
+ val moved2 = testHelper[25]
+ val item3 = testHelper[26]
+ moveTwoItems(testHelper, from = 24, to = 0)
+
+ // Because we moved the item in focus (item 0) and the layout manager will maintain the
+ // focus the recycler view should scroll to end
+
+ Assert.assertEquals(0, llm.findFirstVisibleItemPosition())
+ Assert.assertEquals(19, llm.findLastCompletelyVisibleItemPosition())
+
+ with(moved1) {
+ // moved 1 should still be in focus so still 100% visible
+ assert(
+ visibleHeight = itemHeight,
+ percentVisibleHeight = 100.0f,
+ visible = true,
+ fullImpression = true,
+ visitedStates = intArrayOf(
+ VISIBLE,
+ FOCUSED_VISIBLE,
+ FULL_IMPRESSION_VISIBLE
+ )
+ )
+ }
+
+ with(moved2) {
+ // moved 2 should still be in focus so still 100% visible
+ assert(
+ visibleHeight = itemHeight,
+ percentVisibleHeight = 100.0f,
+ visible = true,
+ fullImpression = true,
+ visitedStates = intArrayOf(
+ VISIBLE,
+ FOCUSED_VISIBLE,
+ FULL_IMPRESSION_VISIBLE
+ )
+ )
+ }
+
+ with(item3) {
+ // the item after moved2 should not be visible now
+ assert(
+ visibleHeight = 0,
+ percentVisibleHeight = 0.0f,
+ visible = false,
+ fullImpression = false,
+ visitedStates = ALL_STATES
+ )
+ }
+ }
+
+
/**
* Test visibility events using scrollToPosition on the recycler view
*/
@Test
fun testScrollBy() {
- val testHelper = buildTestData(10)
+ val testHelper = buildTestData(10, TWO_AND_HALF_VISIBLE)
// At this point we have the 1st and 2nd item visible
// The 3rd item is 50% visible
// Now scroll to the end
- for(to in 0..testHelper.size) {
+ for (to in 0..testHelper.size) {
(recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(to, 10)
}
@@ -358,7 +520,7 @@ class EpoxyVisibilityTrackerTest {
*/
@Test
fun testScrollToPosition() {
- val testHelper = buildTestData(10)
+ val testHelper = buildTestData(10, TWO_AND_HALF_VISIBLE)
// At this point we have the 1st and 2nd item visible
// The 3rd item is 50% visible
@@ -468,11 +630,14 @@ class EpoxyVisibilityTrackerTest {
/**
* Attach an EpoxyController on the RecyclerView
*/
- private fun buildTestData(sampleSize: Int): MutableList {
- // Build a test sample of 0 items
+ private fun buildTestData(sampleSize: Int, visibleItemsOnScreen: Float): MutableList {
+ // Compute individual item height
+ itemHeight = (recyclerView.measuredHeight / visibleItemsOnScreen).toInt()
+ // Build a test sample of sampleSize items
val helpers = mutableListOf().apply {
for (index in 0 until sampleSize) add(AssertHelper(ids++))
}
+ log(helpers.ids())
epoxyController.setData(helpers)
return helpers
}
@@ -481,6 +646,7 @@ class EpoxyVisibilityTrackerTest {
log("insert at $position")
val helper = AssertHelper(ids++)
helpers.add(position, helper)
+ log(helpers.ids())
epoxyController.setData(helpers)
return helper
}
@@ -488,10 +654,21 @@ class EpoxyVisibilityTrackerTest {
private fun deleteAt(helpers: MutableList, position: Int): AssertHelper {
log("delete at $position")
val helper = helpers.removeAt(position)
+ log(helpers.ids())
epoxyController.setData(helpers)
return helper
}
+ private fun moveTwoItems(helpers: MutableList, from: Int, to: Int) {
+ log("move at $from -> $to")
+ val helper1 = helpers.removeAt(from)
+ val helper2 = helpers.removeAt(from)
+ helpers.add(to, helper2)
+ helpers.add(to, helper1)
+ log(helpers.ids())
+ epoxyController.setData(helpers)
+ }
+
/**
* Setup a RecyclerView and compute item height so we have 3.5 items on screen
*/
@@ -512,9 +689,9 @@ class EpoxyVisibilityTrackerTest {
recyclerView.adapter = epoxyController.adapter
})
viewportHeight = recyclerView.measuredHeight
- itemHeight = (recyclerView.measuredHeight / VISIBLE_ITEMS).toInt()
activity = this
}
+ ShadowLog.stream = System.out
}
@After
@@ -637,6 +814,17 @@ class EpoxyVisibilityTrackerTest {
}
+private fun List.ids(): String {
+ val builder = StringBuilder("[")
+ forEachIndexed { index, element ->
+ (element as? EpoxyVisibilityTrackerTest.AssertHelper)?.let {
+ builder.append(it.id)
+ }
+ builder.append(if (index < size - 1) "," else "]")
+ }
+ return builder.toString()
+}
+
/**
* List of Int to VisibilityState constant names.
*/
diff --git a/epoxy-annotations/src/main/java/com/airbnb/epoxy/OnVisibilityChanged.java b/epoxy-annotations/src/main/java/com/airbnb/epoxy/OnVisibilityChanged.java
index f11cde12eb..ace3feeb74 100644
--- a/epoxy-annotations/src/main/java/com/airbnb/epoxy/OnVisibilityChanged.java
+++ b/epoxy-annotations/src/main/java/com/airbnb/epoxy/OnVisibilityChanged.java
@@ -10,10 +10,14 @@
* with this annotation will be called when visibility part of the view change.
*
* Annotated methods should follow this signature :
- * `@OnVisibilityStateChange
- * public void method(@VisibilityState int state)`
+ * `@OnVisibilityChanged
+ * public void method(
+ * float percentVisibleHeight, float percentVisibleWidth: Float,
+ * int visibleHeight, int visibleWidth
+ * )`
+ *
+ * The equivalent methods on the model is {@link com.airbnb.epoxy.EpoxyModel#onVisibilityChanged}
*
- * The equivalent methods on the model is {@link EpoxyModel#onVisibilityChanged}
* @see OnModelVisibilityChangedListener
*/
@Target(ElementType.METHOD)
diff --git a/epoxy-annotations/src/main/java/com/airbnb/epoxy/OnVisibilityStateChanged.java b/epoxy-annotations/src/main/java/com/airbnb/epoxy/OnVisibilityStateChanged.java
index 3bec2b5661..abf8450025 100644
--- a/epoxy-annotations/src/main/java/com/airbnb/epoxy/OnVisibilityStateChanged.java
+++ b/epoxy-annotations/src/main/java/com/airbnb/epoxy/OnVisibilityStateChanged.java
@@ -10,11 +10,8 @@
* with this annotation will be called when the visibility state is changed.
*
* Annotated methods should follow this signature :
- * `@OnVisibilityStateChanged
- * public void method(
- * float percentVisibleHeight, float percentVisibleWidth: Float,
- * int visibleHeight, int visibleWidth
- * )`
+ * `@OnVisibilityStateChange
+ * public void method(@VisibilityState int state)`
*
* Possible States are declared in {@link com.airbnb.epoxy.OnModelVisibilityStateChangedListener}.
*