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}. *