From e5258890f3cd14fee369ff6b538d472f2ed17122 Mon Sep 17 00:00:00 2001 From: Joshua Quick Date: Tue, 19 Oct 2021 20:12:33 -0700 Subject: [PATCH] fix(android): edit move issues with ListView/TableView (#13120) Fixes TIMOB-28552, TIMOB-28553, TIMOB-28554, TIMOB-28555 --- .../modules/titanium/ui/TableViewProxy.java | 58 ++++++--- .../ui/widget/listview/ItemTouchHandler.java | 36 +++--- .../ui/widget/listview/ListItemProxy.java | 6 +- .../ui/widget/listview/ListViewProxy.java | 116 +++++++++++++++--- .../ui/widget/listview/RecyclerViewProxy.java | 6 +- .../ui/widget/listview/TiListView.java | 13 +- .../ui/widget/tableview/TiTableView.java | 11 ++ .../org/appcelerator/kroll/KrollDict.java | 9 ++ .../java/org/appcelerator/titanium/TiC.java | 3 + 9 files changed, 204 insertions(+), 54 deletions(-) diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/TableViewProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/TableViewProxy.java index 467f173151b..71b3e18dca0 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/TableViewProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/TableViewProxy.java @@ -65,7 +65,6 @@ public class TableViewProxy extends RecyclerViewProxy private static final String TAG = "TableViewProxy"; private final List sections = new ArrayList<>(); - private KrollDict contentOffset = null; public TableViewProxy() @@ -248,8 +247,13 @@ public void swipeItem(int adapterIndex) * * @param fromAdapterIndex Index of item in adapter. * @param toAdapterIndex Index of item in adapter. + * @return + * Returns adapter index the item was moved to after updating adapter list, + * which might not match given "toAdapterIndex" if moved to an empty section placeholder. + *

+ * Returns -1 if item was not moved. Can happen if indexes are invalid or if move to destination is not allowed. */ - public void moveItem(int fromAdapterIndex, int toAdapterIndex) + public int moveItem(int fromAdapterIndex, int toAdapterIndex) { final TiTableView tableView = getTableView(); @@ -257,29 +261,51 @@ public void moveItem(int fromAdapterIndex, int toAdapterIndex) final TableViewRowProxy fromItem = tableView.getAdapterItem(fromAdapterIndex); final TableViewSectionProxy fromSection = (TableViewSectionProxy) fromItem.getParent(); final TableViewRowProxy toItem = tableView.getAdapterItem(toAdapterIndex); - final TableViewSectionProxy toSection = (TableViewSectionProxy) toItem.getParent(); - final int toIndex = toItem.getIndexInSection(); - - fromSection.remove(fromItem); - toSection.add(toIndex, fromItem); - - update(); + final TiViewProxy parentProxy = toItem.getParent(); + if (parentProxy instanceof TableViewSectionProxy) { + final TableViewSectionProxy toSection = (TableViewSectionProxy) parentProxy; + final int toIndex = Math.max(toItem.getIndexInSection(), 0); + fromSection.remove(fromItem); + toSection.add(toIndex, fromItem); + update(); + return tableView.getAdapterIndex(fromItem); + } } + return -1; } /** - * Fire `move` event upon finalized movement of an item. + * Called when row drag-and-drop movement is about to start. * - * @param fromAdapterIndex Index of item in adapter. + * @param adapterIndex Index of row in adapter that is about to be moved. + * @return Returns true if row movement is allowed. Returns false to prevent row movement. */ - public void fireMoveEvent(int fromAdapterIndex) + public boolean onMoveItemStarting(int adapterIndex) { final TiTableView tableView = getTableView(); + if ((tableView != null) && (adapterIndex >= 0)) { + final TableViewRowProxy rowProxy = tableView.getAdapterItem(adapterIndex); + if ((rowProxy != null) && (rowProxy.getParent() instanceof TableViewSectionProxy)) { + return true; + } + } + return false; + } - if (tableView != null) { - final TableViewRowProxy fromItem = tableView.getAdapterItem(fromAdapterIndex); - - fromItem.fireEvent(TiC.EVENT_MOVE, null); + /** + * Called when row drag-and-drop movement has ended. + * + * @param adapterIndex Index of position the row was dragged in adapter list. + */ + public void onMoveItemEnded(int adapterIndex) + { + // Fire a "move" event. + final TiTableView tableView = getTableView(); + if ((tableView != null) && (adapterIndex >= 0)) { + final TableViewRowProxy rowProxy = tableView.getAdapterItem(adapterIndex); + if (rowProxy != null) { + rowProxy.fireEvent(TiC.EVENT_MOVE, null); + } } } diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ItemTouchHandler.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ItemTouchHandler.java index 17a9b7b4829..e6477620582 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ItemTouchHandler.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ItemTouchHandler.java @@ -21,7 +21,6 @@ import android.view.ViewParent; import androidx.annotation.NonNull; -import androidx.core.util.Pair; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; @@ -37,8 +36,7 @@ public class ItemTouchHandler extends ItemTouchHelper.SimpleCallback private TiRecyclerViewAdapter adapter; private RecyclerViewProxy recyclerViewProxy; - private Pair movePair = null; - + private int moveEndIndex = -1; private Drawable icon; private final ColorDrawable background; @@ -59,14 +57,10 @@ public ItemTouchHandler(@NonNull TiRecyclerViewAdapter adapter, public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { - - // Only fire `move` event upon final movement location. - - if (movePair != null) { - final int fromIndex = movePair.first; - - recyclerViewProxy.fireMoveEvent(fromIndex); - movePair = null; + if (moveEndIndex >= 0) { + // Notify owner that item movement has ended. Will fire a "move" event. + recyclerViewProxy.onMoveItemEnded(moveEndIndex); + moveEndIndex = -1; } } return false; @@ -274,12 +268,26 @@ public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder fromHolder, @NonNull RecyclerView.ViewHolder toHolder) { + // Fetch index positions of items to swap. final int fromIndex = fromHolder.getAdapterPosition(); final int toIndex = toHolder.getAdapterPosition(); - movePair = new Pair<>(fromIndex, toIndex); - this.recyclerViewProxy.moveItem(fromIndex, toIndex); - return true; + // Notify owner if this is the start of item movement. + if (this.moveEndIndex < 0) { + boolean canMove = this.recyclerViewProxy.onMoveItemStarting(fromIndex); + if (!canMove) { + return false; + } + } + + // Swap items and store destination index position which is needed by onMoveItemEnded() call. + // Note: "fromIndex" and "toIndex" will become invalid if item was moved into an empty placeholder section. + // This causes placeholder to be removed and adapter collection length to be reduced by one. + int newToIndex = this.recyclerViewProxy.moveItem(fromIndex, toIndex); + if (newToIndex >= 0) { + this.moveEndIndex = newToIndex; + } + return (newToIndex >= 0); } /** diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListItemProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListItemProxy.java index 2f82443785d..e7286832055 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListItemProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListItemProxy.java @@ -132,9 +132,9 @@ public Object handleEvent(String eventName, Object data, boolean fireItemClick) final ListSectionProxy section = (ListSectionProxy) parent; // Include section specific properties. - payload.put(TiC.PROPERTY_SECTION, section); - payload.put(TiC.PROPERTY_SECTION_INDEX, listViewProxy.getIndexOfSection(section)); - payload.put(TiC.PROPERTY_ITEM_INDEX, getIndexInSection()); + payload.putIfAbsent(TiC.PROPERTY_SECTION, section); + payload.putIfAbsent(TiC.PROPERTY_SECTION_INDEX, listViewProxy.getIndexOfSection(section)); + payload.putIfAbsent(TiC.PROPERTY_ITEM_INDEX, getIndexInSection()); } final Object itemId = getProperties().get(TiC.PROPERTY_ITEM_ID); diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListViewProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListViewProxy.java index af8e0545637..cad8a3b7125 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListViewProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListViewProxy.java @@ -62,6 +62,7 @@ public class ListViewProxy extends RecyclerViewProxy private List sections = new ArrayList<>(); private HashMap> markers = new HashMap<>(); private KrollDict contentOffset = null; + private final MoveEventInfo moveEventInfo = new MoveEventInfo(); public ListViewProxy() { @@ -160,11 +161,12 @@ public void swipeItem(int adapterIndex) if (listView != null) { final ListItemProxy item = listView.getAdapterItem(adapterIndex); - final ListSectionProxy section = (ListSectionProxy) item.getParent(); - - item.fireSyncEvent(TiC.EVENT_DELETE, null); - - section.deleteItemsAt(item.getIndexInSection(), 1, null); + final TiViewProxy parentProxy = item.getParent(); + if (parentProxy instanceof ListSectionProxy) { + final ListSectionProxy section = (ListSectionProxy) parentProxy; + item.fireSyncEvent(TiC.EVENT_DELETE, null); + section.deleteItemsAt(item.getIndexInSection(), 1, null); + } } } @@ -173,8 +175,13 @@ public void swipeItem(int adapterIndex) * * @param fromAdapterIndex Index of item in adapter. * @param toAdapterIndex Index of item in adapter. + * @return + * Returns adapter index the item was moved to after updating adapter list, + * which might not match given "toAdapterIndex" if moved to an empty section placeholder. + *

+ * Returns -1 if item was not moved. Can happen if indexes are invalid or if move to destination is not allowed. */ - public void moveItem(int fromAdapterIndex, int toAdapterIndex) + public int moveItem(int fromAdapterIndex, int toAdapterIndex) { final TiListView listView = getListView(); @@ -183,28 +190,71 @@ public void moveItem(int fromAdapterIndex, int toAdapterIndex) final ListSectionProxy fromSection = (ListSectionProxy) fromItem.getParent(); final int fromIndex = fromItem.getIndexInSection(); final ListItemProxy toItem = listView.getAdapterItem(toAdapterIndex); - final ListSectionProxy toSection = (ListSectionProxy) toItem.getParent(); - final int toIndex = toItem.getIndexInSection(); - - fromSection.deleteItemsAt(fromIndex, 1, null); - toSection.insertItemsAt(toIndex, fromItem, null); + final TiViewProxy parentProxy = toItem.getParent(); + if (parentProxy instanceof ListSectionProxy) { + final ListSectionProxy toSection = (ListSectionProxy) parentProxy; + final int toIndex = Math.max(toItem.getIndexInSection(), 0); + fromSection.deleteItemsAt(fromIndex, 1, null); + toSection.insertItemsAt(toIndex, fromItem, null); + return listView.getAdapterIndex(fromItem); + } } + return -1; } /** - * Fire `move` event upon finalized movement of an item. + * Called when item drag-and-drop movement is about to start. * - * @param fromAdapterIndex Index of item in adapter. + * @param adapterIndex Index of item in adapter that is about to be moved. + * @return Returns true if item movement is allowed. Returns false to prevent item movement. */ - public void fireMoveEvent(int fromAdapterIndex) + public boolean onMoveItemStarting(int adapterIndex) { final TiListView listView = getListView(); + if ((listView != null) && (adapterIndex >= 0)) { + final ListItemProxy itemProxy = listView.getAdapterItem(adapterIndex); + if (itemProxy != null) { + final TiViewProxy parentProxy = itemProxy.getParent(); + if (parentProxy instanceof ListSectionProxy) { + this.moveEventInfo.sectionProxy = (ListSectionProxy) parentProxy; + this.moveEventInfo.sectionIndex = getIndexOfSection(this.moveEventInfo.sectionProxy); + this.moveEventInfo.itemIndex = itemProxy.getIndexInSection(); + return true; + } + } + } + return false; + } - if (listView != null) { - final ListItemProxy fromItem = listView.getAdapterItem(fromAdapterIndex); - - fromItem.fireEvent(TiC.EVENT_MOVE, null); + /** + * Called when item drag-and-drop movement has ended. + * + * @param adapterIndex Index of position the item was dragged in adapter list. + */ + public void onMoveItemEnded(int adapterIndex) + { + // Fire a "move" event. + final TiListView listView = getListView(); + if ((listView != null) && this.moveEventInfo.isMoving()) { + final ListItemProxy targetItemProxy = listView.getAdapterItem(adapterIndex); + if (targetItemProxy != null) { + final TiViewProxy targetParentProxy = targetItemProxy.getParent(); + if (targetParentProxy instanceof ListSectionProxy) { + ListSectionProxy targetSectionProxy = (ListSectionProxy) targetParentProxy; + KrollDict data = new KrollDict(); + data.put(TiC.PROPERTY_SECTION, this.moveEventInfo.sectionProxy); + data.put(TiC.PROPERTY_SECTION_INDEX, this.moveEventInfo.sectionIndex); + data.put(TiC.PROPERTY_ITEM_INDEX, this.moveEventInfo.itemIndex); + data.put(TiC.PROPERTY_TARGET_SECTION, targetSectionProxy); + data.put(TiC.PROPERTY_TARGET_SECTION_INDEX, getIndexOfSection(targetSectionProxy)); + data.put(TiC.PROPERTY_TARGET_ITEM_INDEX, targetItemProxy.getIndexInSection()); + targetItemProxy.fireEvent(TiC.EVENT_MOVE, data); + } + } } + + // Clear last "move" event info. + this.moveEventInfo.clear(); } /** @@ -848,4 +898,34 @@ public void update() listView.update(); } } + + /** Stores starting position info of an item being dragged-and-dropped. */ + private static class MoveEventInfo + { + /** Section proxy the item being dragged originally belonged to. */ + public ListSectionProxy sectionProxy; + + /** Index of section in list the item being dragged originally belonged to. */ + public int sectionIndex = -1; + + /** Original index position of the item being dragged. */ + public int itemIndex = -1; + + /** + * Determines if this object contains start position info. + * @return Returns true if start position info is stored. Returns false if not. + */ + public boolean isMoving() + { + return (this.itemIndex >= 0); + } + + /** Clears start position info. Should be called at end of drag-and-drop event. */ + public void clear() + { + this.sectionProxy = null; + this.sectionIndex = -1; + this.itemIndex = -1; + } + } } diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/RecyclerViewProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/RecyclerViewProxy.java index cc9d01df71e..f2ab8aaebbe 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/RecyclerViewProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/RecyclerViewProxy.java @@ -14,7 +14,9 @@ public abstract class RecyclerViewProxy extends TiViewProxy { public abstract void swipeItem(int index); - public abstract void moveItem(int from, int to); + public abstract int moveItem(int fromIndex, int toIndex); - public abstract void fireMoveEvent(int from); + public abstract boolean onMoveItemStarting(int index); + + public abstract void onMoveItemEnded(int index); } 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 375947a7aa0..415c00b9adc 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 @@ -502,6 +502,17 @@ public int getAdapterIndex(int index) return -1; } + /** + * Obtains adapter index from list item reference. + * + * @param itemProxy The list item to search for by reference. Can be null. + * @return Returns the adapter index position of the given item. Returns -1 if not found. + */ + public int getAdapterIndex(ListItemProxy itemProxy) + { + return this.items.indexOf(itemProxy); + } + /** * Obtain item from adapter index. * @@ -692,7 +703,7 @@ public void update() item.getProperties().put(TiC.PROPERTY_FOOTER_VIEW, sectionProperties.get(TiC.PROPERTY_FOOTER_VIEW)); - item.setParent(this.proxy); + item.setParent(section); this.items.add(item); } } 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 b816be3fa87..2340d9a57e2 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 @@ -489,6 +489,17 @@ public int getAdapterIndex(int index) return -1; } + /** + * Obtains adapter index from list item reference. + * + * @param rowProxy The row object to search for by reference. Can be null. + * @return Returns the adapter index position of the given row. Returns -1 if not found. + */ + public int getAdapterIndex(TableViewRowProxy rowProxy) + { + return this.rows.indexOf(rowProxy); + } + /** * Obtain row from adapter index. * diff --git a/android/titanium/src/java/org/appcelerator/kroll/KrollDict.java b/android/titanium/src/java/org/appcelerator/kroll/KrollDict.java index a7b5a186800..45df4a602fb 100644 --- a/android/titanium/src/java/org/appcelerator/kroll/KrollDict.java +++ b/android/titanium/src/java/org/appcelerator/kroll/KrollDict.java @@ -96,6 +96,15 @@ public void putCodeAndMessage(int code, String message) } } + public Object putIfAbsent(String key, Object value) + { + Object existingValue = this.get(key); + if (existingValue == null) { + this.put(key, value); + } + return existingValue; + } + public boolean containsKeyAndNotNull(String key) { return containsKey(key) && get(key) != null; diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiC.java b/android/titanium/src/java/org/appcelerator/titanium/TiC.java index b50025a4c34..c0b16b8ecb4 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiC.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiC.java @@ -282,6 +282,9 @@ public class TiC public static final String PROPERTY_BACKGROUND_SELECTED_IMAGE = "backgroundSelectedImage"; public static final String PROPERTY_BADGE = "badge"; public static final String PROPERTY_BADGE_COLOR = "badgeColor"; + public static final String PROPERTY_TARGET_ITEM_INDEX = "targetItemIndex"; + public static final String PROPERTY_TARGET_SECTION = "targetSection"; + public static final String PROPERTY_TARGET_SECTION_INDEX = "targetSectionIndex"; public static final String PROPERTY_TOUCH_FEEDBACK = "touchFeedback"; public static final String PROPERTY_TOUCH_FEEDBACK_COLOR = "touchFeedbackColor"; public static final String PROPERTY_TRANSITION_NAME = "transitionName";