From 2cde1bc6dfd4896fb9b86e205c33aa9858304e4e Mon Sep 17 00:00:00 2001 From: Gary Mathews Date: Wed, 16 Dec 2020 14:28:17 -0800 Subject: [PATCH] fix(android): implement missing scroll events for ListView and TableView --- .../ui/widget/listview/TiListView.java | 79 +++++++++++++++ .../widget/listview/TiNestedRecyclerView.java | 16 ++++ .../ui/widget/tableview/TiTableView.java | 96 +++++++++++++++++++ .../java/org/appcelerator/titanium/TiC.java | 20 ++++ 4 files changed, 211 insertions(+) diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java index f1447368d5b..0c264be1c37 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java @@ -33,6 +33,7 @@ import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import ti.modules.titanium.ui.widget.TiSwipeRefreshLayout; import ti.modules.titanium.ui.widget.searchbar.TiUISearchBar.OnSearchChangeListener; @@ -66,6 +67,49 @@ public TiListView(ListViewProxy proxy) this.recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); this.recyclerView.setFocusableInTouchMode(false); + // Add listener to fire scroll events. + this.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() + { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) + { + super.onScrollStateChanged(recyclerView, newState); + + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + proxy.fireSyncEvent(TiC.EVENT_SCROLLEND, generateScrollPayload()); + } + } + + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) + { + super.onScrolled(recyclerView, dx, dy); + + proxy.fireSyncEvent(TiC.EVENT_SCROLLSTART, generateScrollPayload()); + } + }); + this.recyclerView.setOnFlingListener(new RecyclerView.OnFlingListener() + { + @Override + public boolean onFling(int velocityX, int velocityY) + { + final KrollDict payload = new KrollDict(); + + // Determine scroll direction. + if (velocityY > 0) { + payload.put(TiC.PROPERTY_DIRECTION, "down"); + } else if (velocityY < 0) { + payload.put(TiC.PROPERTY_DIRECTION, "up"); + } + + // Set scroll velocity. + payload.put(TiC.EVENT_PROPERTY_VELOCITY, velocityY); + + proxy.fireSyncEvent(TiC.EVENT_SCROLLING, payload); + return true; + } + }); + // Disable list animations. this.recyclerView.setItemAnimator(null); @@ -192,6 +236,41 @@ public void filterBy(String query) this.isFiltered = true; } + /** + * Generate payload for `scrollstart` and `scrollend` events. + * + * @return KrollDict + */ + public KrollDict generateScrollPayload() + { + final KrollDict payload = new KrollDict(); + final LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); + + // Obtain first visible list item view. + final View firstVisibleView = + layoutManager.findViewByPosition(layoutManager.findFirstVisibleItemPosition()); + final ListViewHolder firstVisibleHolder = + (ListViewHolder) recyclerView.getChildViewHolder(firstVisibleView); + + // Obtain first visible list item proxy. + final ListItemProxy firstVisibleProxy = (ListItemProxy) firstVisibleHolder.getProxy(); + payload.put(TiC.PROPERTY_FIRST_VISIBLE_ITEM, firstVisibleProxy); + + // Obtain first visible list item index in section. + final int firstVisibleItemIndex = firstVisibleProxy.getIndexInSection(); + payload.put(TiC.PROPERTY_FIRST_VISIBLE_ITEM_INDEX, firstVisibleItemIndex); + + // Obtain first visible section proxy. + final ListSectionProxy firstVisibleSection = (ListSectionProxy) firstVisibleProxy.getParent(); + payload.put(TiC.PROPERTY_FIRST_VISIBLE_SECTION, firstVisibleSection); + + // Obtain first visible section index. + final int firstVisibleSectionIndex = proxy.getIndexOfSection(firstVisibleSection); + payload.put(TiC.PROPERTY_FIRST_VISIBLE_SECTION_INDEX, firstVisibleSectionIndex); + + return payload; + } + /** * Get list adapter. * diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiNestedRecyclerView.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiNestedRecyclerView.java index 04e942d8d2f..894c55f767e 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiNestedRecyclerView.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiNestedRecyclerView.java @@ -31,6 +31,9 @@ public class TiNestedRecyclerView extends RecyclerView implements NestedScrollin private boolean isScrollEnabled = true; + private float lastTouchX; + private float lastTouchY; + public TiNestedRecyclerView(@NonNull Context context) { super(new ContextThemeWrapper(context, R.style.RecyclerView)); @@ -46,6 +49,9 @@ public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEv switch (action) { case MotionEvent.ACTION_DOWN: { + lastTouchX = e.getX(); + lastTouchY = e.getY(); + if (scrollState == RecyclerView.SCROLL_STATE_SETTLING) { rv.stopScroll(); } @@ -86,6 +92,16 @@ public void setScrollEnabled(boolean enabled) this.isScrollEnabled = enabled; } + public float getLastTouchX() + { + return this.lastTouchX; + } + + public float getLastTouchY() + { + return this.lastTouchY; + } + @Override public boolean dispatchTouchEvent(MotionEvent ev) { diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java index 9ec33a8090b..c4a1c96eea1 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java @@ -12,6 +12,7 @@ import org.appcelerator.kroll.KrollDict; import org.appcelerator.titanium.TiApplication; import org.appcelerator.titanium.TiC; +import org.appcelerator.titanium.TiDimension; import org.appcelerator.titanium.util.TiUIHelper; import android.app.Activity; @@ -33,6 +34,7 @@ import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import ti.modules.titanium.ui.TableViewProxy; import ti.modules.titanium.ui.TableViewRowProxy; @@ -57,6 +59,8 @@ public class TiTableView extends TiSwipeRefreshLayout implements OnSearchChangeL private final SelectionTracker tracker; private boolean isFiltered = false; + private int scrollOffsetX = 0; + private int scrollOffsetY = 0; public TiTableView(TableViewProxy proxy) { @@ -70,6 +74,45 @@ public TiTableView(TableViewProxy proxy) this.recyclerView.setBackgroundColor(Color.TRANSPARENT); this.recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + // Add listener to fire scroll events. + this.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() + { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) + { + super.onScrollStateChanged(recyclerView, newState); + + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + final KrollDict payload = generateScrollPayload(); + final TiNestedRecyclerView nestedRecyclerView = getRecyclerView(); + + // Obtain last touch position for `scrollend` event. + final TiDimension xDimension = + new TiDimension(nestedRecyclerView.getLastTouchX(), TiDimension.TYPE_WIDTH); + final TiDimension yDimension = + new TiDimension(nestedRecyclerView.getLastTouchY(), TiDimension.TYPE_HEIGHT); + payload.put(TiC.EVENT_PROPERTY_X, xDimension.getAsDefault(nestedRecyclerView)); + payload.put(TiC.EVENT_PROPERTY_Y, yDimension.getAsDefault(nestedRecyclerView)); + + proxy.fireSyncEvent(TiC.EVENT_SCROLLEND, payload); + } + } + + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) + { + super.onScrolled(recyclerView, dx, dy); + + // Update scroll offsets. + scrollOffsetX += dx; + scrollOffsetY += dy; + + final KrollDict payload = generateScrollPayload(); + + proxy.fireSyncEvent(TiC.EVENT_SCROLL, payload); + } + }); + // Disable table animations. this.recyclerView.setItemAnimator(null); @@ -193,6 +236,59 @@ public void filterBy(String query) this.isFiltered = true; } + /** + * Generate payload for `scroll` and `scrollend` events. + * + * @return KrollDict + */ + public KrollDict generateScrollPayload() + { + final KrollDict payload = new KrollDict(); + final LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); + + // Obtain index for first visible row. + final View firstVisibleView = + layoutManager.findViewByPosition(layoutManager.findFirstVisibleItemPosition()); + final TableViewHolder firstVisibleHolder = + (TableViewHolder) recyclerView.getChildViewHolder(firstVisibleView); + final TableViewRowProxy firstVisibleProxy = (TableViewRowProxy) firstVisibleHolder.getProxy(); + final int firstVisibleIndex = firstVisibleProxy.getIndexInSection(); + payload.put(TiC.PROPERTY_FIRST_VISIBLE_ITEM, firstVisibleIndex); + + // Obtain scroll offset for content. + final KrollDict contentOffset = new KrollDict(); + final TiDimension scrollOffsetXDimension = new TiDimension(scrollOffsetX, TiDimension.TYPE_WIDTH); + final TiDimension scrollOffsetYDimension = new TiDimension(scrollOffsetY, TiDimension.TYPE_HEIGHT); + contentOffset.put(TiC.EVENT_PROPERTY_X, scrollOffsetXDimension.getAsDefault(recyclerView)); + contentOffset.put(TiC.EVENT_PROPERTY_Y, scrollOffsetYDimension.getAsDefault(recyclerView)); + payload.put(TiC.PROPERTY_CONTENT_OFFSET, contentOffset); + + // Approximate content size. + // NOTE: Due to recycling of views, we cannot calculate the true + // content size without loading all rows. The best we can do is an + // approximation based on first visible row. + final KrollDict contentSize = new KrollDict(); + final TiDimension contentWidthDimension = + new TiDimension(firstVisibleView.getMeasuredWidth(), TiDimension.TYPE_WIDTH); + final TiDimension contentHeightDimension = + new TiDimension(firstVisibleView.getMeasuredHeight() * rows.size(), TiDimension.TYPE_HEIGHT); + contentSize.put(TiC.PROPERTY_WIDTH, contentWidthDimension.getAsDefault(recyclerView)); + contentSize.put(TiC.PROPERTY_HEIGHT, contentHeightDimension.getAsDefault(recyclerView)); + payload.put(TiC.PROPERTY_CONTENT_SIZE, contentSize); + + // Obtain view size. + final KrollDict size = new KrollDict(); + final TiDimension widthDimension = + new TiDimension(recyclerView.getMeasuredWidth(), TiDimension.TYPE_WIDTH); + final TiDimension heightDimension = + new TiDimension(recyclerView.getMeasuredHeight(), TiDimension.TYPE_HEIGHT); + size.put(TiC.PROPERTY_WIDTH, widthDimension.getAsDefault(recyclerView)); + size.put(TiC.PROPERTY_HEIGHT, heightDimension.getAsDefault(recyclerView)); + payload.put(TiC.PROPERTY_SIZE, size); + + return payload; + } + /** * Get table adapter. * diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiC.java b/android/titanium/src/java/org/appcelerator/titanium/TiC.java index 60a3ec9c2b5..aa17b6776aa 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiC.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiC.java @@ -1742,6 +1742,26 @@ public class TiC */ public static final String PROPERTY_FIRSTNAME = "firstName"; + /** + * @module.api + */ + public static final String PROPERTY_FIRST_VISIBLE_ITEM = "firstVisibleItem"; + + /** + * @module.api + */ + public static final String PROPERTY_FIRST_VISIBLE_ITEM_INDEX = "firstVisibleItemIndex"; + + /** + * @module.api + */ + public static final String PROPERTY_FIRST_VISIBLE_SECTION = "firstVisibleSection"; + + /** + * @module.api + */ + public static final String PROPERTY_FIRST_VISIBLE_SECTION_INDEX = "firstVisibleSectionIndex"; + /** * @module.api */