blob: 16704985d7d64c44212e92b9a5cfab386e09fa6a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
#import "WXListComponent.h"
#import "WXCellComponent.h"
#import "WXHeaderComponent.h"
#import "WXComponent.h"
#import "WXComponent_internal.h"
#import "WXComponent+Layout.h"
#import "NSArray+Weex.h"
#import "WXAssert.h"
#import "WXMonitor.h"
#import "WXUtility.h"
#import "NSObject+WXSwizzle.h"
#import "WXSDKInstance_private.h"
#import "WXRefreshComponent.h"
#import "WXLoadingComponent.h"
@interface WXListComponent () <UITableViewDataSource, UITableViewDelegate, WXCellRenderDelegate, WXHeaderRenderDelegate>
@property (nonatomic, assign) NSUInteger currentTopVisibleSection;
// Set whether the content offset position all the way to the bottom
@property (assign, nonatomic) BOOL contentAttachBottom;
@end
@interface WXTableView : UITableView
@end
@implementation WXTableView
+ (BOOL)requiresConstraintBasedLayout
{
return NO;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ([(id <WXScrollerProtocol>) self.wx_component respondsToSelector:@selector(requestGestureShouldStopPropagation:shouldReceiveTouch:)]) {
return [(id <WXScrollerProtocol>) self.wx_component requestGestureShouldStopPropagation:gestureRecognizer shouldReceiveTouch:touch];
}
else{
return YES;
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self.wx_component layoutDidFinish];
}
- (void)setContentOffset:(CGPoint)contentOffset
{
// FIXME: side effect caused by hooking _adjustContentOffsetIfNecessary.
// When UITableView is pulled down and finger releases,contentOffset will be set from -xxxx to about -0.5(greater than -0.5), then contentOffset will be reset to zero by calling _adjustContentOffsetIfNecessary.
// So hooking _adjustContentOffsetIfNecessary will always cause remaining 1px space between list's top and navigator.
// Demo: http://dotwe.org/895630945793a9a044e49abe39cbb77f
// Have to reset contentOffset to zero manually here.
if (fabs(contentOffset.y) < 0.5) {
contentOffset.y = 0;
}
if (isnan(contentOffset.x)) {
contentOffset.x = 0;
}
if(isnan(contentOffset.y)) {
contentOffset.y = 0;
}
[super setContentOffset:contentOffset];
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
if (![self.wx_component isKindOfClass:[WXListComponent class]]) return;
BOOL contentAttachBottom = [(WXListComponent *)self.wx_component contentAttachBottom];
if (contentAttachBottom) {
CGFloat offsetHeight = self.contentSize.height - CGRectGetHeight(self.bounds);
if (offsetHeight >= 0) {
[self setContentOffset:CGPointMake(0, offsetHeight) animated:NO];
}
}
}
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
[super setContentOffset:contentOffset animated:animated];
BOOL scrollStartEvent = [[self.wx_component valueForKey:@"_scrollStartEvent"] boolValue];
id scrollEventListener = [self.wx_component valueForKey:@"_scrollEventListener"];
if (animated && (scrollStartEvent ||scrollEventListener) && !WXPointEqualToPoint(contentOffset, self.contentOffset)) {
CGFloat scaleFactor = self.wx_component.weexInstance.pixelScaleFactor;
NSDictionary *contentSizeData = @{@"width":@(self.contentSize.width / scaleFactor),
@"height":@(self.contentSize.height / scaleFactor)};
NSDictionary *contentOffsetData = @{@"x":@(-self.contentOffset.x / scaleFactor),
@"y":@(-self.contentOffset.y / scaleFactor)};
if (scrollStartEvent) {
[self.wx_component fireEvent:@"scrollstart" params:@{@"contentSize":contentSizeData, @"contentOffset":contentOffsetData} domChanges:nil];
}
if (scrollEventListener) {
WXScrollerComponent *component = (WXScrollerComponent *)self.wx_component;
component.scrollEventListener(component, @"scrollstart", @{@"contentSize":contentSizeData, @"contentOffset":contentOffsetData});
}
}
}
@end
// WXText is a non-public is not permitted
@interface WXSectionComponent : NSObject<NSCopying>
@property (nonatomic, strong) WXHeaderComponent *header;
@property (nonatomic, strong) NSMutableArray<WXCellComponent *> *rows;
@end
@implementation WXSectionComponent
- (instancetype)init
{
if (self = [super init]) {
_rows = [NSMutableArray array];
}
return self;
}
- (id)copyWithZone:(NSZone *)zone
{
WXSectionComponent *newSection = [[[self class] allocWithZone:zone] init];
newSection.header = _header;
newSection.rows = [_rows mutableCopyWithZone:zone];
return newSection;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"%@\n%@", [_header description], [_rows description]];
}
@end
@implementation WXListComponent
{
__weak UITableView * _tableView;
// Only accessed on component thread
NSMutableArray<WXSectionComponent *> *_sections;
// Only accessed on main thread
NSMutableArray<WXSectionComponent *> *_completedSections;
NSUInteger _previousLoadMoreRowNumber;
// insert & reload & batch
NSString *_updataType;
BOOL _isUpdating;
NSMutableArray<void(^)(void)> *_updates;
NSTimeInterval _reloadInterval;
CGPoint *_targetContentOffset;
CGPoint _nextStepContentOffset;
}
- (instancetype)initWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events weexInstance:(WXSDKInstance *)weexInstance
{
if (self = [super initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:weexInstance]) {
_sections = [NSMutableArray array];
_completedSections = [NSMutableArray array];
_reloadInterval = attributes[@"reloadInterval"] ? [WXConvert CGFloat:attributes[@"reloadInterval"]]/1000 : 0;
_updataType = [WXConvert NSString:attributes[@"updataType"]]?:@"insert";
_contentAttachBottom = [WXConvert BOOL:attributes[@"contentAttachBottom"]];
[self fixFlicker];
}
return self;
}
- (void)dealloc
{
if (_tableView) {
_tableView.delegate = nil;
_tableView.dataSource = nil;
}
}
- (UIView *)loadView
{
return [[WXTableView alloc] init];
}
- (void)viewDidLoad
{
[super viewDidLoad];
_tableView = (UITableView *)self.view;
_tableView.allowsSelection = NO;
_tableView.allowsMultipleSelection = NO;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.userInteractionEnabled = YES;
_tableView.estimatedRowHeight = 0;
_tableView.estimatedSectionFooterHeight = 0;
_tableView.estimatedSectionHeaderHeight = 0;
}
- (void)viewWillUnload
{
[super viewWillUnload];
_tableView.delegate = nil;
_tableView.dataSource = nil;
}
- (void)updateAttributes:(NSDictionary *)attributes
{
[super updateAttributes:attributes];
if (attributes[@"reloadInterval"]) {
_reloadInterval = [WXConvert CGFloat:attributes[@"reloadInterval"]] / 1000;
}
if (attributes[@"updataType"]) {
_updataType = [WXConvert NSString:attributes[@"updataType"]];
}
if (attributes[@"contentAttachBottom"]) {
_contentAttachBottom = [WXConvert BOOL:attributes[@"contentAttachBottom"]];
}
}
- (void)setContentSize:(CGSize)contentSize
{
// Do Nothing
}
- (void)_handleFirstScreenTime
{
// Do Nothing, firstScreenTime is set by cellDidRendered:
}
- (void)scrollToComponent:(WXComponent *)component withOffset:(CGFloat)offset animated:(BOOL)animated
{
UIScrollView *scrollView = (UIScrollView *)self.view;
// http://dotwe.org/vue/aa1af34e5fc745c0f1520e346904682a
// ignore scroll action if contentSize smaller than scroller frame
if (scrollView.contentSize.height < scrollView.frame.size.height) {
return;
}
CGPoint contentOffset = _tableView.contentOffset;
CGFloat contentOffsetY = 0;
WXComponent *cellComponent = component;
CGRect cellRect;
while (cellComponent) {
if ([cellComponent isKindOfClass:[WXCellComponent class]]) {
NSIndexPath *toIndexPath = [self indexPathForCell:(WXCellComponent*)cellComponent sections:_completedSections];
cellRect = [_tableView rectForRowAtIndexPath:toIndexPath];
break;
}
if ([cellComponent isKindOfClass:[WXHeaderComponent class]]) {
NSUInteger toIndex = [self indexForHeader:(WXHeaderComponent *)cellComponent sections:_completedSections];
cellRect = [_tableView rectForSection:toIndex];
break;
}
contentOffsetY += cellComponent.calculatedFrame.origin.y;
cellComponent = cellComponent.supercomponent;
}
contentOffsetY += cellRect.origin.y;
contentOffsetY += offset * self.weexInstance.pixelScaleFactor;
if (self.snapData.useSnap) {
CGFloat snapOffset = [self.snapData calcScrollSnapPositionOffset];
contentOffsetY -= (offset * self.weexInstance.pixelScaleFactor + snapOffset);
if (self.snapData.alignment == WXScrollSnapAlignCenter) {
contentOffsetY += (cellRect.size.height / 2);
} else if (self.snapData.alignment == WXScrollSnapAlignEnd) {
contentOffsetY += cellRect.size.height;
}
if (contentOffsetY < 0) {
contentOffsetY = 0;
}
}
if (_tableView.contentSize.height >= _tableView.frame.size.height && contentOffsetY > _tableView.contentSize.height - _tableView.frame.size.height) {
contentOffset.y = _tableView.contentSize.height - _tableView.frame.size.height;
} else {
contentOffset.y = contentOffsetY;
}
[_tableView setContentOffset:contentOffset animated:animated];
}
#pragma mark - Inheritance
- (BOOL)_insertSubcomponent:(WXComponent *)subcomponent atIndex:(NSInteger)index
{
if ([subcomponent isKindOfClass:[WXCellComponent class]]) {
((WXCellComponent *)subcomponent).delegate = self;
} else if ([subcomponent isKindOfClass:[WXHeaderComponent class]]) {
((WXHeaderComponent *)subcomponent).delegate = self;
} else if (![subcomponent isKindOfClass:[WXRefreshComponent class]]
&& ![subcomponent isKindOfClass:[WXLoadingComponent class]]
&& subcomponent->_positionType != WXPositionTypeFixed) {
WXLogError(@"list only support cell/header/refresh/loading/fixed-component as child.");
subcomponent->_isViewTreeIgnored = YES; // do not show this element.
}
BOOL inserted = [super _insertSubcomponent:subcomponent atIndex:index];
if (![subcomponent isKindOfClass:[WXHeaderComponent class]]
&& ![subcomponent isKindOfClass:[WXCellComponent class]]) {
// Don't insert section if subcomponent is not header or cell
return inserted;
}
NSIndexPath *indexPath = [self indexPathForSubIndex:index];
if ([subcomponent isKindOfClass:[WXHeaderComponent class]] || _sections.count <= indexPath.section) {
// conditions to insert section: insert a header or insert first cell of table view
// this will be updated by recycler's update controller in the future
WXSectionComponent *insertSection = [WXSectionComponent new];
BOOL keepScrollPosition = NO;
if ([subcomponent isKindOfClass:[WXHeaderComponent class]]) {
WXHeaderComponent *header = (WXHeaderComponent*)subcomponent;
insertSection.header = header;
keepScrollPosition = header.keepScrollPosition;
}
NSUInteger insertIndex = indexPath.section;
WXSectionComponent *reloadSection;
if (insertIndex > 0 && insertIndex <= _sections.count
&& [subcomponent isKindOfClass:[WXHeaderComponent class]]) {
// insert a header in the middle, one section may divide into two
// so the original section need to be reloaded
/*
Here we may encounter a problem that _sections is not always containing all cells of list.
Because cell are not added to _sections until cellDidLayout. So if a cell is not added to _sections,
NSIndexPath *indexPathBeforeHeader = [self indexPathForSubIndex:index - 1];
The indexPathForSubIndex method use all sub components of list to calculate row in section. This would
be incorrect if a cell is not added to _sections. And the split is incorrect resulting some cells put to
wrong WXSectionComponent and then UITableView crash.
In fixed version, we use _subcomponents[index - 1] to get the last component that should be put to original section
and get the index of it in section rows.
*/
if (_sections[insertIndex - 1].rows.count > 0) {
WXComponent* componentBeforeHeader = _subcomponents[index - 1];
NSArray *rowsToSeparate = _sections[insertIndex - 1].rows;
NSUInteger indexOfLastComponentAfterSeparate = [rowsToSeparate indexOfObject:componentBeforeHeader];
if (indexOfLastComponentAfterSeparate != NSNotFound && componentBeforeHeader != [rowsToSeparate lastObject]) {
reloadSection = _sections[insertIndex - 1];
insertSection.rows = [[rowsToSeparate subarrayWithRange:NSMakeRange(indexOfLastComponentAfterSeparate + 1, rowsToSeparate.count - (indexOfLastComponentAfterSeparate + 1))] mutableCopy];
reloadSection.rows = [[rowsToSeparate subarrayWithRange:NSMakeRange(0, indexOfLastComponentAfterSeparate + 1)] mutableCopy];
}
}
// This is wrong!!!
// NSIndexPath *indexPathBeforeHeader = [self indexPathForSubIndex:index - 1];
// if (_sections[insertIndex - 1].rows.count != 0 && indexPathBeforeHeader.row < _sections[insertIndex - 1].rows.count - 1) {
// reloadSection = _sections[insertIndex - 1];
// NSArray *rowsToSeparate = reloadSection.rows;
// insertSection.rows = [[rowsToSeparate subarrayWithRange:NSMakeRange(indexPathBeforeHeader.row + 1, rowsToSeparate.count - indexPathBeforeHeader.row - 1)] mutableCopy];
// reloadSection.rows = [[rowsToSeparate subarrayWithRange:NSMakeRange(0, indexPathBeforeHeader.row + 1)] mutableCopy];
// }
}
[_sections insertObject:insertSection atIndex:insertIndex];
WXSectionComponent *completedInsertSection = [insertSection copy];
WXSectionComponent *completedReloadSection;
if (reloadSection) {
completedReloadSection = [reloadSection copy];
}
[self.weexInstance.componentManager _addUITask:^{
WXLogDebug(@"Insert section:%ld", (unsigned long)insertIndex);
[UIView performWithoutAnimation:^{
@try {
[_tableView beginUpdates];
[_completedSections insertObject:completedInsertSection atIndex:insertIndex];
if (completedReloadSection) {
WXLogDebug(@"Reload section:%lu", (unsigned long)(insertIndex - 1));
_completedSections[insertIndex - 1] = completedReloadSection;
}
[self _insertTableViewSectionAtIndex:insertIndex keepScrollPosition:keepScrollPosition animation:UITableViewRowAnimationNone];
if (completedReloadSection) {
[_tableView reloadSections:[NSIndexSet indexSetWithIndex:insertIndex - 1] withRowAnimation:UITableViewRowAnimationNone];
}
[_tableView endUpdates];
} @catch (NSException *exception) {
WXLogError(@"list insert component occurs exception %@", exception);
}
}];
}];
}
return inserted;
}
- (void)insertSubview:(WXComponent *)subcomponent atIndex:(NSInteger)index
{
//Here will not insert cell or header's view again
if (![subcomponent isKindOfClass:[WXCellComponent class]]
&& ![subcomponent isKindOfClass:[WXHeaderComponent class]]) {
[super insertSubview:subcomponent atIndex:index];
}
}
#pragma mark - WXHeaderRenderDelegate
- (float)headerWidthForLayout:(WXHeaderComponent *)cell
{
return [self safeContainerStyleWidth];
}
- (void)headerDidLayout:(WXHeaderComponent *)header
{
[self.weexInstance.componentManager _addUITask:^{
// trigger section header update
[UIView performWithoutAnimation:^{
[_tableView beginUpdates];
NSUInteger reloadIndex = [self indexForHeader:header sections:_completedSections];
[_tableView reloadSections:[NSIndexSet indexSetWithIndex:reloadIndex] withRowAnimation:UITableViewRowAnimationNone];
[_tableView endUpdates];
}];
}];
}
- (void)headerDidRemove:(WXHeaderComponent *)header
{
NSUInteger headerIndex = [self indexForHeader:header sections:_sections];
// this will be updated by recycler's update controller in the future
WXSectionComponent *headerSection = _sections[headerIndex];
WXSectionComponent *reloadSection;
NSUInteger reloadIndex = -1;
BOOL isDeleteSection = NO;
if (headerIndex == 0 && headerSection.rows.count > 0) {
// delete a header in the first section and the section still has cells
// reload the first section
reloadIndex = 0;
reloadSection = _sections[reloadIndex];
_sections[reloadIndex].header = nil;
} else if (headerIndex > 0 && headerSection.rows.count > 0) {
// delete a header in the middle, two sections merge into one
// so one section need to be deleted and the other should be relo
isDeleteSection = YES;
reloadIndex = headerIndex - 1;
reloadSection = _sections[reloadIndex];
reloadSection.rows = [[reloadSection.rows arrayByAddingObjectsFromArray:headerSection.rows] mutableCopy];
[_sections removeObjectAtIndex:headerIndex];
} else {
// delete a header with no cell in that section
// just delete the section
isDeleteSection = YES;
[_sections removeObjectAtIndex:headerIndex];
}
WXSectionComponent *completedReloadSection;
if (reloadSection) {
completedReloadSection = [reloadSection copy];
}
BOOL keepScrollPosition = header.keepScrollPosition;
[self.weexInstance.componentManager _addUITask:^{
if (isDeleteSection) {
WXLogDebug(@"delete section:%lu", (unsigned long)headerIndex);
[_completedSections removeObjectAtIndex:headerIndex];
}
if (reloadIndex == 0 && !isDeleteSection) {
_completedSections[reloadIndex].header = nil;
}
if (completedReloadSection) {
WXLogDebug(@"Reload section:%lu", (unsigned long)reloadIndex);
_completedSections[reloadIndex] = completedReloadSection;
}
[UIView performWithoutAnimation:^{
[_tableView beginUpdates];
if (isDeleteSection) {
[self _deleteTableViewSectionAtIndex:headerIndex keepScrollPosition:keepScrollPosition animation:UITableViewRowAnimationNone];
}
if (completedReloadSection) {
[_tableView reloadSections:[NSIndexSet indexSetWithIndex:reloadIndex] withRowAnimation:UITableViewRowAnimationNone];
}
[_tableView endUpdates];
}];
}];
}
#pragma mark - WXCellRenderDelegate
- (float)containerWidthForLayout:(WXCellComponent *)cell
{
return [self safeContainerStyleWidth];
}
- (void)cellDidRemove:(WXCellComponent *)cell
{
WXAssertComponentThread();
NSIndexPath *indexPath = [self indexPathForCell:cell sections:_sections];
if(!indexPath){
//protect when cell not exist in sections
return;
}
[self removeCellForIndexPath:indexPath withSections:_sections];
[self.weexInstance.componentManager _addUITask:^{
[self removeCellForIndexPath:indexPath withSections:_completedSections];
WXLogDebug(@"Delete cell:%@ at indexPath:%@", cell.ref, indexPath);
if (cell.deleteAnimation == UITableViewRowAnimationNone) {
[UIView performWithoutAnimation:^{
[self _deleteTableViewCellAtIndexPath:indexPath keepScrollPosition:cell.keepScrollPosition animation:UITableViewRowAnimationNone];
}];
} else {
[self _deleteTableViewCellAtIndexPath:indexPath keepScrollPosition:cell.keepScrollPosition animation:cell.deleteAnimation];
}
}];
}
- (void)cellDidLayout:(WXCellComponent *)cell
{
WXAssertComponentThread() ;
NSUInteger index = [self.subcomponents indexOfObject:cell];
NSIndexPath *indexPath = [self indexPathForSubIndex:index];
NSInteger sectionNum = indexPath.section;
if (sectionNum >= [_sections count] || sectionNum < 0) {
// try to protect sectionNum out of range.
return;
}
NSInteger row = indexPath.row;
NSMutableArray *sections = _sections;
WXSectionComponent *section = sections[sectionNum];
WXAssert(section, @"no section found for section number:%ld", sectionNum);
NSMutableArray *completedSections;
BOOL isReload = [section.rows containsObject:cell];
if (!isReload && row > [section.rows count]) {
// protect crash when row out of bounds
return ;
}
if (!isReload) {
[section.rows insertObject:cell atIndex:row];
// deep copy
completedSections = [[NSMutableArray alloc] initWithArray:sections copyItems:YES];;
}
[self.weexInstance.componentManager _addUITask:^{
if (!isReload) {
WXLogDebug(@"Insert cell:%@ at indexPath:%@", cell.ref, indexPath);
_completedSections = completedSections;
if (cell.insertAnimation == UITableViewRowAnimationNone) {
[UIView performWithoutAnimation:^{
[self _insertTableViewCellAtIndexPath:indexPath keepScrollPosition:cell.keepScrollPosition animation:UITableViewRowAnimationNone];
}];
} else {
[self _insertTableViewCellAtIndexPath:indexPath keepScrollPosition:cell.keepScrollPosition animation:cell.insertAnimation];
}
} else {
WXLogInfo(@"Reload cell:%@ at indexPath:%@", cell.ref, indexPath);
[UIView performWithoutAnimation:^{
[_tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
[self handleAppear];
}];
}
}];
}
- (void)cellDidRendered:(WXCellComponent *)cell
{
WXAssertMainThread();
if (WX_MONITOR_INSTANCE_PERF_IS_RECORDED(WXPTFirstScreenRender, self.weexInstance) && !self.weexInstance.onRenderProgress) {
// improve performance
return;
}
NSIndexPath *indexPath = [self indexPathForCell:cell sections:_completedSections];
if (!indexPath || indexPath.section >= [_tableView numberOfSections] ||
indexPath.row < 0 || indexPath.row >= [_tableView numberOfRowsInSection:indexPath.section]) {
WXLogWarning(@"Rendered cell:%@ out of range, sections:%@", cell, _completedSections);
return;
}
CGRect cellRect = [_tableView rectForRowAtIndexPath:indexPath];
if (cellRect.origin.y + cellRect.size.height >= _tableView.frame.size.height) {
WX_MONITOR_INSTANCE_PERF_END(WXPTFirstScreenRender, self.weexInstance);
}
if (self.weexInstance.onRenderProgress) {
CGRect renderRect = [_tableView convertRect:cellRect toView:self.weexInstance.rootView];
self.weexInstance.onRenderProgress(renderRect);
}
}
- (void)cell:(WXCellComponent *)cell didMoveToIndex:(NSUInteger)index
{
WXAssertComponentThread();
NSIndexPath *fromIndexPath = [self indexPathForCell:cell sections:_sections];
NSIndexPath *toIndexPath = [self indexPathForSubIndex:index];
if (toIndexPath.row > [_sections[toIndexPath.section].rows count] || toIndexPath.row < 0) {
//FIXME: WXLogError trigger crash
// WXLogError(@"toIndexPath %@ is out of range as the current is %lu",toIndexPath ,(unsigned long)[_sections[toIndexPath.section].rows count]);
return;
}
[self removeCellForIndexPath:fromIndexPath withSections:_sections];
[self insertCell:cell forIndexPath:toIndexPath withSections:_sections];
[self.weexInstance.componentManager _addUITask:^{
if (_reloadInterval > 0) {
// use [UITableView reloadData] to do batch updates, will move to recycler's update controller
__weak typeof(self) weakSelf = self;
if (!_updates) {
_updates = [NSMutableArray array];
}
[_updates addObject:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf removeCellForIndexPath:fromIndexPath withSections:strongSelf->_completedSections];
[strongSelf insertCell:cell forIndexPath:toIndexPath withSections:strongSelf->_completedSections];
}];
[self checkReloadData];
} else {
[self removeCellForIndexPath:fromIndexPath withSections:_completedSections];
[self insertCell:cell forIndexPath:toIndexPath withSections:_completedSections];
[UIView performWithoutAnimation:^{
@try {
[_tableView beginUpdates];
[_tableView moveRowAtIndexPath:fromIndexPath toIndexPath:toIndexPath];
[self handleAppear];
[_tableView endUpdates];
}@catch(NSException * exception){
WXLogDebug(@"move cell exception: %@", [exception description]);
}
}];
}
}];
}
- (void)checkReloadData
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_reloadInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (_isUpdating || _updates.count == 0) {
return ;
}
_isUpdating = YES;
NSArray *updates = [_updates copy];
[_updates removeAllObjects];
for (void(^update)(void) in updates) {
update();
}
[_tableView reloadData];
_isUpdating = NO;
[self checkReloadData];
});
}
- (void)addStickyComponent:(WXComponent *)sticky
{
}
- (void)removeStickyComponent:(WXComponent *)sticky
{
}
#pragma mark - TableView delegate
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
WXLogDebug(@"Did end displaying cell:%@, at index path:%@", cell, indexPath);
NSArray *visibleIndexPaths = [tableView indexPathsForVisibleRows];
if (![visibleIndexPaths containsObject:indexPath]) {
if (cell.contentView.subviews.count > 0) {
UIView *wxCellView = [cell.contentView.subviews firstObject];
// Must invoke synchronously otherwise it will remove the view just added.
WXCellComponent *cellComponent = (WXCellComponent *)wxCellView.wx_component;
if (cellComponent.isRecycle) {
[cellComponent _unloadViewWithReusing:YES];
}
}
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
WXCellComponent *cell = [self cellForIndexPath:indexPath];
return cell.calculatedFrame.size.height;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
WXHeaderComponent *header = ((WXSectionComponent *)[_completedSections wx_safeObjectAtIndex:section]).header;
if (header) {
return header.calculatedFrame.size.height;
} else {
return 0.0;
}
}
- (NSIndexPath *)getNeighbouringIndexPath:(NSIndexPath *)currentIndexPath findNext:(BOOL)findNext {
NSIndexPath *neighbourIndexPath;
if (findNext) {
if (currentIndexPath.row == _completedSections[currentIndexPath.section].rows.count - 1) {
if (currentIndexPath.section == _completedSections.count-1) {// the last cell
neighbourIndexPath = [NSIndexPath indexPathForRow:currentIndexPath.row inSection:currentIndexPath.section];
} else {// the first cell on next section
neighbourIndexPath = [NSIndexPath indexPathForRow:0 inSection:currentIndexPath.section+1];
}
} else {// the next cell
neighbourIndexPath = [NSIndexPath indexPathForRow:currentIndexPath.row+1 inSection:currentIndexPath.section];
}
} else {
if (currentIndexPath.row == 0) {
if (currentIndexPath.section == 0) {// the first cell
neighbourIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
} else {// the last cell on previous section
neighbourIndexPath = [NSIndexPath indexPathForRow:_completedSections[currentIndexPath.section-1].rows.count-1 inSection:currentIndexPath.section-1];
}
} else {// the previous cell
neighbourIndexPath = [NSIndexPath indexPathForRow:currentIndexPath.row-1 inSection:currentIndexPath.section];
}
}
return neighbourIndexPath;
}
- (CGPoint)calculateSnapPosition:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity startPosition:(CGPoint)startPosition targetPosition:(CGPoint)preTargetPosition{
[self.snapData bindingScrollView:scrollView];
WXTableView *tableView = (WXTableView *)scrollView;
/// The offset for start position, to avoid the start position error in continuous sliding if last sliding was not finished
CGFloat kstartPositionOffset = 10.f;
/// The snap point of scroll container when finger touch down
self.snapData.startPosition = startPosition;
CGPoint snapContainerPosition, currentOffset;
CGPoint currentPoint = scrollView.contentOffset;
/// The offset of the snap point relative to the container vertex
CGFloat snapOffset = [self.snapData calcScrollSnapPositionOffset];
if (self.scrollDirection == WXScrollDirectionHorizontal) {
currentOffset = CGPointMake(currentPoint.x-startPosition.x, currentPoint.y);
snapContainerPosition = CGPointMake(startPosition.x + snapOffset, startPosition.y);
} else {
currentOffset = CGPointMake(currentPoint.x, currentPoint.y-startPosition.y);
snapContainerPosition = CGPointMake(startPosition.x, startPosition.y + snapOffset);
}
/// Calculate snap staus
WXScrollSnapStatus snapStatus = [self.snapData shouldTriggerSnap:currentOffset velocity:velocity];
CGPoint targetContentOffset = startPosition;
if (currentPoint.x < 0 || currentPoint.y < 0) {
return targetContentOffset;
}
/// Bounce to origin offset
if (snapStatus == WXScrollSnapNone) {
return targetContentOffset;
}
/// Determine the start position, if align-start + 4, else if align-end - 4
CGPoint correctSnapPosition = snapContainerPosition;
if (self.scrollDirection == WXScrollDirectionHorizontal) {
switch (self.snapData.alignment) {
case WXScrollSnapAlignStart:
correctSnapPosition.x += kstartPositionOffset;
break;
case WXScrollSnapAlignEnd:
correctSnapPosition.x -= kstartPositionOffset;
break;
default:
break;
}
} else {
switch (self.snapData.alignment) {
case WXScrollSnapAlignStart:
correctSnapPosition.y += kstartPositionOffset;
break;
case WXScrollSnapAlignEnd:
correctSnapPosition.y -= kstartPositionOffset;
break;
default:
break;
}
}
/// The cell corresponding to the starting point
NSIndexPath *beginIndexPath = [tableView indexPathForRowAtPoint:correctSnapPosition];
CGRect beginCellRect = [tableView rectForRowAtIndexPath:beginIndexPath];
if (CGRectIsNull(beginCellRect)) {
return targetContentOffset;
}
NSIndexPath *targetIndexPath = beginIndexPath;
if (snapStatus == WXScrollSnapStay) {
// Do nothing
} else {
NSIndexPath *lastIndexPath = beginIndexPath;
targetIndexPath = [self getNeighbouringIndexPath:beginIndexPath findNext:(snapStatus == WXScrollSnapToNext)];
WXCellComponent *cell = [self cellForIndexPath:targetIndexPath];
while (cell.ignoreScrollSnap && [lastIndexPath compare:targetIndexPath] != NSOrderedSame) {
lastIndexPath = targetIndexPath;
targetIndexPath = [self getNeighbouringIndexPath:targetIndexPath findNext:(snapStatus == WXScrollSnapToNext)];
cell = [self cellForIndexPath:targetIndexPath];
}
}
CGRect targetCellRect = [tableView rectForRowAtIndexPath:targetIndexPath];
if (self.scrollDirection == WXScrollDirectionVertical) {
targetContentOffset.y = targetCellRect.origin.y - snapOffset;
if (self.snapData.alignment == WXScrollSnapAlignCenter) {
targetContentOffset.y += (targetCellRect.size.height / 2);
} else if (self.snapData.alignment == WXScrollSnapAlignEnd) {
targetContentOffset.y += targetCellRect.size.height;
}
if (targetContentOffset.y < 0) {
targetContentOffset.y = 0;
}
} else {
targetContentOffset.x = targetCellRect.origin.x - snapOffset;
if (self.snapData.alignment == WXScrollSnapAlignCenter) {
targetContentOffset.x += (targetCellRect.size.width / 2);
} else if (self.snapData.alignment == WXScrollSnapAlignEnd) {
targetContentOffset.x += targetCellRect.size.width;
}
if (targetContentOffset.x < 0) {
targetContentOffset.x = 0;
}
}
WXLogInfo(@"[scroll snap] veloc:%.2f (%.2f,%.2f)=>(%.2f,%.2f)", velocity.y, startPosition.x, startPosition.y, targetContentOffset.x, targetContentOffset.y);
self.snapData.targetIndexPath = targetIndexPath;
self.snapData.snapping = true;
return targetContentOffset;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[super scrollViewDidScroll:scrollView];
if ([[_tableView indexPathsForVisibleRows] count] > 0) {
NSIndexPath *topCellPath = [[_tableView indexPathsForVisibleRows] objectAtIndex:0];
if (self.currentTopVisibleSection != topCellPath.section) {
if (self.currentTopVisibleSection) {
WXSectionComponent *removeSection = [_sections wx_safeObjectAtIndex:self.currentTopVisibleSection];
if (removeSection.header && [removeSection.header.events containsObject:@"unsticky"]) {
[removeSection.header fireEvent:@"unsticky" params:nil];
}
}
self.currentTopVisibleSection = topCellPath.section;
WXSectionComponent *showSection = [_sections wx_safeObjectAtIndex:topCellPath.section];
if (showSection.header && [showSection.header.events containsObject:@"sticky"]) {
[showSection.header fireEvent:@"sticky" params:nil];
}
}
}
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
WXHeaderComponent *header = ((WXSectionComponent *)_completedSections[section]).header;
WXLogDebug(@"header view for section %ld:%@", (long)section, header.view);
return header.view;
}
#pragma mark - TableView Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return _completedSections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return ((WXSectionComponent *)[_completedSections wx_safeObjectAtIndex:section]).rows.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
WXLogDebug(@"Getting cell at indexPath:%@", indexPath);
static NSString *reuseIdentifier = @"WXTableViewCell";
UITableViewCell *cellView = [_tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
if (!cellView) {
cellView = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
cellView.backgroundColor = [UIColor clearColor];
}
WXCellComponent *cell = [self cellForIndexPath:indexPath];
if (cell.zIndex) {
cellView.layer.zPosition = [WXConvert CGFloat:cell.zIndex];
}
if (!cell) {
return cellView;
}
if (cell.view.superview == cellView.contentView) {
return cellView;
}
for (UIView *view in cellView.contentView.subviews) {
[view removeFromSuperview];
}
[cellView.contentView addSubview:cell.view];
[cellView setAccessibilityIdentifier:cell.view.accessibilityIdentifier];
WXLogDebug(@"Created cell:%@ view:%@ cellView:%@ at indexPath:%@", cell.ref, cell.view, cellView, indexPath);
return cellView;
}
#pragma mark - Load More Event
- (void)setLoadmoreretry:(NSUInteger)loadmoreretry
{
if (loadmoreretry != self.loadmoreretry) {
_previousLoadMoreRowNumber = 0;
}
[super setLoadmoreretry:loadmoreretry];
}
- (void)loadMore
{
[super loadMore];
_previousLoadMoreRowNumber = [self totalNumberOfRows];
}
- (BOOL)isNeedLoadMore
{
BOOL superNeedLoadMore = [super isNeedLoadMore];
return superNeedLoadMore && _previousLoadMoreRowNumber != [self totalNumberOfRows];
}
- (NSUInteger)totalNumberOfRows
{
NSUInteger rowNumber = 0;
NSUInteger sectionCount = [_tableView numberOfSections];
for (int section = 0; section < sectionCount; section ++) {
rowNumber += [_tableView numberOfRowsInSection:section];
}
return rowNumber;
}
- (void)resetLoadmore{
[super resetLoadmore];
_previousLoadMoreRowNumber=0;
}
#pragma mark Private
- (WXCellComponent *)cellForIndexPath:(NSIndexPath *)indexPath
{
WXSectionComponent *section = [_completedSections wx_safeObjectAtIndex:indexPath.section];
if (!section) {
WXLogError(@"No section found for num:%ld, completed sections:%ld", (long)indexPath.section, (unsigned long)_completedSections.count);
return nil;
}
WXCellComponent *cell = [section.rows wx_safeObjectAtIndex:indexPath.row];
if (!cell) {
WXLogError(@"No cell found for num:%ld, completed rows:%ld", (long)indexPath.row, (unsigned long)section.rows.count);
return nil;
}
return cell;
}
- (void)insertCell:(WXCellComponent *)cell forIndexPath:(NSIndexPath *)indexPath withSections:(NSMutableArray *)sections
{
WXSectionComponent *section = [sections wx_safeObjectAtIndex:indexPath.section];
if (indexPath.row > [section.rows count] || indexPath.row < 0) {
WXLogError(@"inserting cell at indexPath:%@ outof range, sections:%@", indexPath, sections);
return;
}
WXAssert(section, @"inserting cell at indexPath:%@ section has not been inserted to list before, sections:%@", indexPath, sections);
WXAssert(indexPath.row <= section.rows.count, @"inserting cell at indexPath:%@ outof range, sections:%@", indexPath, sections);
[section.rows insertObject:cell atIndex:indexPath.row];
}
- (void)removeCellForIndexPath:(NSIndexPath *)indexPath withSections:(NSMutableArray *)sections
{
WXSectionComponent *section = [sections wx_safeObjectAtIndex:indexPath.section];
if (0 == [section.rows count]) {
return;
}
WXAssert(section, @"Removing cell at indexPath:%@ has not been inserted to cell list before, sections:%@", indexPath, sections);
WXAssert(indexPath.row < section.rows.count, @"Removing cell at indexPath:%@ outof range, sections:%@", indexPath, sections);
[section.rows removeObjectAtIndex:indexPath.row];
}
- (NSIndexPath *)indexPathForCell:(WXCellComponent *)cell sections:(NSMutableArray<WXSectionComponent *> *)sections
{
__block NSIndexPath *indexPath;
[sections enumerateObjectsUsingBlock:^(WXSectionComponent * _Nonnull section, NSUInteger sectionIndex, BOOL * _Nonnull sectionStop) {
[section.rows enumerateObjectsUsingBlock:^(WXCellComponent * _Nonnull row, NSUInteger rowIndex, BOOL * _Nonnull stop) {
if (row == cell) {
indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex];
*stop = YES;
*sectionStop = YES;
}
}];
}];
return indexPath;
}
- (NSUInteger)indexForHeader:(WXHeaderComponent *)header sections:(NSMutableArray<WXSectionComponent *> *)sections
{
__block NSUInteger index;
[sections enumerateObjectsUsingBlock:^(WXSectionComponent * _Nonnull section, NSUInteger sectionIndex, BOOL * _Nonnull stop) {
if (section.header == header) {
index = sectionIndex;
*stop = YES;
}
}];
return index;
}
- (NSIndexPath *)indexPathForSubIndex:(NSUInteger)index
{
NSInteger section = 0;
NSInteger row = -1;
WXComponent *firstComponent;
for (int i = 0; i <= index; i++) {
WXComponent* component = [self.subcomponents wx_safeObjectAtIndex:i];
if (!component) {
continue;
}
if (([component isKindOfClass:[WXHeaderComponent class]]
|| [component isKindOfClass:[WXCellComponent class]])
&& !firstComponent) {
firstComponent = component;
}
if (component != firstComponent && [component isKindOfClass:[WXHeaderComponent class]]) {
section ++;
row = -1;
}
if ([component isKindOfClass:[WXCellComponent class]]) {
row ++;
}
}
return [NSIndexPath indexPathForRow:row inSection:section];
}
- (void)_performUpdates:(void(^)(void))updates withKeepScrollPosition:(BOOL)keepScrollPosition adjustmentBlock:(CGFloat(^)(NSIndexPath *topVisibleCell))adjustmentBlock
{
CGFloat adjustment = 0;
// keep the scroll position when inserting or deleting sections/rows by adjusting the content offset
if (keepScrollPosition) {
NSIndexPath *top = _tableView.indexPathsForVisibleRows.firstObject;
adjustment = adjustmentBlock(top);
}
updates();
if (keepScrollPosition) {
CGPoint afterContentOffset = _tableView.contentOffset;
CGPoint newContentOffset = CGPointMake(afterContentOffset.x, afterContentOffset.y + ceilf(adjustment));
_tableView.contentOffset = newContentOffset;
}
[self handleAppear];
}
- (void)_insertTableViewSectionAtIndex:(NSUInteger)section keepScrollPosition:(BOOL)keepScrollPosition animation:(UITableViewRowAnimation)animation
{
[self _performUpdates:^{
// catch system exception under 11.2 https://forums.developer.apple.com/thread/49676
@try {
[_tableView insertSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:animation];
} @catch(NSException *) {//!OCLint
}
} withKeepScrollPosition:keepScrollPosition adjustmentBlock:^CGFloat(NSIndexPath *top) {
if (section <= top.section) {
return [self tableView:_tableView heightForHeaderInSection:section];
} else {
return 0.0;
}
}];
}
- (void)_deleteTableViewSectionAtIndex:(NSUInteger)section keepScrollPosition:(BOOL)keepScrollPosition animation:(UITableViewRowAnimation)animation
{
[self _performUpdates:^{
// catch system exception under 11.2 https://forums.developer.apple.com/thread/49676
@try {
[_tableView deleteSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:animation];
} @catch(NSException *) {//!OCLint
}
} withKeepScrollPosition:keepScrollPosition adjustmentBlock:^CGFloat(NSIndexPath *top) {
if (section <= top.section) {
return [self tableView:_tableView heightForHeaderInSection:section];
} else {
return 0.0;
}
}];
}
- (void)_insertTableViewCellAtIndexPath:(NSIndexPath *)indexPath keepScrollPosition:(BOOL)keepScrollPosition animation:(UITableViewRowAnimation)animation
{
[self _performUpdates:^{
if ([_updataType isEqual: @"reload"]) {
[_tableView reloadData];
} else {
// catch system exception under 11.2 https://forums.developer.apple.com/thread/49676
@try {
[_tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:animation];
} @catch(NSException *e) {//!OCLint
}
}
} withKeepScrollPosition:keepScrollPosition adjustmentBlock:^CGFloat(NSIndexPath *top) {
if (([indexPath compare:top] <= 0) || [_updataType isEqual: @"reload"]) {
return [self tableView:_tableView heightForRowAtIndexPath:indexPath];
} else {
return 0.0;
}
}];
}
- (void)_deleteTableViewCellAtIndexPath:(NSIndexPath *)indexPath keepScrollPosition:(BOOL)keepScrollPosition animation:(UITableViewRowAnimation)animation
{
if (!indexPath) {
return ;
}
[self _performUpdates:^{
// catch system exception under 11.2 https://forums.developer.apple.com/thread/49676
@try {
[_tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:animation];
} @catch (NSException* e) {//!OCLint
}
} withKeepScrollPosition:keepScrollPosition adjustmentBlock:^CGFloat(NSIndexPath *top) {
if ([indexPath compare:top] <= 0) {
return [self tableView:_tableView heightForRowAtIndexPath:indexPath];
} else {
return 0.0;
}
}];
}
// Hook _adjustContentOffsetIfNecessary will cause UITableView freezing if bounces is set to NO.
- (void)fixFlicker
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// FIXME:(ง •̀_•́)ง┻━┻ Stupid scoll view, always reset content offset to zero by calling _adjustContentOffsetIfNecessary after insert cells.
// So if you pull down list while list is rendering, the list will be flickering.
// Demo:
// Have to hook _adjustContentOffsetIfNecessary here.
// Any other more elegant way?
NSString *a = @"ntOffsetIfNe";
NSString *b = @"adjustConte";
NSString *originSelector = [NSString stringWithFormat:@"_%@%@cessary", b, a];
[[self class] weex_swizzle:[WXTableView class] Method:NSSelectorFromString(originSelector) withMethod:@selector(fixedFlickerSelector)];
});
}
- (void)fixedFlickerSelector
{
// DO NOT delete this method.
}
@end