UITableView介紹 之下拉刷新原理

UITableView下拉刷新原理

  咱們在用tableView加載數據時,常常會用到下拉刷新這個功能,那麼下拉刷新的原理是什麼,如何個封裝一個好用下拉刷新控件呢?下面由我來詳細介紹一下。ios

下拉刷新

  下拉和上拉基本原理類似可是上拉刷新稍微複雜一點,因此咱們先從下拉刷新講起。ui

基本原理

  下拉刷新的基本原理是經過判斷tableView的contenOffset的屬性變化來作一些相應的處理,實現方式主要用到了狀態機模式,下拉過程當中主要有三種狀態(正常狀態、正在下拉、正在刷新)在這三種狀態下作不一樣的處理。spa

  爲了使用方便,因此代碼的基本都封裝在自定義控件中了,不說廢話上代碼.net

//
//  XQRefresh.h
//  下拉刷新
//
//  Created by code_xq on 16/3/5.
//  Copyright © 2016年 code_xq. All rights reserved.
//

#import <UIKit/UIKit.h>
#import "UIView+Expand.h"

#ifndef XQRefresh
#define XQRefresh
    typedef NS_ENUM(NSInteger, RefreshState) {
        RefreshStateNormal = 0,
        RefreshStatePulling = 1,
        RefreshStateRefreshing = 2,
        RefreshStateDefault = 3
    };
#endif // XQRefresh

@interface XQRefreshHeader : UIView

+ (instancetype)initWithBlock:(void (^)(void))refreshingBlock;
- (void)beginRefreshing;
- (void)endRefreshing;

@end

  這裏提供了三個方法因此調用方式也很是簡單代理

__weak typeof (self)weakSelf = self;
    XQRefreshHeader *refreshHeader = [XQRefreshHeader initWithBlock:^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
        dispatch_get_main_queue(), ^{
            
            // 業務邏輯
            ....
            [weakSelf.tableView reloadData];
            [weakSelf.refreshHeader endRefreshing];
        });
    }];
    [tableView addSubview:refreshHeader];

  XQRefreshHeader 的實現會用到ios的kvo機制,用來監聽tableView的contentOffset的變化,這樣作的好處是不用使用scrollView的衆多代理方面,少了一層ViewController能夠將全部的操做封裝到view中實現,這裏借鑑了MJRefresh的思路code

/**
 * 當view被添加到父視圖時被調用,父視圖銷燬時也會被調用此時newSuperview爲空
 */
- (void)willMoveToSuperview:(UIView *)newSuperview {
    [super willMoveToSuperview:newSuperview];
    // 移除監聽
    [self.superview removeObserver:self forKeyPath:XQRefreshContentOffset];
    
    if (newSuperview) {
        self.tableView = (UITableView *)newSuperview;
        
        self.width = newSuperview.width;
        self.height = newSuperview.height;
        self.bottom = newSuperview.top;
        
        UILabel *textLabel = [[UILabel alloc] init];
        [self addSubview:textLabel];
        textLabel.width = 100;
        textLabel.center = self.center;
        textLabel.height = 30;
        textLabel.bottom = self.height - 10;
        textLabel.textColor = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
        textLabel.textAlignment = NSTextAlignmentCenter;
        self.textLabel = textLabel;
        
        UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"arrow"]];
        [self addSubview:imageView];
        imageView.width = 18;
        imageView.height = 26;
        imageView.right = textLabel.left -5;
        imageView.bottom = self.height - 12;
        imageView.hidden = YES;
        self.imageView = imageView;
        
        
        UIActivityIndicatorView *activity = [[UIActivityIndicatorView alloc] init];
        activity.width = 50;
        activity.height = 50;
        activity.center = textLabel.center;
        
        [activity setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleGray];
        [self addSubview:activity];
        self.activity = activity;
        // 設置view的背景色
        self.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:0.9];
        self.hidden = YES;
        self.curState = RefreshStateDefault;
        // 添加監聽
        [newSuperview addObserver:self forKeyPath:XQRefreshContentOffset options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

    }
    
}

  這裏用willMoveToSuperview方法初始化控件主要考慮到了這個方法的一個特性,當一個view被添加到父view時newSuperView不爲空可是self.superView卻爲空,當控制器跳轉時還會調用一次這個方法,此時正好相反newSuperView爲空self.superView不爲空,利用這個特性能夠用來添加監聽和移除監聽,若是說只給某個對象的屬性添加了kvo監聽不去移除監聽的話程序會報錯。orm

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // 這裏是爲了記錄初始化完成後的contentInset值
    if (!self.tableView.isTracking && self.curState == RefreshStateDefault) {
        _startInsetTop = self.tableView.contentInset.top;
        return;
    }
    
    if ([keyPath isEqualToString:XQRefreshContentOffset]) {
        CGFloat offsetY = - [change[@"new"] CGPointValue].y;
        CGFloat cValue = offsetY - _startInsetTop;
        
        if (cValue > 0 && cValue < refreshHeigh) {
            // 下拉過程可是沒有超過給定的高度此時的狀態爲RefreshStatePulling 
            
        } else if (cValue >= refreshHeigh && !_tableView.isDragging) {
            // 正在刷新狀態此時變化值等於給定的高度且手指離開屏幕 RefreshStateRefreshing 
        } else if (cValue <= 0){
            // 正常狀態RefreshStateNormal
        } else if (cValue >= refreshHeigh && _tableView.isDragging) {
            // 下拉過程可是超過給定的高度此時的狀態爲RefreshStatePulling
        }
    } 
}

當contentOffset值發生改變時會調用上面的方法,狀態方法以下server

- (void)setStates:(RefreshState)state offsetValue:(CGFloat)offsetValue {
    switch (state) {
        case RefreshStateNormal: {
            // 清理工做將view中的全部改變了的屬性恢復到下拉以前

        }
            break;
        case RefreshStatePulling: {

        }
            break;
        case RefreshStateRefreshing: {
            ....
            // 改變tableView的contentInset值,讓它停留在下拉狀態(重要)
            [UIView animateWithDuration:0.5 animations:^{
                self.tableView.contentInset = UIEdgeInsetsMake(offsetValue + refreshHeigh, 0, 0, 0);
            }];
            // 回調block(重要)
            _refreshingBolck();
        }
            break;
        default:
            break;
    }
    // 記錄當前的刷新狀態
    _curState = state; 
}

這裏還要說明的一個細節是- (void)beginRefreshing方法的實現對象

- (void)beginRefreshing {
    self.hidden = NO;
    self.textLabel.text = self.textLabel.text = @"鬆手刷新...";
    [UIView animateWithDuration:0.09 animations:^{
    } completion:^(BOOL finished) {
        self.curState = RefreshStatePulling;
        [self setStates:RefreshStateRefreshing offsetValue:_startInsetTop];
    }]; 
}

由於此方法通常在tableView建立之後當即調用,此時有可能取到的startInsetTop原始值不正確,全部這裏採用適當的延遲等tableView顯示完成後再取初始值。rem

運行效果

上拉刷新

上拉刷新的原理和下拉相同,就是一些細節須要注意:

  • 每次刷新時刷新控件footerRefresh的y值要隨contentSize的改變而改變
  • 下拉完成時也要改變footerRefresh的y值

完整代碼的下載地址

相關文章
相關標籤/搜索