先说下下拉刷新动画效果的实现,重写写一个动画的类,并且重写prepare方法,在这里面添加UI,并在placeSubviews方法中设置她的frame和坐标,因为placeSubviews方法是写在layoutSubviews方法里面的。
- (void)prepare {
[super prepare];
[self addSubview:self.gifImageView];
}
- (void)placeSubviews {
[super placeSubviews];
CGFloat stateTextWidth = self.stateLabel.textWidth;
CGFloat lastTimeTextWidth = self.lastUpdatedTimeLable.textWidth;
CGFloat finalTextWidth = MAX(stateTextWidth, lastTimeTextWidth);
_gifImageView.center = CGPointMake((self.eoc_w - finalTextWidth)/4, self.eoc_h/2-20.f);
_gifImageView.image = [_stateImages[@(EOCRefreshStateIdle)] firstObject];
_gifImageView.eoc_size = _gifImageView.image.size;
}
其次动画是一帧一帧的,我们根据下拉的比例来决定显示哪张照片,因为GIF动画其实是一组照片依次显示出来的。
- (void)setPullingPercent:(CGFloat)pullingPercent {
[super setPullingPercent:pullingPercent];
NSArray *images = self.stateImages[@(EOCRefreshStateIdle)];
if (self.state != EOCRefreshStateIdle || images.count == 0) return;
// 停止动画
[self.gifImageView stopAnimating];
// 设置当前需要显示的图片
NSUInteger index = images.count * pullingPercent;
if (index >= images.count) index = images.count - 1;
self.gifImageView.image = images[index];
}
最后在刷新和下拉状态的时候开始动画,在闲置状态的时候结束动画。
- (void)setState:(EOCRefreshState)state {
[super setState:state];
if (state == EOCRefreshStateRefreshing || state == EOCRefreshStatePulling) {
_gifImageView.animationImages = _stateImages[@(EOCRefreshStateRefreshing)];
_gifImageView.animationDuration = [_stateAnimationDurations[@(EOCRefreshStateRefreshing)] doubleValue];
[_gifImageView startAnimating];
} else if (state == EOCRefreshStateIdle) {
[_gifImageView stopAnimating];
}
}
上拉加载更多会涉及到ContentSize和GestureState的变化,所以基类里面增加了
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change;
- (void)scrollViewGestureStateDidChange:(NSDictionary *)change;
这两个公开的方法,并且通过KVO进行监听。
- (void)willMoveToSuperview:(UIView *)newSuperview {
//当self被添加到superView的时候,调用
if (newSuperview && [newSuperview isKindOfClass:[UIScrollView class]]) {
//非空,而且是UIScrollView
//同一个header被不同的table来添加的时候
//这里的 self.superView 对应的ATableView
if (self.superview && [self.superview isKindOfClass:[UIScrollView class]]) {
UIScrollView *lastSuperView = (UIScrollView *)self.superview;
[lastSuperView removeObserver:self forKeyPath:@"contentOffset"];
[lastSuperView removeObserver:self forKeyPath:@"contentSize"];
[lastSuperView.panGestureRecognizer removeObserver:self forKeyPath:@"state"];
}
self.scrollView = (UIScrollView *)newSuperview;
self.originalScrollInsets = self.scrollView.contentInset;
//控件还没有设置frame
self.eoc_x = 0.f;
self.eoc_w = self.scrollView.eoc_w;
//footer和header都继承,这两者的高度是不一样
[_scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[_scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[_scrollView.panGestureRecognizer addObserver:self forKeyPath:@"state" options:NSKeyValueObservingOptionNew context:nil];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"contentOffset"]) {
[self scrollOffsetDidChange:change];
} else if ([keyPath isEqualToString:@"contentSize"]) {
[self scrollViewContentSizeDidChange:change];
} else if ([keyPath isEqualToString:@"state"]) {
[self scrollViewGestureStateDidChange:change];
}
}
AutoFooter
一直都会,刚开始就会出现在tableView的底部
内容超过了一屏scrollView的大小的时候
这里的一屏并不一定是屏幕大小,而是scrollView的frame大小。这个时候,当yOffset大于内容高度减去scrollView本身高度后,加上footer的高度的和就完全显示出footer。
其中红色为scrollView的frame,蓝色为内容的大小,橙色为footer的大小,当滑动的距离超过下面红色那段的时候就完全显示出footer了。
- (void)scrollOffsetDidChange:(NSDictionary *)change {
//如果内容超过了一屏scrollView的大小
if (self.scrollView.eoc_h < self.scrollView.eoc_contentH + self.scrollView.eoc_insetT) {
if (self.scrollView.contentOffset.y >= self.scrollView.eoc_contentH - self.scrollView.eoc_h + self.eoc_h) {
//完全显示出footer
// 防止手松开时连续调用
CGPoint old = [change[@"old"] CGPointValue];
CGPoint new = [change[@"new"] CGPointValue];
if (new.y <= old.y) return; // 新的Y小于旧的Y说明,往上拉动的距离不够,或者是footer向下离开屏幕的过程,这个时候直接返回,不进行刷新
self.state = EOCRefreshStateRefreshing;
}
}
}
注意1
设置scrollView的contentInset底部为footer的高,即增加可视范围,完全显示出AutoFooter
- (void)willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];
if (newSuperview) {
//设置scrollView的contentInset底部为footer的高,即增加了滑动距离即可视范围刚刚好为footer的高,完全显示出AutoFooter
self.scrollView.eoc_insetB = self.eoc_h;
self.eoc_y = self.scrollView.eoc_contentH;
} else { //self被移除掉
//修改还原scrollView的contentInset
self.scrollView.eoc_insetB = self.originalScrollInsets.bottom;
}
}
注意2
footer的Y坐标需要专门在监听contentSize的方法中设置,因为只有有了contentSize的时候,才能设置在contentSize的底部,不然当contentSize为零的时候,就加载在tableView的头部去了
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
//contentSize发生变化,一般是tableView发生变化
self.eoc_y = self.scrollView.eoc_contentH;
}
内容没有超过一屏scrollView的大小的时候
这个时候tableView是无法滚动的,需要来监听手势,通过公开的手势监听方法来实现
- (void)scrollViewGestureStateDidChange:(NSDictionary *)change {
//如果在一屏的时候
if (self.scrollView.eoc_h > self.scrollView.eoc_contentH + self.scrollView.eoc_insetT) { // 内容小于一个屏幕时
CGPoint transitionPoint = [self.scrollView.panGestureRecognizer translationInView:self.scrollView];
if (transitionPoint.y < 0 && self.scrollView.panGestureRecognizer.state == UIGestureRecognizerStateEnded)
{
//往上拉,手势不能动
self.state = EOCRefreshStateRefreshing;
}
} else { //超过一屏的时候
if (self.scrollView.eoc_offsetY >= self.scrollView.eoc_contentH + self.scrollView.eoc_insetB - self.scrollView.eoc_h ) {
self.state = EOCRefreshStateRefreshing;
}
}
}
BackFooter
需要向上拖动一定的距离才会显现,并且刷新的时候停留在底部,当加载更多完了过后,就消失。
注意1
其中Y坐标的设定分为两种情况,一种是内容超过了scrollView的frame,Y坐标应该紧跟着内容的后面,另一种是内容没有超过scrollView的frame,Y坐标应该紧跟在scrollView的后面,如果有InsetTop值还要考虑减去它,因为它的坐标是从InsetTop下面才开始计算的,不然Y坐标会向下移动top的距离。
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
//内容和contentSize进行比对
CGFloat contentSizeH = self.scrollView.eoc_contentH;
//这里必须是_originalEdgeInsets
CGFloat contentHeight = self.scrollView.eoc_h - self.originalScrollInsets.top - self.originalScrollInsets.bottom; //减去eoc_insetT,是希望在上拉scrollView的时候就显示出来,减去eoc_insetB,是把eoc_footer变成内容区域块
self.eoc_y = MAX(contentSizeH, contentHeight);
}
上面红色为手机屏幕大小,紫色为scrollView的大小,灰色为内容的大小,青色为footer的大小,灰色和紫色之间为top值。
注意2
找临界点,即刚刚出现footer头部时候的值
分为两种情况:
- 内容超过scrollView的frame,即contentSize的H大于scrollView的H
临界值就等于contentSize的H减去scrollView的H - 内容小于scrollView的frame的时候,即为inset的Top值
- (CGFloat)boundaryOffset {
//内容和contentSize进行比对
CGFloat contentSizeH = self.scrollView.eoc_contentH;
CGFloat contentHeight = self.scrollView.eoc_h - self.scrollView.eoc_insetT - self.scrollView.eoc_insetB; //减去eoc_insetT,是希望在上拉scrollView的时候就显示出来,减去eoc_insetB,是把eoc_footer变成内容区域块
CGFloat finalY = MAX(contentSizeH, contentHeight);
if (finalY == contentSizeH) {
// return _scrollView.eoc_contentH - _scrollView.eoc_h + _scrollView.eoc_insetB;
return contentSizeH - self.scrollView.eoc_h;
} else {
return -self.scrollView.eoc_insetT;
}
}
注意3
让其在刷新的时候,让footer保持显示,刷新完成就消失
- 内容小于scrollView的frame的时候
如上图,原来的展示范围只是到灰色框为止,而现在要是footer展示出来,所以要将展示的距离增加蓝色的高度再加上footer的高度,如果原来还有bottom,还有加上原来的bottom,并且将这个新加的和设置为新的Inset的bottom的值,这样footer就能够展示了。
- 内容大于scrollView的frame的时候
要使footer完全显示出来,要将Inset的bottom的值设置为footer的高度,如果有原来的bottom,还要加上原来的bottom值。
并且还要使tableView滚动到最底部,这样才能看到footer,即要将Offset设置为原来算出来内容大于scrollView的frame时候的临界值,即刚刚露出footer头部,再加上footer的高度和新设置的Inset的bottom的值。
-(void)setState:(EOCRefreshState)state {
[super setState:state];
if (state == EOCRefreshStateRefreshing) {
[UIView animateWithDuration:0.25f animations:^{
CGFloat bottom = self.eoc_h + self.originalScrollInsets.bottom;
CGFloat contentSizeH = self.scrollView.eoc_contentH;
CGFloat contentHeight = self.scrollView.eoc_h - self.originalScrollInsets.top - self.originalScrollInsets.bottom;
CGFloat deltaH = contentSizeH - contentHeight;
if (deltaH < 0) { // 如果内容高度小于view的高度
bottom -= deltaH; // 因为deltaH < 0,所以bottom -= deltaH 相当于加上了一个绝对值为deltaH的正值,即可视范围增加了deltaH的距离
}
self.scrollView.eoc_insetB = bottom;
self.scrollView.eoc_offsetY = [self boundaryOffset] + self.eoc_h + self.scrollView.eoc_insetB;
} completion:^(BOOL finished) {
[self beginRefresh];
}];
} else if (state == EOCRefreshStateIdle || state == EOCRefreshStateNoMoreData) {
[UIView animateWithDuration:0.25f animations:^{
// 刷新完了过后,回到初始值,即隐藏掉footer
self.scrollView.eoc_insetB = self.originalScrollInsets.bottom;
} completion:^(BOOL finished) {
}];
}
}