先說下下拉刷新動畫效果的實現(xiàn)恨樟,重寫寫一個動畫的類,并且重寫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];
}
最后在刷新和下拉狀態(tài)的時候開始動畫绳泉,在閑置狀態(tài)的時候結束動畫逊抡。
- (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進行監(jiān)聽秦忿。
- (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
一直都會,剛開始就會出現(xiàn)在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
// 防止手松開時連續(xù)調用
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坐標需要專門在監(jiān)聽contentSize的方法中設置,因為只有有了contentSize的時候颓屑,才能設置在contentSize的底部斤寂,不然當contentSize為零的時候,就加載在tableView的頭部去了
- (void)scrollViewContentSizeDidChange:(NSDictionary *)change {
//contentSize發(fā)生變化揪惦,一般是tableView發(fā)生變化
self.eoc_y = self.scrollView.eoc_contentH;
}
內容沒有超過一屏scrollView的大小的時候
這個時候tableView是無法滾動的遍搞,需要來監(jiān)聽手勢,通過公開的手勢監(jiān)聽方法來實現(xiàn)
- (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
需要向上拖動一定的距離才會顯現(xiàn)尾抑,并且刷新的時候停留在底部,當加載更多完了過后蒂培,就消失再愈。
注意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變成內容區(qū)域塊
self.eoc_y = MAX(contentSizeH, contentHeight);
}
上面紅色為手機屏幕大小衔沼,紫色為scrollView的大小,灰色為內容的大小昔瞧,青色為footer的大小指蚁,灰色和紫色之間為top值。
注意2
找臨界點自晰,即剛剛出現(xiàn)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變成內容區(qū)域塊
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的時候
如上圖,原來的展示范圍只是到灰色框為止牲剃,而現(xiàn)在要是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) {
}];
}
}