Skip to content

Commit 600ae49

Browse files
committed
feat: reorder feature is here (using reorderEnabled property)
1 parent 6c27444 commit 600ae49

File tree

4 files changed

+440
-55
lines changed

4 files changed

+440
-55
lines changed

src/collectionview-common.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
removeWeakEventListener,
2525
widthProperty
2626
} from '@nativescript/core';
27-
import { CollectionView as CollectionViewDefinition, Orientation } from './collectionview';
27+
import { CollectionView as CollectionViewDefinition, CollectionViewItemEventData, Orientation } from './collectionview';
2828

2929
export const CollectionViewTraceCategory = 'NativescriptCollectionView';
3030

@@ -110,6 +110,9 @@ export abstract class CollectionViewBase extends View implements CollectionViewD
110110
public static scrollEndEvent = 'scrollEnd';
111111
public static itemTapEvent = 'itemTap';
112112
public static displayItemEvent = 'displayItem';
113+
public static itemReorderedEvent = 'itemReordered';
114+
public static itemReorderStartingEvent = 'itemReorderStarting';
115+
public static itemReorderStartedEvent = 'itemReorderStarted';
113116
public static loadMoreItemsEvent = 'loadMoreItems';
114117
public static dataPopulatedEvent = 'dataPopulated';
115118
public static knownFunctions = ['itemTemplateSelector', 'itemIdGenerator']; // See component-builder.ts isKnownFunction
@@ -132,6 +135,9 @@ export abstract class CollectionViewBase extends View implements CollectionViewD
132135

133136
public loadMoreThreshold: number;
134137

138+
public reorderEnabled: boolean;
139+
protected _dataUpdatesSuspended = false;
140+
135141
public layoutStyle: string = 'grid';
136142
public plugins: string[] = [];
137143
public static plugins: { [k: string]: Plugin } = {};
@@ -423,7 +429,9 @@ export abstract class CollectionViewBase extends View implements CollectionViewD
423429
this.refresh();
424430
}
425431
onSourceCollectionChangedInternal(event: ChangedData<any>) {
426-
this.onSourceCollectionChanged(event);
432+
if (this._dataUpdatesSuspended === false) {
433+
this.onSourceCollectionChanged(event);
434+
}
427435
}
428436
// onItemsChanged(oldValue, newValue) {
429437
// this.onItemsChangedInternal(oldValue, newValue);
@@ -435,6 +443,76 @@ export abstract class CollectionViewBase extends View implements CollectionViewD
435443
[heightProperty.getDefault]() {
436444
return '100%';
437445
}
446+
public suspendUpdates() {
447+
this._dataUpdatesSuspended = true;
448+
}
449+
public updatesSuspended(): boolean {
450+
return this._dataUpdatesSuspended;
451+
}
452+
public resumeUpdates(refresh: boolean) {
453+
this._dataUpdatesSuspended = false;
454+
if (refresh === true) {
455+
this.refresh();
456+
}
457+
}
458+
abstract getViewForItemAtIndex(index: number): View;
459+
draggingView: View;
460+
_callItemReorderedEvent(oldPosition, newPosition, item) {
461+
console.log('_callItemReorderedEvent', this.draggingView);
462+
const args = {
463+
eventName: CollectionViewBase.itemReorderedEvent,
464+
object: this,
465+
index: oldPosition,
466+
item,
467+
data: { targetIndex: newPosition },
468+
view: this.draggingView
469+
} as CollectionViewItemEventData;
470+
this.notify(args);
471+
this.draggingView = null;
472+
}
473+
_reorderItemInSource(oldPosition: number, newPosition: number, callEvents = true) {
474+
this.suspendUpdates();
475+
const ownerSource = this.items as any;
476+
const item = this.getItemAtIndex(oldPosition);
477+
ownerSource.splice(oldPosition, 1);
478+
ownerSource.splice(newPosition, 0, item);
479+
480+
this.resumeUpdates(false);
481+
if (callEvents) {
482+
this._callItemReorderedEvent(oldPosition, newPosition, item);
483+
}
484+
485+
}
486+
487+
shouldMoveItemAtIndex(index: number) {
488+
if (!this.reorderEnabled) {
489+
return false;
490+
}
491+
const item = this.getItemAtIndex(index);
492+
const view = this.draggingView = this.getViewForItemAtIndex(index);
493+
console.log('shouldMoveItemAtIndex', this.draggingView);
494+
let args = {
495+
returnValue: true,
496+
eventName: CollectionViewBase.itemReorderStartingEvent,
497+
object: this,
498+
index,
499+
item,
500+
view
501+
};
502+
this.notify(args);
503+
if (!args.returnValue) {
504+
return false;
505+
}
506+
args = {
507+
eventName: CollectionViewBase.itemReorderStartedEvent,
508+
object: this,
509+
index,
510+
item,
511+
view
512+
} as any;
513+
this.notify(args);
514+
return true;
515+
}
438516
}
439517

440518
const defaultRowHeight: Length = 'auto';
@@ -560,3 +638,9 @@ export const loadMoreThresholdProperty = new Property<CollectionViewBase, number
560638
valueConverter: v => parseInt(v, 10)
561639
});
562640
loadMoreThresholdProperty.register(CollectionViewBase);
641+
export const reorderingEnabledProperty = new Property<CollectionViewBase, boolean>({
642+
name: 'reorderEnabled',
643+
defaultValue: false,
644+
valueConverter: booleanConverter,
645+
});
646+
reorderingEnabledProperty.register(CollectionViewBase);

src/collectionview.android.ts

Lines changed: 185 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,92 @@ import {
1515
profile,
1616
} from '@nativescript/core';
1717
import { layout } from '@nativescript/core/utils/utils';
18-
import { CollectionViewItemEventData, Orientation, reverseLayoutProperty } from './collectionview';
18+
import { CollectionViewItemDisplayEventData, CollectionViewItemEventData, Orientation, reorderingEnabledProperty, reverseLayoutProperty } from './collectionview';
1919
import { CLog, CLogTypes, CollectionViewBase, ListViewViewTypes, isScrollEnabledProperty, orientationProperty } from './collectionview-common';
2020

2121
export * from './collectionview-common';
2222

23+
declare module '@nativescript/core/ui/core/view' {
24+
interface View {
25+
handleGestureTouch(event: android.view.MotionEvent);
26+
}
27+
}
28+
29+
30+
@NativeClass
31+
class SimpleCallback extends androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback {
32+
owner: WeakRef<CollectionView>;
33+
constructor(param1: number, param2: number) {
34+
super(param1, param2);
35+
return global.__native(this);
36+
}
37+
onMove(recyclerview: androidx.recyclerview.widget.RecyclerView, viewHolder: androidx.recyclerview.widget.RecyclerView.ViewHolder, target: androidx.recyclerview.widget.RecyclerView.ViewHolder): boolean {
38+
const startPosition = viewHolder.getAdapterPosition();
39+
const endPosition = target.getAdapterPosition();
40+
if (this.startPosition === -1) {
41+
this.startPosition = startPosition;
42+
}
43+
this.endPosition = endPosition;
44+
const owner = this.owner && this.owner.get();
45+
if (owner) {
46+
owner._reorderItemInSource(startPosition, endPosition);
47+
return true;
48+
}
49+
return false;
50+
}
51+
startPosition = -1;
52+
endPosition = -1;
53+
onSelectedChanged(viewHolder: androidx.recyclerview.widget.RecyclerView.ViewHolder, state: number) {
54+
console.log('onSelectedChanged', viewHolder, this.startPosition, this.endPosition);
55+
if (viewHolder) {
56+
if (this.startPosition === -1) {
57+
this.startPosition = viewHolder.getAdapterPosition();
58+
}
59+
}
60+
if (!viewHolder) {
61+
// this is where we identify the end of the drag and call the end event
62+
const owner = this.owner && this.owner.get();
63+
64+
if (this.endPosition === -1) {
65+
this.endPosition = this.startPosition;
66+
}
67+
if (owner) {
68+
const item = owner.getItemAtIndex(this.startPosition);
69+
owner._callItemReorderedEvent(this.startPosition, this.endPosition, item);
70+
}
71+
this.startPosition = -1;
72+
this.endPosition = -1;
73+
owner.isDragging = false;
74+
}
75+
}
76+
onSwiped(viewHolder: androidx.recyclerview.widget.RecyclerView.ViewHolder,
77+
direction: number) {
78+
}
79+
isItemViewSwipeEnabled() {
80+
// disabled for now
81+
return false;
82+
}
83+
isLongPressDragEnabled() {
84+
// we use our custom longpress gesture handler
85+
return false;
86+
}
87+
}
88+
89+
90+
@NativeClass
91+
class LongPressGestureListenerImpl extends android.view.GestureDetector.SimpleOnGestureListener {
92+
constructor(private _owner: WeakRef<CollectionView>) {
93+
super();
94+
return global.__native(this);
95+
}
96+
public onLongPress(motionEvent: android.view.MotionEvent): void {
97+
const owner = this._owner && this._owner.get();
98+
if (owner) {
99+
owner.onReorderLongPress(motionEvent);
100+
}
101+
}
102+
103+
}
23104

24105
// @NativeClass
25106
// class PreCachingGridLayoutManager extends com.nativescript.collectionview.PreCachingGridLayoutManager {
@@ -127,6 +208,10 @@ export class CollectionView extends CollectionViewBase {
127208

128209
private _listViewAdapter: com.nativescript.collectionview.Adapter;
129210

211+
// reordering
212+
private _simpleItemTouchCallback: SimpleCallback;
213+
private _itemTouchHelper: androidx.recyclerview.widget.ItemTouchHelper;
214+
130215
@profile
131216
public createNativeView() {
132217
// storing the class in a property for reuse in the future cause a materializing which is pretty slow!
@@ -187,7 +272,19 @@ export class CollectionView extends CollectionViewBase {
187272
// rowHeightProperty.coerce(this);
188273
}
189274
_getSpanSize: (position: number) => number;
275+
public getViewForItemAtIndex(index: number): View {
190276

277+
let result: View;
278+
this._viewHolders.some(function (cellItemView, key) {
279+
if (cellItemView && cellItemView.getAdapterPosition() === index) {
280+
result = cellItemView.view;
281+
return true;
282+
}
283+
return false;
284+
});
285+
286+
return result;
287+
}
191288
//@ts-ignore
192289
set spanSize(inter: (position: number) => number) {
193290
if (!(typeof inter === 'function')) {
@@ -236,6 +333,7 @@ export class CollectionView extends CollectionViewBase {
236333
if (nativeView.scrollListener) {
237334
this.nativeView.removeOnScrollListener(nativeView.scrollListener);
238335
nativeView.scrollListener = null;
336+
this._nScrollListener = null;
239337
}
240338
}
241339
}
@@ -305,19 +403,26 @@ export class CollectionView extends CollectionViewBase {
305403

306404
public disposeNativeView() {
307405
// clear the cache
308-
this.eachChildView((view) => {
309-
view.parent._removeView(view);
310-
return true;
311-
});
406+
// this.eachChildView((view) => {
407+
// view.parent._removeView(view);
408+
// return true;
409+
// });
312410
// this._realizedItems.clear();
313411

314412
const nativeView = this.nativeView;
315-
316413
if (nativeView.scrollListener) {
317414
this.nativeView.removeOnScrollListener(nativeView.scrollListener);
318415
nativeView.scrollListener = null;
416+
this._nScrollListener = null;
319417
}
320418
nativeView.layoutManager = null;
419+
this._listViewAdapter = null;
420+
this._itemTouchHelper = null;
421+
this._simpleItemTouchCallback = null;
422+
this.disposeViewHolderViews();
423+
this._hlayoutParams = null;
424+
this._vlayoutParams = null;
425+
this.clearTemplateTypes();
321426

322427
super.disposeNativeView();
323428
}
@@ -416,6 +521,79 @@ export class CollectionView extends CollectionViewBase {
416521
public [itemViewCacheSizeProperty.setNative](value: number) {
417522
this.nativeViewProtected.setItemViewCacheSize(value);
418523
}
524+
public startDragging(index: number) {
525+
if (this.reorderEnabled && this._itemTouchHelper) {
526+
let viewHolder: CollectionViewCellHolder;
527+
this._viewHolders.some((v) => {
528+
if (v.getAdapterPosition() === index) {
529+
viewHolder = v;
530+
return true;
531+
}
532+
return false;
533+
});
534+
if (viewHolder) {
535+
this.startViewHolderDragging(index, viewHolder);
536+
}
537+
}
538+
}
539+
isDragging = false;
540+
startViewHolderDragging (index, viewHolder: CollectionViewCellHolder) {
541+
// isDragging is to prevent longPress from triggering and starting a new drag
542+
// when triggered manually
543+
if (!this.isDragging && this.shouldMoveItemAtIndex(index)) {
544+
this.isDragging = true;
545+
this._itemTouchHelper.startDrag(viewHolder);
546+
}
547+
}
548+
onReorderLongPress(motionEvent: android.view.MotionEvent) {
549+
const collectionView =this.nativeViewProtected;
550+
if (!collectionView) {
551+
return;
552+
}
553+
const view = collectionView.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
554+
const viewHolder = view != null ? collectionView.getChildViewHolder(view) : null;
555+
if (viewHolder) {
556+
this.startViewHolderDragging(viewHolder.getAdapterPosition(), viewHolder as CollectionViewCellHolder);
557+
}
558+
}
559+
_reorderItemInSource(oldPosition: number, newPosition: number) {
560+
const adapter = this._listViewAdapter;
561+
// 3. Tell adapter to render the model update.
562+
adapter.notifyItemMoved(oldPosition, newPosition);
563+
// on android _reorderItemInSource is call on every "move" and needs to update the adapter/items
564+
// we will call events only at then end
565+
super._reorderItemInSource(oldPosition, newPosition, false);
566+
}
567+
568+
_longPressGesture: androidx.core.view.GestureDetectorCompat;
569+
_itemTouchListerner: androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
570+
public [reorderingEnabledProperty.setNative](value: boolean) {
571+
if (value) {
572+
if (!this._simpleItemTouchCallback) {
573+
const ItemTouchHelper = androidx.recyclerview.widget.ItemTouchHelper;
574+
this._simpleItemTouchCallback = new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.START | ItemTouchHelper.END , 0);
575+
this._simpleItemTouchCallback.owner = new WeakRef(this);
576+
this._itemTouchHelper = new androidx.recyclerview.widget.ItemTouchHelper(this._simpleItemTouchCallback);
577+
this._itemTouchHelper.attachToRecyclerView(this.nativeViewProtected);
578+
}
579+
if (!this._longPressGesture) {
580+
this._longPressGesture = new androidx.core.view.GestureDetectorCompat(this._context, new LongPressGestureListenerImpl(new WeakRef(this)));
581+
this._itemTouchListerner = new androidx.recyclerview.widget.RecyclerView.OnItemTouchListener({
582+
onInterceptTouchEvent:(view: android.view.View, event: android.view.MotionEvent)=>{
583+
if (this.reorderEnabled && this._longPressGesture) {
584+
this._longPressGesture.onTouchEvent(event);
585+
}
586+
return false;
587+
},
588+
onTouchEvent:(param0: androidx.recyclerview.widget.RecyclerView, param1: globalAndroid.view.MotionEvent)=>{
589+
},
590+
onRequestDisallowInterceptTouchEvent:(disallowIntercept: boolean) =>{
591+
}
592+
});
593+
this.nativeViewProtected.addOnItemTouchListener(this._itemTouchListerner);
594+
}
595+
}
596+
}
419597

420598
onItemViewLoaderChanged() {
421599
if (this.itemViewLoader) {
@@ -675,6 +853,7 @@ export class CollectionView extends CollectionViewBase {
675853
});
676854
this._viewHolders = new Array();
677855
this._viewHolderChildren.forEach(this._removeViewCore);
856+
this._viewHolderChildren = new Array();
678857
}
679858
getKeyByValue(viewType: number) {
680859
return this.templateStringTypeNumber.get(viewType);

src/collectionview.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type Orientation = 'horizontal' | 'vertical';
77
export class CollectionView extends CollectionViewBase {
88
public refresh();
99
public scrollToIndex(index: number, animated: boolean);
10+
public getViewForItemAtIndex(index: number): View;
1011
}
1112

1213
export interface CollectionViewItemEventData extends EventData {

0 commit comments

Comments
 (0)