iOS手把手教會(huì)自定義刷新控件

一:前言

記得工作中第一次用的刷新控件是svpulltorefresh,用法稍微有點(diǎn)麻煩,而且bug頗多,后來果斷放棄,現(xiàn)在用的是MJRefresh,不管是用法還是bug,都比前一個(gè)好多了,但是不久前也遇到了一個(gè)致命的bug,有好些情況下會(huì)導(dǎo)致MJRefresh陷入一個(gè)死循環(huán),導(dǎo)致不斷的刷新,只能重啟軟件才行。MJRefresh工程比較龐大,找到了bug也很難修改,然后還是決定自己寫一個(gè),系統(tǒng)提供的UIRefreshControl我認(rèn)為是最好的,缺點(diǎn)是不提供自定義UI的方法,那么我就自己基于它來自定義UI。我不是一開始就決定繼承于UIRefreshControl,我同時(shí)也寫了一個(gè)繼承與UIView的control,兩個(gè)進(jìn)行對(duì)比,發(fā)現(xiàn)使用UIview會(huì)有很多弊端,這種弊端在一些復(fù)雜特殊的情況下一下子就暴露出來了,而且很難解決,當(dāng)然,正常狀態(tài)下是沒什么問題的,有興趣的同學(xué)倒是可以去試一試。本demo供大家學(xué)習(xí)和參考,如有發(fā)現(xiàn)bug,還請(qǐng)issues 我。

二: 了解 UIRefreshControl

  • 基本使用方法
//初始化一個(gè)control
UIRefreshControl *control = [[UIRefreshControl alloc] init];
//給control 添加一個(gè)刷新方法
[control addTarget:self action:@selector(refreshAction) forControlEvents:UIControlEventValueChanged];
//把control 添加到 tableView
[self.tableView addSubview:control];
  • 存在的問題

    1. 刷新時(shí)的動(dòng)畫是一個(gè)灰色小菊花,很多情況下不符合app的刷新動(dòng)畫效果
    1. 經(jīng)過多次反復(fù)測(cè)試,下拉的偏移量達(dá)到130以上才會(huì)觸發(fā)刷新方法,很顯然這個(gè)也不符合,一般的刷新控件的高度60左右,所以下拉的偏移量達(dá)到60就可以觸發(fā)刷新的方法了。
  • 自定義控件的思路

    1. 去掉默認(rèn)的動(dòng)畫效果
    1. 自定義自己的動(dòng)畫效果
    1. 改變滿足刷新時(shí)的條件

三:FMRefreshControl

  • 先看一下我寫完的這個(gè)控件的使用方法
FMRefreshControl *control = [[FMRefreshControl alloc] initWithTargrt:self refreshAction:@selector(refreshAction)];

[self.tableView addSubview:control];

兩行代碼,用法比系統(tǒng)的還要稍微簡(jiǎn)單一點(diǎn)。

  • 再看一下效果
image
image

四:思路與代碼

1. 關(guān)于 UIRefreshControl 的幾個(gè)注意點(diǎn),通過frame無法修改它的高度,修改高度目前只找到一種方法,先添加到 superViwe,再執(zhí)行

[[_control.subviews objectAtIndex:0] setFrame:CGRectMake(0, 0, _control.bounds.size.width, 30)];
一開始我是想改變它的高度是否就能改變它的觸發(fā)刷新的偏移量,然后我找到了這個(gè)方法可以修改它的高度,但實(shí)際上改變了高度還是無法改變觸發(fā)下拉刷新的偏移量,所以我們需要自定義去觸發(fā)刷新這個(gè)動(dòng)作的時(shí)機(jī)。

2.手動(dòng)去觸發(fā)刷新動(dòng)作也有幾個(gè)注意點(diǎn),我們是根據(jù)偏移量去觸發(fā)刷新,但是僅僅靠這一個(gè)動(dòng)作是不夠的,還需要一個(gè)條件,那就是用戶手指響應(yīng)過屏幕,簡(jiǎn)單地說,先定義一個(gè)變量,如果用戶觸摸過屏幕,就把變量置為YES,然后再判斷用戶手指離開時(shí)是否達(dá)到了觸發(fā)刷新的偏移量,如果兩個(gè)條件都滿足,就觸發(fā)刷新,刷新完把變量置為NO,如果不滿足,就不觸發(fā),也把變量置為NO。這樣就避免了UIScrollow 因偏移量變動(dòng)而導(dǎo)致非人為的刷新。

3. 進(jìn)入代碼階段

FMRefreshControl *control = [[FMRefreshControl alloc] initWithTargrt:self refreshAction:@selector(refreshAction)];
[self.tableView addSubview:control];

初始化的時(shí)候賦一個(gè) target 和 一個(gè) action,當(dāng)滿足條件的時(shí)候,我們需要知道讓誰去執(zhí)行刷新方法,有這兩個(gè)參數(shù)足夠,當(dāng)執(zhí)行到第二行 addSubView的時(shí)候,我們需要在control內(nèi)部實(shí)現(xiàn)這個(gè)方法:

- (void)willMoveToSuperview:(UIView *)newSuperview {
    [super willMoveToSuperview:newSuperview];
    
    if ([newSuperview isKindOfClass:[UIScrollView class]]) {
        self.superScrollView = (UIScrollView *)newSuperview;
        
        [self.superScrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
    }
}

這樣,我們就知道當(dāng)前這個(gè)control被添加到哪個(gè)父視圖上了,為了安全及代碼的嚴(yán)謹(jǐn),先判斷父視圖是否屬于
UIScrollView,如果是,就用KVO監(jiān)聽contentOffset屬性,這樣便能知道用戶滑動(dòng)的偏移量。

這里我定義了3種狀態(tài):

typedef NS_ENUM(NSInteger, FMRefreshState) {
    FMRefreshStateNormal = 0,     /** 普通狀態(tài) */
    FMRefreshStatePulling,        /** 釋放刷新狀態(tài) */
    FMRefreshStateRefreshing,     /** 正在刷新 */
};

以及切換狀態(tài)后UI的切換和方法的觸發(fā):

- (void)setCurrentStatus:(FMRefreshState)currentStatus {
    _currentStatus = currentStatus;
    switch (_currentStatus) {
        case FMRefreshStateNormal:
            NSLog(@"切換到Normal");
            [self.imageView stopAnimating];
            self.label.text = FM_Refresh_normal_title;
            [self.label sizeToFit];
            self.imageView.image = [UIImage imageNamed:@"refresh_1"];
            
            break;
        case FMRefreshStatePulling:
            NSLog(@"切換到Pulling");
            self.label.text = FM_Refresh_pulling_title;
            [self.label sizeToFit];
            self.imageView.animationImages = self.refreshingImages;
            self.imageView.animationDuration = 1.5;
            [self.imageView startAnimating];
            
            break;
        case FMRefreshStateRefreshing:
            NSLog(@"切換到Refreshing");
            self.label.text = FM_Refresh_Refreshing_title;
            [self.label sizeToFit];
            [self beginRefreshing];
            self.imageView.animationImages = self.refreshingImages;
            self.imageView.animationDuration = 1.5;
            [self.imageView startAnimating];
            [self doRefreshAction];
            
            break;
    }
}

切換到FMRefreshStateNormal 停止動(dòng)畫,切換到FMRefreshStatePulling 開始動(dòng)畫,達(dá)到這個(gè)狀態(tài),說明用戶已經(jīng)達(dá)到了刷新的偏移量,此時(shí)松手便可刷新,切換到FMRefreshStateRefreshing,如果此時(shí)往回滑動(dòng),小于臨界值,那么狀態(tài)重新切回FMRefreshStateNormal。
滿足刷新條件,則便可執(zhí)行以下方法:

- (void)doRefreshAction
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if (self.refreshTarget && [self.refreshTarget respondsToSelector:self.refreshAction])
        [self.refreshTarget performSelector:self.refreshAction];
#pragma clang diagnostic pop
    
}

下面看最關(guān)鍵的KVO方法,也是這里面最復(fù)雜的邏輯處理代碼:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    
    //isDragging 屬性是指用戶手指是否在拖動(dòng)
    if (self.superScrollView.isDragging && !self.isRefreshing) {
        if (!self.originalOffsetY) {
            self.originalOffsetY = -self.superScrollView.contentInset.top;
        }
        CGFloat normalPullingOffset =  self.originalOffsetY - k_FMRefresh_Height;
        if (self.currentStatus == FMRefreshStatePulling && self.superScrollView.contentOffset.y > normalPullingOffset) {
            
            self.currentStatus = FMRefreshStateNormal;
        } else if (self.currentStatus == FMRefreshStateNormal && self.superScrollView.contentOffset.y < normalPullingOffset) {
            self.currentStatus = FMRefreshStatePulling;
        }
    } else if(!self.superScrollView.isDragging){
        
        if (self.currentStatus == FMRefreshStatePulling) {
            
            self.currentStatus = FMRefreshStateRefreshing;
        }
    }
 //拖動(dòng)的偏移量,轉(zhuǎn)換成正數(shù)
    CGFloat pullDistance = -self.frame.origin.y;
    self.backgroundView.frame = CGRectMake(0, 0, k_FMRefresh_Width, pullDistance);
    CGFloat totalWidth = 35 + 20 + self.label.bounds.size.width;
    CGFloat imageViewX = (k_FMRefresh_Width - totalWidth)/2;
    
    self.imageView.frame = CGRectMake(imageViewX,  -k_FMRefresh_Height+pullDistance+(k_FMRefresh_Height - self.imageView.bounds.size.height)/2, self.imageView.frame.size.width, self.imageView.frame.size.height);
    self.label.frame = CGRectMake(imageViewX + 35 + 20, -k_FMRefresh_Height + pullDistance + (k_FMRefresh_Height - self.label.bounds.size.height)/2, self.label.frame.size.width, self.label.frame.size.height);   
}

這里最重要的就是處理兩點(diǎn):1. 根據(jù)偏移量和用戶手指的拖動(dòng)來切換狀態(tài),2. control上面的子視圖需要我們根據(jù)偏移量來實(shí)時(shí)更新。

還有一種情況,上面也提到過,用戶先滑動(dòng)到FMRefreshStatePulling狀態(tài),然后又往回滑動(dòng),此時(shí)的偏移量在0-FMRefreshStatePulling狀態(tài)的偏移量之間,此時(shí)調(diào)用自身的 endRefreshing偏移量不會(huì)復(fù)原,還需要我們自己處理,看了幾個(gè)老外寫的自定義刷新控件,他們都沒修復(fù)這個(gè)bug。他們也沒封裝,全部代碼寫在了控制器里,什么都沒有改變,只是實(shí)現(xiàn)了一個(gè)動(dòng)畫效果,還多了個(gè)bug,動(dòng)畫效果倒是不錯(cuò)的。有興趣的可以參考一番:
https://www.jackrabbitmobile.com/app-development/ios-custom-pull-to-refresh-contro/
https://possiblemobile.com/2014/05/ios-custom-pull-to-refresh/

- (void)endRefreshing {
    if (self.currentStatus != FMRefreshStateRefreshing) {
        return;
    }
    self.currentStatus = FMRefreshStateNormal;
    [super endRefreshing];
    
    //在執(zhí)行刷新的狀態(tài)中,用戶手動(dòng)拖動(dòng)到 nornal 狀態(tài)的 offset,[super endRefreshing] 無法回到初始位置,所以手動(dòng)設(shè)置
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if(self.superScrollView.contentOffset.y >= self.originalOffsetY - k_FMRefresh_Height && self.superScrollView.contentOffset.y <= self.originalOffsetY) {
            CGPoint offset = self.superScrollView.contentOffset;
            offset.y = self.originalOffsetY;
            [self.superScrollView setContentOffset:offset animated:YES];
        }
    });

}

最后還有一點(diǎn)不要忘記 dealloc移除監(jiān)聽:

- (void)dealloc {
    [self.superScrollView removeObserver:self forKeyPath:@"contentOffset"];
}

整篇文章從上至下是按照整個(gè)完整的思路寫下來的,先是提出遇到的問題以及難點(diǎn),然后最后的代碼和思路也是由外至內(nèi)一路寫下來,希望方便大家閱讀。這是上篇,下拉刷新的,還有下篇,上拉加載,過兩天寫,demo中已經(jīng)有了,不過就是還沒優(yōu)化。

domo地址:https://github.com/suifengqjn/FMRefreshControl

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,008評(píng)論 25 709
  • 曾記得導(dǎo)演候孝賢說過,所有的時(shí)光都被辜負(fù)被浪費(fèi)后,才能從記憶里將某一段拎起,拍拍上面沉積的灰塵,感嘆它是最好的時(shí)光...
    丁翎閱讀 221評(píng)論 0 0
  • 斷斷續(xù)續(xù)看完了這部長(zhǎng)達(dá)四個(gè)小時(shí)的電影,思緒萬千。這部電影融合了色情,暴力,純愛,宗教等多種元素,讓漫長(zhǎng)的四小...
    好久沒看見雪了閱讀 1,182評(píng)論 2 1
  • 第一站:慕尼黑·Munich 12.04.2017.柏林-慕尼黑(飛機(jī))47€/約353¥ 75min(一般提前1...
    琪仔小丸子閱讀 543評(píng)論 0 1
  • 踐行18天,這些年我管理過的時(shí)間 這些年都沒刻意去管理過時(shí)間!曾經(jīng)青春年少,肆意揮霍時(shí)間,也曾一瞬間的長(zhǎng)大,發(fā)現(xiàn)不...
    徐殊文閱讀 229評(píng)論 0 0

友情鏈接更多精彩內(nèi)容