本文主要介紹 上下滾動時菜單懸停在頂端,并且可以左右滑動切換的特殊視圖的實現方式。涉及知識包含事件響應鏈,UIScorllView滾動模擬,以及刷新控件的基本原理。
一、前言
隨著業務的發展,系統提供的常規視圖已經難以滿足需求,偉大的UI設計師總能想出一些特殊的違反常理的視圖來挑戰程序員的腦細胞。這種上下滾動還可左右滑動切換最初也不知是哪家提出來的,但是經過這么久發展,這種視圖展現方式也被越來越多的App使用,此類開源框架也有不少,實現處理方式也很奇妙,但也各有缺點和限制。
二、部分框架
2.1 YX_UITableView_IN_UITableView
此類視圖常見的做法便是UIScorllView套UIScorllView,使內層的UITableView(TAB欄里面)和外層的UITableView同時響應用戶的手勢滑動事件。當用戶從頁面頂端從下往上滑動到TAB欄的過程中,使外層的UITableView跟隨用戶手勢滑動,內層的UITableView不跟隨手勢滑動。當用戶繼續往上滑動的時候,讓外層的UITableView不跟隨手勢滑動,讓內層的UITableView跟隨手勢滑動。反之從下往上滑動也一樣。
缺點:當用戶從頁面頂端從下往上滑動到TAB欄的過程中,會停住不會有單獨scrollview那如絲滑一般的滾動效果。
大部分類似框架都存在該問題,如 MXSegmentedPager也是如此,本來這樣效果也挺好了,但是UI說你看看簡書就可以,美妝心得就可以,產品說如果沒有如絲滑一般的滾動不如不上......
2.2 HHHorizontalPagingView
為了滿足UI和產品的需求,筆者輾轉反側 夜不能寐終于發現了曙光,該作者的思路非常巧妙:
HHHorizontalPagingView 通過重寫 - (UIView *)hitTest:(CGPoint)point
withEvent:(UIEvent *)event方法 將headerView 上的響應作用在了
self.currentScrollView (當前展現的scrollerView)上,滾動就根據contentOffset來移動
headerView。點擊就調用 @property (nonatomic, copy) void
(^clickEventViewsBlock)(UIView *eventView);
eventView 是hitTest方法查找到的view。
缺點:1.只要headerView稍微復雜點,點擊事件就非常難以處理。
2.破壞了headerView的事件響應鏈,如果想在headerView上添加輪播圖就無法手勢左右滑動了。
針對以上兩個缺點,缺點2 限于攔截的實現方法導致系統的手勢處理都沒作用在headerView,已是無法實現。缺點1在筆者冥思苦想下做出了一種解決方案。
點擊難以處理主要是,作者為了實現該效果,重寫hitTest方法,導致了headerView響應者鏈條的斷裂,雖然作者提供了一個block回調,但對于點擊處理無疑是反人類。我的想法是在點擊處理時將響應者鏈條接起來。關于響應者鏈條可以看看該文章。
Huanhoo 使用@property (nonatomic, copy) void (^clickEventViewsBlock)
(UIView *eventView);來處理點擊事件,而eventView就是 命中測試view , 而我要做的
就是通過這個命中測試view向上查找處理該事件。
實現方法:
引入UIView+WhenTappedBlocks這是一個手勢處理的分類,
#pragma mark - 模擬響應者鏈條 由被觸發的View 向它的兄弟控件 父控件 延伸查找響應
- (void)viewWasTappedPoint:(CGPoint)point{
[self clickOnThePoint:point];
}
- (BOOL)clickOnThePoint:(CGPoint)point{
if ([self.superview isKindOfClass:[UIWindow class]]) {
return NO;
}
if (self.block) {
self.block();
return YES;
}
__block BOOL click = NO;
// 看兄弟控件
[self.superview.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// 轉換坐標系 看點是否在View上
CGPoint objPoint = [obj convertPoint:point fromView:self];
if (!CGRectContainsPoint(obj.frame, objPoint)) {
// NSLog(@"-----%@",NSStringFromCGPoint(objPoint));
return;
}
if (self.block) {
self.block();
click = YES;
*stop = YES;
}
}];
if (!click) {
return [self.superview clickOnThePoint:point];
}
return click;
}
正常響應,有點擊手勢觸發方法來執行block,非正常點擊 主動調用
- (void)viewWasTappedPoint:(CGPoint)point;方法就可以接起響應者鏈條。
關于以上的實現可以看 JYHHHorizontalPagingView 1.1.0版本這是筆者對
HHHorizontalPagingView的一個優化處理讓點擊易于處理,但是也僅僅只能接起點擊事件。在headerView上加輪播圖無法實現,甚至UIButton的長按高亮效果在headerView上也會失效。但是它有絲滑一般的滑動,headerView和下部的每一個ScrollView滾動效果都是一體的。
三、 JYHHHorizontalPagingView模擬ScrollView的滾動
上面介紹的框架限于實現方式都各有缺陷,那到底能不能做到完美,答案是肯定的,畢竟美妝心得做到了,簡書做到了(UIButton 的長按高亮效果猶在說明headerView上的響應并沒有被破環)。我的思路是在headerView上添加拖拽手勢改變下方scrollView的contentOffset,模擬scrollView的減速滑動以及彈簧效果。
3.1模擬彈簧效果
彈簧效果的實現很簡單使用UIView 動畫即可。
[UIView animateWithDuration:0.35 animations:^{
self.currentScrollView.contentOffset = CGPointMake(contentOffset.x, border);
[self layoutIfNeeded];
}];
3.2模擬減速滑動效果
減速滑動效果確實不好實現,我嘗試過不少方法效果都不太好,后來看到了餓了么一位開發者的博客,他是通過UIDynamic的物理特性來模擬scrollView滾動。按照他的方法完美實現了模擬。作者的博客地址目前好像無法進去就貼一個推酷的轉載用UIKit Dynamics模仿UIScrollView,具體的一些說明作者講的很清晰我就不多說了,大家可以自己看看,也可以直接看我的代碼。
四、擴展功能
到目前為止,該類視圖的功能可以說是相當完美了,但是需求永遠是難以滿足的,某天產品說現在數據沒有更新機制只能上拉刷新,不能下拉刷新。what?這么反人類的功能你還要加下拉刷新......
針對此類需求,筆者為此添加了單獨下拉刷新以及整體下拉刷新,由于篇幅問題,筆者就不再多說了,感興趣的同學可以去 github -JYHHHorizontalPagingView看具體介紹說明。
五、結尾
如果我的文章對你有幫助或者給了你一些啟發,希望你能在github給個小星星,如果你在使用過程中遇到了Bug請留言反饋,我會及時解決。歡迎轉載(在文章開頭標明來源即可)。
六、補充
1.很多人反饋有偏移啥的,看下 self.edgesForExtendedLayout = UIRectEdgeNone;
- pod 1.2.1 版本已經支持自定義 SegmentView,自定義view只需支持JYSegmentViewProtocol協議即可。具體可參考默認的JYSegmentView 。
七、最后再推薦一個(相當給力,可定制性強)
github -HVScrollView
感謝 S型身材的豬
思路:
首先最底部是一個全屏的scrollView,這個scrollView的作用是橫向滑動,scrollView上面添加若干個tableView,然后每個tableView上設置頂部內邊距,頂部由內邊距空出來的地方就放headerView和菜單欄。headerView和菜單欄放在控制器 view上。當滑動tableView時,讓headerView的y值隨著 scrollview的偏移量時刻改變,刷新也不會有問題。
我看了下代碼,相當給力,思路實現都相當好值得學習。
大家看評論的話,作者有這么一句話:
不知道為什么網上幾個人封裝都去用collectionView搞那么復雜。
為什么這么復雜呢?一個功能又是響應鏈的打斷,又是模擬滾動,view還一層套一層。針對一種特殊需求的出現,前期并沒有太多的開源代碼供大家學習和研究,在時間的緊逼之下,大家都選擇了自己的處理方式,隨著后期的迭代,為了達到需要的效果,各個流派在原有基礎上做出了相應處理。這些方案也許并不是最好的,最簡單的,但它們都包含了作者的智慧與思想。對于這些作者我們應心存感激,因為他們開源,他們無私的分享了自己的思路,這些如今看來也許并不友好的實現方式卻是在我們最迷茫最需要時給了我們幫助與啟發。也正因為有他們的方案來保證項目的正常上線,我們才敢嘗試更簡單更高效的方法。最后感謝開源。
本文所列舉的一些DEMO都有自己的實現方式,大家可研究學習一下選擇最適合的。感謝這些開源作者。