scrollview在上下滑動(dòng)時(shí)耐床,改變視圖高度

一、需求

之前遇到一個(gè)需求是,要求在scrollview在上下滑動(dòng)時(shí),scrollview顯示區(qū)域高度變化气笙。向上滑動(dòng)時(shí)——拉高次企,向下滑動(dòng)時(shí)——恢復(fù)怯晕。

二、項(xiàng)目中的實(shí)現(xiàn)

由于項(xiàng)目中要實(shí)現(xiàn)的幾個(gè)頁(yè)面都用到了自定義的SITableView缸棵,剛好就在自定義的SITableView中實(shí)現(xiàn)了

1.向外傳遞滑動(dòng)

有以下兩種方案

  • 1)協(xié)議 如果是多級(jí)或者是跨層的舟茶,不好要拿到響應(yīng)者,同時(shí)如果視圖層級(jí)改變的話堵第,也需要改變賦值響應(yīng)者的代碼吧凉。可以精準(zhǔn)的傳遞事件給需要改變的視圖踏志,也可以自定義滑動(dòng)距離阀捅,雖然實(shí)際用處不大。本次實(shí)現(xiàn)用的是協(xié)議针余。

還有一種思路是饲鄙,定義一個(gè)BOOL值,標(biāo)識(shí)是否開(kāi)啟滑動(dòng)改變傳遞圆雁,然后向上查找第一個(gè)能響應(yīng)協(xié)議的responder忍级,把它記錄為委托者。

  • 2)通知
    傳遞數(shù)據(jù)方便伪朽,但不能自定義滑動(dòng)距離轴咱。并且如果多個(gè)界面都注冊(cè)了的話,接受到通知要進(jìn)行判斷烈涮,判斷要調(diào)整大小的視圖是不是在屏幕上朴肺。如果頁(yè)面復(fù)用過(guò)程中,導(dǎo)致某個(gè)視圖加載完成后坚洽,視圖層級(jí)中有父視圖和子視圖都能響應(yīng)通知戈稿,會(huì)出現(xiàn)問(wèn)題,雖然出現(xiàn)的可能性不大酪术。

協(xié)議的代碼如下:

@class SITableView;
@protocol SITableViewUpDownScrollProtocol <NSObject>
//告訴外部對(duì)象器瘪,是向上還是向下滑動(dòng)
- (void)tableView:(SITableView *)tableView updownScroll:(BOOL)isUp;
@optional
// 是否要自定義判斷移動(dòng)的距離
- (CGFloat)tableViewMinMoveDistance:(SITableView *)tableView;

@end

滑動(dòng)方向是向上還是向下翠储,應(yīng)該用枚舉的,偷懶了

2.SITableView中的主要變動(dòng)

scrollViewDidScroll :方法中橡疼,判斷contentOffset.y的變化援所,與前一刻的差值作為上下的依據(jù)。
要考慮以下幾個(gè)問(wèn)題:

1.只有當(dāng)用戶手動(dòng)滑動(dòng)時(shí)欣除,才改變視圖高度住拭。需要記錄是不是手動(dòng)拖拽,雖然历帚,scrollview有dragging滔岳,但不夠精確,在手松開(kāi)減速時(shí)依然是YES挽牢,不符合要求
2.需要記錄初始值谱煤,來(lái)做參考
3.要移動(dòng)一定距離,才能判斷是否執(zhí)行回調(diào)禽拔,避免有時(shí)手觸碰屏幕引起的誤操作
4.攔截的方法刘离,不能影響原方法的調(diào)用

  • 1.增加私有屬性,協(xié)助判斷
//是不是手動(dòng)移動(dòng)
@property (nonatomic, assign, getter=isManuallyMoving) BOOL manuallyMoving;
//開(kāi)始手動(dòng)移動(dòng)時(shí)contentOffset.y值
@property (nonatomic, assign) CGFloat startOffsetY;
//tableview的新的delegate睹栖,用來(lái)判斷是否要攔截
@property (nonatomic, strong) SITableViewWeakProxy *weakProxy;
//默認(rèn)最小移動(dòng)距離 5
@property (nonatomic, assign) CGFloat minMoveDistance;
  • 2.實(shí)現(xiàn)
#pragma mark - 上下滑動(dòng)回調(diào)
//調(diào)用有參無(wú)返回值的方法
- (void)callTableViewUpDownScrollProtocol:(BOOL)isUp {
    
    if (self.upDownScrollDelegate == nil) {
        return;
    }
    // 1. 根據(jù)方法創(chuàng)建簽名對(duì)象sig
    NSMethodSignature *sig = [self.upDownScrollDelegate methodSignatureForSelector:@selector(tableView:updownScroll:)];
    
    // 2. 根據(jù)簽名對(duì)象創(chuàng)建調(diào)用對(duì)象invocation
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
    
    // 3. 設(shè)置調(diào)用對(duì)象的相關(guān)信息
    invocation.target = self.upDownScrollDelegate;
    invocation.selector = @selector(tableView:updownScroll:);
 
    SITableView *tempSelf = self;
    // 參數(shù)必須從第2個(gè)索引開(kāi)始硫惕,因?yàn)榍皟蓚€(gè)已經(jīng)被target和selector使用
    [invocation setArgument:&tempSelf atIndex:2];
    [invocation setArgument:&isUp atIndex:3];
    
    // 4. 調(diào)用方法
    [invocation invoke];
    
}
#pragma mark - 攔截的協(xié)議方法

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    self.manuallyMoving = NO;
    //不影響原有的邏輯,回調(diào)原來(lái)delegate的方法
    if ([self.weakProxy.originTarget respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) {
        [self.weakProxy.originTarget scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
    }
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    self.manuallyMoving = YES;
    self.startOffsetY = scrollView.contentOffset.y;

    //不影響原有的邏輯野来,回調(diào)原來(lái)delegate的方法
    if ([self.weakProxy.originTarget respondsToSelector:@selector(scrollViewWillBeginDragging:)]) {
        [self.weakProxy.originTarget scrollViewWillBeginDragging:scrollView];
    }
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (self.isManuallyMoving) {
        if (self.startOffsetY < scrollView.contentOffset.y - self.minMoveDistance) {
          
            [self callTableViewUpDownScrollProtocol:YES];
        }
        if (self.startOffsetY > scrollView.contentOffset.y + self.minMoveDistance) {
      
            [self callTableViewUpDownScrollProtocol:NO];
        }
    }
    self.startOffsetY = scrollView.contentOffset.y;
    
    //不影響原有的邏輯恼除,回調(diào)原來(lái)delegate的方法
    if ([self.weakProxy.originTarget respondsToSelector:@selector(scrollViewDidScroll:)]) {
        [self.weakProxy.originTarget scrollViewDidScroll:scrollView];
    }
}
#pragma mark - setter與getter
- (void)setDelegate:(id<UITableViewDelegate>)delegate {
    self.weakProxy.originTarget = delegate;
    [super setDelegate:self.weakProxy];
}

- (void)setUpDownScrollDelegate:(id<SITableViewUpDownScrollProtocol>)upDownScrollDelegate {
    if (upDownScrollDelegate && [upDownScrollDelegate conformsToProtocol:@protocol(SITableViewUpDownScrollProtocol)] && [upDownScrollDelegate respondsToSelector:@selector(tableView:updownScroll:)]) {
        _upDownScrollDelegate = upDownScrollDelegate;
        
        if ([upDownScrollDelegate respondsToSelector:@selector(tableViewMinMoveDistance:)]) {
            self.minMoveDistance = [upDownScrollDelegate tableViewMinMoveDistance:self];
        }
    }
    if (upDownScrollDelegate == nil) {
        _upDownScrollDelegate = upDownScrollDelegate;
    }
}
- (SITableViewWeakProxy *)weakProxy {
    if (_weakProxy == nil) {
        _weakProxy = [SITableViewWeakProxy alloc];
        _weakProxy.interceptionTarget = self;
    }
    return _weakProxy;
}

注意 [SITableViewWeakProxy alloc];這樣寫(xiě)沒(méi)有錯(cuò),它沒(méi)有init方法曼氛。

3.SITableViewWeakProxy的實(shí)現(xiàn)

為什么要做的這樣復(fù)雜豁辉,
不直接把delegate設(shè)為自己,用一個(gè)屬性記錄原始的delegate呢搪锣?如果這樣做了秋忙,tableview的UITableViewDelegate協(xié)議中的其他方法呢,怎么把協(xié)議中的方法傳遞給原始的delegate呢构舟。實(shí)現(xiàn)所有的方法灰追,在里面判斷原始的delegate是否實(shí)現(xiàn)了,原始未實(shí)現(xiàn)的但方法需要返回值的你怎么操作狗超。如果里面后面新增了方法怎么辦弹澎,一個(gè)個(gè)版本維護(hù)更新?
走消息轉(zhuǎn)發(fā)努咐,UITableViewDelegate協(xié)議中的很多方法是optional苦蒿,會(huì)調(diào)用respondsToSelector來(lái)判斷是否協(xié)議中某個(gè)方法,這個(gè)地方的響應(yīng)者是SITableView的實(shí)例渗稍,它明顯沒(méi)有實(shí)現(xiàn)協(xié)議中的其他方法佩迟,就無(wú)法調(diào)用了团滥。當(dāng)然也可以重寫(xiě)respondsToSelector,但怎么判斷這個(gè)sel是UITableViewDelegate協(xié)議中的方法报强,一個(gè)個(gè)列出來(lái)

使用SITableViewWeakProxy灸姊,是實(shí)例不會(huì)在方法列表中查找,而是直接走消息轉(zhuǎn)發(fā)秉溉,效率高力惯,也安全,不用擔(dān)心其他的影響召嘶。包括respondsToSelector方法也是走的消息轉(zhuǎn)發(fā)父晶,所以在具體的實(shí)現(xiàn)中,要特殊處理弄跌,判斷這個(gè)方法的參數(shù)甲喝,如果是要攔截的三個(gè)方法,就要攔截碟绑。

@interface SITableViewWeakProxy : NSProxy <UITableViewDelegate>

@property (nonatomic, weak) NSObject<UITableViewDelegate> *originTarget;
@property (nonatomic, weak) NSObject *interceptionTarget;

@end

@implementation SITableViewWeakProxy

//- (id)forwardingTargetForSelector:(SEL)selector {
//    NSLog(@"%@...%@", self, NSStringFromSelector(selector));
//    for (NSString *interceptionSEL in self.interceptionSELS) {
//        if (NSSelectorFromString(interceptionSEL) == selector) {
//            return _interceptionTarget;
//        }
//    }
//    return _originTarget;
//}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.originTarget methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    //這個(gè)很重要俺猿,SITableViewWeakProxy不能響應(yīng)respondsToSelector方法茎匠,只是做轉(zhuǎn)發(fā)格仲,所以需要特殊判斷下
    if (self.interceptionTarget && invocation.selector == @selector(respondsToSelector:)) {
        SEL parameterSel;
        [invocation getArgument:&parameterSel atIndex:2];
        
        if ([self interceptionSelector:parameterSel]) {
            [invocation invokeWithTarget:self.interceptionTarget];
            return;
        }
      
    }else if (self.interceptionTarget && [self interceptionSelector:invocation.selector]) {
        [invocation invokeWithTarget:self.interceptionTarget];
        return;
    }
    //不需要攔截,直接調(diào)用原來(lái)的delegate
    [invocation invokeWithTarget:self.originTarget];
}
//只需要攔截這三個(gè)方法诵冒,不需其他方法
- (BOOL)interceptionSelector:(SEL)sel {
    return  sel == @selector(scrollViewDidScroll:) || sel == @selector(scrollViewDidEndDragging:willDecelerate:) || sel == @selector(scrollViewWillBeginDragging:);
}

@end

三凯肋、scrollview分類(lèi)的實(shí)現(xiàn)

ps:以下來(lái)自5月1日補(bǔ)充

@selector(setDelegate:) @selector(delegate)一個(gè)屬性的set與get方法,它們是一個(gè)整體汽馋,不能拆分開(kāi)來(lái)侮东,需要都hook,之前思慮不周全豹芯,沒(méi)考慮到這一點(diǎn)悄雅。比如說(shuō),不斷的調(diào)用get方法然后再重新賦值給set方法铁蹈,之前的實(shí)現(xiàn)就會(huì)有問(wèn)題宽闲,改變了原有的實(shí)現(xiàn),雖然一般不會(huì)這么做握牧,但程序要嚴(yán)謹(jǐn)容诬,不留漏洞。

分類(lèi)方式的實(shí)現(xiàn)沒(méi)有采用協(xié)議的方式沿腰,主要是考慮到幾點(diǎn):

  • 如果有協(xié)議回調(diào)览徒、又有通知可以選,那么在開(kāi)啟監(jiān)聽(tīng)方法設(shè)計(jì)不夠優(yōu)雅

  • 這樣在組件化使用中更加方便颂龙,耦合性比協(xié)議小

  • 不在實(shí)現(xiàn)中統(tǒng)一判斷最小滑動(dòng)距離习蓬,而是直接傳遞纽什,由使用者自行判斷,靈活性更大躲叼;之前的最小滑動(dòng)距離設(shè)定不好操作也是一方面

實(shí)現(xiàn)方案說(shuō)明:

  1. 通知的userInfo中稿湿,有兩個(gè)key,一直是滑動(dòng)的距離(當(dāng)前位置減去上一次的位置)押赊,還有一個(gè)就是哪一個(gè)scrollView滑動(dòng)發(fā)出的通知饺藤,來(lái)解決使用通知引起的多點(diǎn)觸發(fā),不知道該不該響應(yīng)的問(wèn)題流礁。

  2. 消息轉(zhuǎn)發(fā)者與攔截方法判斷分別在兩個(gè)類(lèi)實(shí)現(xiàn)涕俗,雖然職責(zé)分開(kāi)了,但是之間互相耦合神帅,沒(méi)有通過(guò)接口(協(xié)議)編程再姑。消息轉(zhuǎn)發(fā)類(lèi)的實(shí)現(xiàn)參考了YYKit里面的實(shí)現(xiàn)。

  3. 兩種實(shí)現(xiàn)方式找御,實(shí)際上大同小異

    • 通過(guò)函數(shù)指針的方式元镀,hook方法的實(shí)現(xiàn)。這里替換的是UIScrollView這個(gè)類(lèi)的delegate屬性對(duì)應(yīng)的兩個(gè)方法霎桅,使用GCD確保只會(huì)進(jìn)行一次

    • 通過(guò)派生一個(gè)子類(lèi)栖疑,類(lèi)似KVO模式。調(diào)用方法使用的是編譯后的方法objc_msgSendSuper 滔驶,還要處理如果之前這個(gè)類(lèi)添加過(guò)KVO的情況遇革,并且處理的用的是KVC,如果有變動(dòng)揭糕,不會(huì)知道萝快。如果有其他類(lèi)也使用這種方案,將互相沖突抵消掉著角。思路與實(shí)現(xiàn)參考了IMYAOPTableView

    • 測(cè)試中分了兩種情況:在開(kāi)啟監(jiān)聽(tīng)之前delegate有值揪漩;開(kāi)啟監(jiān)聽(tīng)之后才設(shè)置delegate。通過(guò)宏來(lái)進(jìn)行不同情況測(cè)試吏口。兩種實(shí)現(xiàn)方式也是通過(guò)宏來(lái)控制切換奄容。

具體代碼實(shí)現(xiàn)參見(jiàn):WeakProxy
對(duì)于參考與借鑒的源碼在這里一并表示感謝!歡迎斧正锨侯!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嫩海,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子囚痴,更是在濱河造成了極大的恐慌叁怪,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件深滚,死亡現(xiàn)場(chǎng)離奇詭異奕谭,居然都是意外死亡涣觉,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)血柳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)官册,“玉大人,你說(shuō)我怎么就攤上這事难捌∠ツ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵根吁,是天一觀的道長(zhǎng)员淫。 經(jīng)常有香客問(wèn)我,道長(zhǎng)击敌,這世上最難降的妖魔是什么介返? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮沃斤,結(jié)果婚禮上圣蝎,老公的妹妹穿的比我還像新娘。我一直安慰自己衡瓶,他們只是感情好徘公,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著鞍陨,像睡著了一般步淹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上诚撵,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音键闺,去河邊找鬼寿烟。 笑死,一個(gè)胖子當(dāng)著我的面吹牛辛燥,可吹牛的內(nèi)容都是我干的筛武。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼挎塌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼徘六!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起榴都,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤待锈,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后嘴高,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體竿音,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡和屎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了春瞬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柴信。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖宽气,靈堂內(nèi)的尸體忽然破棺而出随常,到底是詐尸還是另有隱情,我是刑警寧澤萄涯,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布线罕,位于F島的核電站,受9級(jí)特大地震影響窃判,放射性物質(zhì)發(fā)生泄漏钞楼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一袄琳、第九天 我趴在偏房一處隱蔽的房頂上張望询件。 院中可真熱鬧,春花似錦唆樊、人聲如沸宛琅。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嘿辟。三九已至,卻和暖如春片效,著一層夾襖步出監(jiān)牢的瞬間红伦,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工淀衣, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留昙读,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓膨桥,卻偏偏與公主長(zhǎng)得像蛮浑,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子只嚣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • *7月8日上午 N:Block :跟一個(gè)函數(shù)塊差不多沮稚,會(huì)對(duì)里面所有的內(nèi)容的引用計(jì)數(shù)+1,想要解決就用__block...
    炙冰閱讀 2,488評(píng)論 1 14
  • 廢話不多說(shuō)册舞,直接上干貨 ---------------------------------------------...
    小小趙紙農(nóng)閱讀 3,361評(píng)論 0 15
  • ## iOS常用問(wèn)題總結(jié)#### iOS基礎(chǔ)知識(shí)回顧##### 1蕴掏、為什么說(shuō)Objective-C是一門(mén)動(dòng)態(tài)的語(yǔ)言...
    蟬始鳴閱讀 471評(píng)論 0 3
  • 一、前言: 卓APP一旦發(fā)生任何報(bào)錯(cuò),就會(huì)停止運(yùn)行囚似,這是令許多開(kāi)發(fā)者頭疼的問(wèn)題剩拢,很多情況下,一些隱性BUG在測(cè)試部...
    因?yàn)槲业男?/span>閱讀 462評(píng)論 2 0
  • 佛說(shuō)“五百年的緣分才換了今生的回眸一笑”饶唤。 她在微信上問(wèn)我“李老師徐伐,你在西安培訓(xùn)?我也在募狂“焖兀” 因?yàn)榕嘤?xùn)業(yè)務(wù)的關(guān)系,...
    辰溪行者閱讀 94評(píng)論 0 0