From 02259823da062015da4bbe4a1ae09246528b7625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=8A=B2=E9=B9=8F?= Date: Wed, 24 Aug 2022 18:07:22 +0800 Subject: [PATCH] iOS: implement horizontal list --- doric-iOS/Pod/Classes/DoricRegistry.m | 4 + .../Shader/DoricHorizontalListItemNode.h | 25 + .../Shader/DoricHorizontalListItemNode.m | 44 ++ .../Classes/Shader/DoricHorizontalListNode.h | 25 + .../Classes/Shader/DoricHorizontalListNode.m | 539 ++++++++++++++++++ 5 files changed, 637 insertions(+) create mode 100644 doric-iOS/Pod/Classes/Shader/DoricHorizontalListItemNode.h create mode 100644 doric-iOS/Pod/Classes/Shader/DoricHorizontalListItemNode.m create mode 100644 doric-iOS/Pod/Classes/Shader/DoricHorizontalListNode.h create mode 100644 doric-iOS/Pod/Classes/Shader/DoricHorizontalListNode.m diff --git a/doric-iOS/Pod/Classes/DoricRegistry.m b/doric-iOS/Pod/Classes/DoricRegistry.m index 4786b1dd..b5729010 100644 --- a/doric-iOS/Pod/Classes/DoricRegistry.m +++ b/doric-iOS/Pod/Classes/DoricRegistry.m @@ -31,6 +31,8 @@ #import "DoricImageNode.h" #import "DoricListNode.h" #import "DoricListItemNode.h" +#import "DoricHorizontalListNode.h" +#import "DoricHorizontalListItemNode.h" #import "DoricScrollerNode.h" #import "DoricSliderNode.h" #import "DoricSlideItemNode.h" @@ -143,6 +145,8 @@ - (void)innerRegister { [self registerViewNode:DoricGestureContainerNode.class withName:@"GestureContainer"]; [self registerViewNode:DoricBlurEffectViewNode.class withName:@"BlurEffect"]; [self registerViewNode:DoricAeroEffectViewNode.class withName:@"AeroEffect"]; + [self registerViewNode:DoricHorizontalListItemNode.class withName:@"HorizontalListItem"]; + [self registerViewNode:DoricHorizontalListNode.class withName:@"HorizontalList"]; [self.loaderManager registerLoader:[[DoricBundleResourceLoader alloc] initWithResourceType:@"mainBundle" diff --git a/doric-iOS/Pod/Classes/Shader/DoricHorizontalListItemNode.h b/doric-iOS/Pod/Classes/Shader/DoricHorizontalListItemNode.h new file mode 100644 index 00000000..4762d285 --- /dev/null +++ b/doric-iOS/Pod/Classes/Shader/DoricHorizontalListItemNode.h @@ -0,0 +1,25 @@ +/* + * Copyright [2022] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// Created by jingpeng.wang on 2022/8/23. +// + +#import + +#import "DoricStackNode.h" + +@interface DoricHorizontalListItemNode : DoricStackNode +@end diff --git a/doric-iOS/Pod/Classes/Shader/DoricHorizontalListItemNode.m b/doric-iOS/Pod/Classes/Shader/DoricHorizontalListItemNode.m new file mode 100644 index 00000000..0b03fb12 --- /dev/null +++ b/doric-iOS/Pod/Classes/Shader/DoricHorizontalListItemNode.m @@ -0,0 +1,44 @@ +/* + * Copyright [2022] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// Created by jingpeng.wang on 2022/8/23. +// + +#import "DoricHorizontalListItemNode.h" + +@interface DoricHorizontalListItemNode () +@end + +@implementation DoricHorizontalListItemNode +- (instancetype)initWithContext:(DoricContext *)doricContext { + if (self = [super initWithContext:doricContext]) { + self.reusable = YES; + } + return self; +} + +- (void)initWithSuperNode:(DoricSuperNode *)superNode { + [super initWithSuperNode:superNode]; + self.reusable = YES; +} + +- (void)blendView:(UIView *)view forPropName:(NSString *)name propValue:(id)prop { + if ([@"identifier" isEqualToString:name] || [@"actions" isEqualToString:name]) { + } else { + [super blendView:view forPropName:name propValue:prop]; + } +} +@end diff --git a/doric-iOS/Pod/Classes/Shader/DoricHorizontalListNode.h b/doric-iOS/Pod/Classes/Shader/DoricHorizontalListNode.h new file mode 100644 index 00000000..87d0c8ee --- /dev/null +++ b/doric-iOS/Pod/Classes/Shader/DoricHorizontalListNode.h @@ -0,0 +1,25 @@ +/* + * Copyright [2022] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// Created by jingpeng.wang on 2022/8/23. +// + +#import +#import "DoricSuperNode.h" +#import "DoricScrollableProtocol.h" + +@interface DoricHorizontalListNode : DoricSuperNode +@end diff --git a/doric-iOS/Pod/Classes/Shader/DoricHorizontalListNode.m b/doric-iOS/Pod/Classes/Shader/DoricHorizontalListNode.m new file mode 100644 index 00000000..937a8ebd --- /dev/null +++ b/doric-iOS/Pod/Classes/Shader/DoricHorizontalListNode.m @@ -0,0 +1,539 @@ +/* + * Copyright [2022] [Doric.Pub] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// +// Created by jingpeng.wang on 2022/8/23. +// + +#import +#import +#import "DoricHorizontalListNode.h" +#import "DoricExtensions.h" +#import "DoricHorizontalListItemNode.h" +#import "DoricRefreshableNode.h" +#import "DoricJSDispatcher.h" +#import "DoricUtil.h" +#import "DoricExtensions.h" + +@interface DoricHorizontalTableViewCell : UICollectionViewCell +@property(nonatomic, strong) DoricHorizontalListItemNode *doricHorizontalListItemNode; +@end + +@implementation DoricHorizontalTableViewCell +@end + +@interface DoricHorizontalTableView : UICollectionView +@end + +@implementation DoricHorizontalTableView +- (CGSize)sizeThatFits:(CGSize)size { + return [super sizeThatFits:size]; +} +@end + +@interface DoricHorizontalListNode () +@property(nonatomic, strong) NSMutableDictionary *itemViewIds; +@property(nonatomic, strong) NSMutableDictionary *itemWidths; +@property(nonatomic, strong) NSMutableDictionary *itemActions; +@property(nonatomic, assign) NSUInteger itemCount; +@property(nonatomic, assign) NSUInteger batchCount; +@property(nonatomic, copy) NSString *onLoadMoreFuncId; +@property(nonatomic, copy) NSString *renderItemFuncId; +@property(nonatomic, copy) NSString *loadMoreViewId; +@property(nonatomic, assign) BOOL loadMore; +@property(nonatomic, assign) NSInteger loadAnchor; +@property(nonatomic, strong) NSMutableSet *didScrollBlocks; +@property(nonatomic, copy) NSString *onScrollFuncId; +@property(nonatomic, copy) NSString *onScrollEndFuncId; +@property(nonatomic, strong) DoricJSDispatcher *jsDispatcher; + +@property(nonatomic, strong) UILongPressGestureRecognizer *longPress; +@property(nonatomic, strong) NSIndexPath *initialDragIndexPath; +@property(nonatomic, strong) NSIndexPath *currentDragIndexPath; +@property(nonatomic, copy) NSString *beforeDraggingFuncId; +@property(nonatomic, copy) NSString *onDraggingFuncId; +@property(nonatomic, copy) NSString *onDraggedFuncId; + +@property(nonatomic, assign) NSUInteger rowCount; +@property(nonatomic, assign) BOOL needReload; +@end + +@implementation DoricHorizontalListNode +- (instancetype)initWithContext:(DoricContext *)doricContext { + if (self = [super initWithContext:doricContext]) { + _itemViewIds = [NSMutableDictionary new]; + _itemWidths = [NSMutableDictionary new]; + _itemActions = [NSMutableDictionary new]; + _batchCount = 15; + _loadAnchor = -1; + } + return self; +} + +- (void)initWithSuperNode:(DoricSuperNode *)superNode { + [super initWithSuperNode:superNode]; + if ([superNode isKindOfClass:[DoricRefreshableNode class]]) { + self.view.bounces = NO; + } +} + +- (UICollectionView *)build { + UICollectionViewFlowLayout *collectionViewFlowLayout = [[UICollectionViewFlowLayout alloc] init]; + [collectionViewFlowLayout setScrollDirection:UICollectionViewScrollDirectionHorizontal]; + return [[[DoricHorizontalTableView alloc] initWithFrame:CGRectZero + collectionViewLayout:collectionViewFlowLayout] also:^(UICollectionView *it) { + it.dataSource = self; + it.delegate = self; + it.allowsSelection = NO; + it.showsHorizontalScrollIndicator = NO; + if (@available(iOS 11, *)) { + it.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } + self.longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressAction:)]; + [it addGestureRecognizer:self.longPress]; + [self.longPress setEnabled:NO]; + }]; +} + +- (void)longPressAction:(UILongPressGestureRecognizer *)sender { + CGPoint locationInView = [sender locationInView:self.view]; + NSIndexPath *indexPath = [self.view indexPathForItemAtPoint:locationInView]; + if (sender.state == UIGestureRecognizerStateBegan) { + if (indexPath != nil) { + self.initialDragIndexPath = indexPath; + self.currentDragIndexPath = indexPath; + if (self.beforeDraggingFuncId != nil) { + [self callJSResponse:self.beforeDraggingFuncId, @(indexPath.row), nil]; + } + } + } else if (sender.state == UIGestureRecognizerStateChanged) { + if ((indexPath != nil) && (indexPath != self.currentDragIndexPath)) { + NSString *fromValue = self.itemViewIds[@(self.currentDragIndexPath.row)]; + NSString *toValue = self.itemViewIds[@(indexPath.row)]; + self.itemViewIds[@(self.currentDragIndexPath.row)] = toValue; + self.itemViewIds[@(indexPath.row)] = fromValue; + + [self.view moveItemAtIndexPath:self.currentDragIndexPath toIndexPath:indexPath]; + if (self.onDraggingFuncId != nil) { + [self callJSResponse:self.onDraggingFuncId, @(self.currentDragIndexPath.row), @(indexPath.row), nil]; + } + self.currentDragIndexPath = indexPath; + } + } else if (sender.state == UIGestureRecognizerStateEnded) { + if (self.onDraggedFuncId != nil) { + [self callJSResponse:self.onDraggedFuncId, @(self.initialDragIndexPath.row), @(self.currentDragIndexPath.row), nil]; + } + } +} + +- (void)blendView:(UICollectionView *)view forPropName:(NSString *)name propValue:(id)prop { + if ([@"scrollable" isEqualToString:name]) { + self.view.scrollEnabled = [prop boolValue]; + } else if ([@"bounces" isEqualToString:name]) { + self.view.bounces = [prop boolValue]; + } else if ([@"itemCount" isEqualToString:name]) { + self.itemCount = [prop unsignedIntegerValue]; + self.needReload = true; + } else if ([@"renderItem" isEqualToString:name]) { + if (![self.renderItemFuncId isEqualToString:prop]) { + self.loadAnchor = -1; + self.renderItemFuncId = prop; + [self.itemViewIds.allValues forEach:^(NSString *obj) { + [self removeSubModel:obj]; + }]; + [self.itemViewIds removeAllObjects]; + self.needReload = true; + } + } else if ([@"batchCount" isEqualToString:name]) { + self.batchCount = [prop unsignedIntegerValue]; + } else if ([@"onLoadMore" isEqualToString:name]) { + self.onLoadMoreFuncId = prop; + } else if ([@"loadMoreView" isEqualToString:name]) { + self.loadMoreViewId = prop; + } else if ([@"loadMore" isEqualToString:name]) { + BOOL loadMore = [prop boolValue]; + if (loadMore != self.loadMore) { + self.loadMore = loadMore; + self.needReload = true; + } + } else if ([@"onScroll" isEqualToString:name]) { + self.onScrollFuncId = prop; + } else if ([@"onScrollEnd" isEqualToString:name]) { + self.onScrollEndFuncId = prop; + } else if ([@"scrolledPosition" isEqualToString:name]) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSUInteger pos = [prop unsignedIntegerValue]; + if (pos < self.rowCount && pos >= 0) { + [view scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:pos inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:NO]; + } + }); + } else if ([@"canDrag" isEqualToString:name]) { + bool canDrag = [prop boolValue]; + [self.longPress setEnabled:canDrag]; + } else if ([@"beforeDragging" isEqualToString:name]) { + self.beforeDraggingFuncId = prop; + } else if ([@"onDragging" isEqualToString:name]) { + self.onDraggingFuncId = prop; + } else if ([@"onDragged" isEqualToString:name]) { + self.onDraggedFuncId = prop; + } else { + [super blendView:view forPropName:name propValue:prop]; + } +} + +- (void)blend:(NSDictionary *)props { + self.needReload = false; + [super blend:props]; + if (self.needReload) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.rowCount = self.itemCount + (self.loadMore ? 1 : 0); + [self.view reloadData]; + }); + } + self.needReload = false; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return self.rowCount; +} + +- (void)callLoadMore { + if (self.rowCount - 1 != self.loadAnchor) { + self.loadAnchor = self.rowCount - 1; + [self callJSResponse:self.onLoadMoreFuncId, nil]; + } +} + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + NSUInteger position = (NSUInteger) indexPath.row; + NSDictionary *model = [self itemModelAt:position]; + NSDictionary *props = model[@"props"]; + NSString *identifier = props[@"identifier"] ?: @"doricCell"; + self.itemActions[@(position)] = props[@"actions"]; + if (self.loadMore + && position >= self.rowCount - 1 + && self.onLoadMoreFuncId) { + identifier = @"doricLoadMoreCell"; + [self callLoadMore]; + } + [collectionView registerClass:[DoricHorizontalTableViewCell class] forCellWithReuseIdentifier:identifier]; + DoricHorizontalTableViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath]; + if (!cell.doricHorizontalListItemNode) { + DoricHorizontalListItemNode *itemNode = (DoricHorizontalListItemNode *) [DoricViewNode create:self.doricContext withType:@"HorizontalListItem"]; + [itemNode initWithSuperNode:self]; + cell.doricHorizontalListItemNode = itemNode; + cell.backgroundColor = [UIColor clearColor]; + itemNode.view.height = collectionView.height; + [cell.contentView addSubview:itemNode.view]; + } else { + [cell.doricHorizontalListItemNode reset]; + } + DoricHorizontalListItemNode *node = cell.doricHorizontalListItemNode; + node.viewId = model[@"id"]; + [node blend:props]; + [node.view.doricLayout apply:CGSizeMake(collectionView.width, collectionView.height)]; + [node requestLayout]; + [self callItem:position width:node.view.width]; + return cell; +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { + return 0; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + NSUInteger position = (NSUInteger) indexPath.row; + NSNumber *widthNumber = self.itemWidths[@(position)]; + + float width = 44.f; + if (widthNumber) { + width = [widthNumber floatValue]; + } + + return CGSizeMake(width, collectionView.height); +} + +- (BOOL)collectionView:(UICollectionView *)collectionView canEditItemAtIndexPath:(NSIndexPath *)indexPath { + NSArray *actions = self.itemActions[@(indexPath.row)]; + return actions.count > 0; +} + +- (NSDictionary *)itemModelAt:(NSUInteger)position { + if (self.loadMore && position >= self.rowCount - 1) { + if (self.loadMoreViewId && self.loadMoreViewId.length > 0) { + return [self subModelOf:self.loadMoreViewId]; + } else { + return nil; + } + } + NSString *viewId = self.itemViewIds[@(position)]; + if (viewId && viewId.length > 0) { + return [self subModelOf:viewId]; + } else { + NSInteger batchCount = self.batchCount; + NSInteger start = position; + while (start > 0 && self.itemViewIds[@(start - 1)] == nil) { + start--; + batchCount++; + } + DoricAsyncResult *result = [self pureCallJSResponse:@"renderBunchedItems", @(start), @(batchCount), nil]; + NSArray *array = [result waitUntilResult:^(JSValue *models) { + return [models toArray]; + }]; + [array enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL *stop) { + NSString *thisViewId = obj[@"id"]; + [self setSubModel:obj in:thisViewId]; + NSUInteger pos = start + idx; + self.itemViewIds[@(pos)] = thisViewId; + }]; + viewId = self.itemViewIds[@(position)]; + if (viewId && viewId.length > 0) { + return [self subModelOf:viewId]; + } else { + return nil; + } + } +} + +- (void)blendSubNode:(NSDictionary *)subModel { + ///Here async blend sub node because the item count need to be applied first. + NSUInteger currentCount = self.rowCount; + NSString *viewId = subModel[@"id"]; + DoricViewNode *viewNode = [self subNodeWithViewId:viewId]; + BOOL skipReload = NO; + if (viewNode) { + CGSize originSize = viewNode.view.frame.size; + [viewNode blend:subModel[@"props"]]; + [viewNode.view.doricLayout apply:CGSizeMake(self.view.width, self.view.height)]; + [viewNode requestLayout]; + if (CGSizeEqualToSize(originSize, viewNode.view.frame.size)) { + skipReload = YES; + } + } else { + NSMutableDictionary *model = [[self subModelOf:viewId] mutableCopy]; + [self recursiveMixin:subModel to:model]; + [self setSubModel:model in:viewId]; + } + if (skipReload) { + return; + } + dispatch_async(dispatch_get_main_queue(), ^{ + [self.itemViewIds enumerateKeysAndObjectsUsingBlock:^(NSNumber *_Nonnull key, NSString *_Nonnull obj, BOOL *_Nonnull stop) { + if ([viewId isEqualToString:obj]) { + *stop = YES; + [UIView performWithoutAnimation:^{ + NSUInteger itemCount = self.rowCount; + if (itemCount <= [key integerValue] || currentCount != itemCount) { + [self.view reloadData]; + return; + } + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[key integerValue] inSection:0]; + @try { + [self.view reloadItemsAtIndexPaths:@[indexPath]]; + } + @catch (id exception) { + [self.doricContext.driver.registry onException:exception inContext:self.doricContext]; + } + }]; + } + }]; + }); + +} + +- (void)callItem:(NSUInteger)position width:(CGFloat)width { + NSNumber *old = self.itemWidths[@(position)]; + if (old && [old isEqualToNumber:@(width)]) { + return; + } + NSUInteger currentCount = self.rowCount; + self.itemWidths[@(position)] = @(width); + if (@available(iOS 12.0, *)) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.view.doricLayout.heightSpec == DoricLayoutFit) { + DoricSuperNode *node = self.superNode; + while (node.superNode != nil) { + node = node.superNode; + } + [node requestLayout]; + } + [UIView performWithoutAnimation:^{ + NSUInteger itemCount = self.rowCount; + if (itemCount <= position || currentCount != itemCount) { + return; + } + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:position inSection:0]; + @try { + [self.view reloadItemsAtIndexPaths:@[indexPath]]; + } + @catch (id exception) { + [self.doricContext.driver.registry onException:exception inContext:self.doricContext]; + } + }]; + }); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.view.doricLayout.heightSpec == DoricLayoutFit) { + DoricSuperNode *node = self.superNode; + while (node.superNode != nil) { + node = node.superNode; + } + [node requestLayout]; + } + [self.view reloadData]; + }); + } +} + +- (DoricViewNode *)subNodeWithViewId:(NSString *)viewId { + __block DoricViewNode *ret = nil; + [self.doricContext.driver ensureSyncInMainQueue:^{ + for (UICollectionViewCell *collectionViewCell in self.view.visibleCells) { + if ([collectionViewCell isKindOfClass:[DoricHorizontalTableViewCell class]]) { + DoricHorizontalListItemNode *node = ((DoricHorizontalTableViewCell *) collectionViewCell).doricHorizontalListItemNode; + if ([viewId isEqualToString:node.viewId]) { + ret = node; + break; + } + } + } + }]; + return ret; +} + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + for (DoricDidScrollBlock block in self.didScrollBlocks) { + block(scrollView); + } + if (self.onScrollFuncId) { + if (!self.jsDispatcher) { + self.jsDispatcher = [DoricJSDispatcher new]; + } + __weak typeof(self) __self = self; + [self.jsDispatcher dispatch:^DoricAsyncResult * { + __strong typeof(__self) self = __self; + return [self callJSResponse:self.onScrollFuncId, + @{ + @"x": @(self.view.contentOffset.x), + @"y": @(self.view.contentOffset.y), + }, + nil]; + }]; + } +} + +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + if (self.onScrollEndFuncId) { + [self callJSResponse:self.onScrollEndFuncId, + @{ + @"x": @(self.view.contentOffset.x), + @"y": @(self.view.contentOffset.y), + }, + nil]; + } +} + +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { + if (!decelerate) { + if (self.onScrollEndFuncId) { + [self callJSResponse:self.onScrollEndFuncId, + @{ + @"x": @(self.view.contentOffset.x), + @"y": @(self.view.contentOffset.y), + }, + nil]; + } + } +} + +- (NSMutableSet *)didScrollBlocks { + if (!_didScrollBlocks) { + _didScrollBlocks = [NSMutableSet new]; + } + return _didScrollBlocks; +} + +- (void)addDidScrollBlock:(__nonnull DoricDidScrollBlock)didScrollListener { + [self.didScrollBlocks addObject:didScrollListener]; +} + +- (void)removeDidScrollBlock:(__nonnull DoricDidScrollBlock)didScrollListener { + [self.didScrollBlocks removeObject:didScrollListener]; +} + +- (void)scrollToItem:(NSDictionary *)params { + BOOL animated = [params[@"animated"] boolValue]; + NSUInteger scrolledPosition = [params[@"index"] unsignedIntegerValue]; + + if (scrolledPosition < self.rowCount && scrolledPosition >= 0) { + for (int i = 0; i <= scrolledPosition; i++) { + [self collectionView:self.view cellForItemAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [self.view scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:scrolledPosition inSection:0] atScrollPosition:UICollectionViewScrollPositionNone animated:animated]; + }); + } else { + [self.doricContext.driver.registry onLog:DoricLogTypeError + message:[NSString stringWithFormat:@"scrollToItem Error:%@", @"scrolledPosition range error"]]; + } +} + +- (NSArray *)findVisibleItems { + return [self.view.indexPathsForVisibleItems map:^id(NSIndexPath *obj) { + return @(obj.row); + }]; +} + +- (NSArray *)findCompletelyVisibleItems { + NSArray<__kindof UICollectionViewCell *> *items = [self.view.visibleCells filter:^BOOL(__kindof UICollectionViewCell *obj) { + return CGRectContainsRect(self.view.bounds, obj.frame); + }]; + return [items map:^id(__kindof UICollectionViewCell *obj) { + return @([self.view indexPathForCell:obj].row); + }]; +} + +- (void)reset { + [super reset]; + self.view.scrollEnabled = YES; + self.renderItemFuncId = nil; + self.onLoadMoreFuncId = nil; + self.loadMoreViewId = nil; + self.onScrollFuncId = nil; + self.onScrollEndFuncId = nil; + self.beforeDraggingFuncId = nil; + self.onDraggingFuncId = nil; + self.onDraggedFuncId = nil; + self.loadMore = NO; +} + +- (void)subNodeContentChanged:(DoricViewNode *)subNode { + [subNode.view.doricLayout apply]; + [super subNodeContentChanged:subNode]; +} + +- (void)reload { + self.loadAnchor = -1; + [self.itemViewIds.allValues forEach:^(NSString *obj) { + [self removeSubModel:obj]; + }]; + [self.itemViewIds removeAllObjects]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.rowCount = self.itemCount + (self.loadMore ? 1 : 0); + [self.view reloadData]; + }); +} +@end