最近嘗試模仿實現(xiàn)B站的豎屏視頻的拖動效果,實現(xiàn)的最終效果圖如下:
(視頻有最大尺寸和最小尺寸限制,通過滑動UITableView來動態(tài)更改視頻的高度)
github上Demo地址
相應的實現(xiàn)文件名稱為:PullAndScrollViewController
項目開始前需要注意的點
在做這個項目時遇到了一些坑冰寻,在這里分享一下
使用Masonry.h進行view的初始化布局,之后在viewDidLayoutSubViews或者按鈕的實現(xiàn)方法中改變view的frame缀壤,主要是改變高度
會發(fā)現(xiàn)猪杭,不論怎么寫餐塘,界面上view的大小都不發(fā)生變化
但是使用RacObserve監(jiān)聽view的frame屬性,就會發(fā)現(xiàn)皂吮,其實view的frame已經(jīng)發(fā)生了變化
但是在界面上表現(xiàn)不出來
甚至在更改frame的大小后加上強制刷新的代碼戒傻,界面上的表現(xiàn)依舊沒什么反應
//強制刷新代碼
[self.view setNeedsLayout];
[self.view layoutifNeeded];
后來發(fā)現(xiàn),如果初始使用masonry布局進行約束蜂筹,那么之后更改的話需纳,同樣需要使用masonry布局約束進行更改,這樣可以很好的達到效果
如果前面布局使用frame直接布局艺挪,那么后面不論是更改frame還是通過masonry更改約束都能實現(xiàn)相應的效果
具體的原因我還沒有確定不翩,通過查詢資料發(fā)現(xiàn):
參考鏈接:https://www.sohu.com/a/195141167_163917
該文章中有提到:
首先你要知道autolayout和frame的關系,autolayout最終也是轉(zhuǎn)成frame,masonry是建立在autolayout之上的口蝠。你沒獲取到正確的值器钟,那是因為約束還沒布局完成。相當于就是我們給一定的約束亚皂,系統(tǒng)內(nèi)部自己去根據(jù)約束條件轉(zhuǎn)成對應的frame俱箱,而這需要一個過程。想要拿到正確的frame最好的就是讓autolayout完成之后灭必,什么時候完成呢?那就是在layoutsubviews for view or didlayoutsubviews for controller 里獲取,當然在控制器的viewdidappear里也拿得到乃摹,但是正確做法和最佳做法還是在控制器里的viewdidlayout里獲取最好~因為autolayout會根據(jù)約束禁漓,不停的去改變frame,這方法里最后拿到的frame就是最終姿勢.
意思就是masonry布局的并不能馬上獲取到frame的高度大小孵睬,autolayout轉(zhuǎn)化為frame需要一定的時間播歼,或許是因為使用masonry布局的,后續(xù)使用frame直接更改會出現(xiàn)一些問題
之后掰读,去查看了masonry在github上的庫秘狞,在其中的issue中看到了相同的提問
可惜,并沒有進行解答
等后面找到相應的解答之后再更新在這里
項目中TestViewController就是為了驗證這個問題所寫的測試文件蹈集,其中使用#import <ReactiveObjC/ReactiveObjC.h>來對myView的frame屬性進行監(jiān)聽
有興趣的可以看看
具體的實現(xiàn)步驟
具體的實現(xiàn)文件為pullAndScrollViewConroller
在.h中定義相關的屬性
@property (nonatomic, strong) UIView *myView;
@property (nonatomic, assign) CGFloat maxViewHeight;//最大高度
@property (nonatomic, assign) CGFloat minViewHeight;//最小高度
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, assign) CGPoint scrollBeginDraggingOffset;
之后在.m中實現(xiàn)初始的基本的界面以及懶加載
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.navigationController.navigationBar.translucent = NO;
self.title = @"pull scrollView Demo 使用frame來改變";
//初始化高度
self.minViewHeight = 200;
self.maxViewHeight = 400;
[self.view addSubview:self.myView];
[self.view addSubview:self.tableView];
//這個方法主要為了查看過程中一些屬性的變化烁试,在使用時可以將其注釋掉
[self addObserve];
}
- (void)addObserve {
typeof(self) __weak weakSelf = self;
[RACObserve(self.myView, frame) subscribeNext:^(id _Nullable x) {
typeof(weakSelf) __strong self = weakSelf;
NSLog(@"--------------");
NSLog(@"height高度發(fā)生了變化%f",self.myView.frame.size.height);
}];
[RACObserve(self, scrollBeginDraggingOffset) subscribeNext:^(id _Nullable x) {
typeof(weakSelf) __strong self = weakSelf;
NSLog(@"1111111111111111");
NSLog(@"scrollBeginDraggingOffSet發(fā)生了變化%f",self.scrollBeginDraggingOffset.y);
}];
[RACObserve(self.tableView, contentOffset) subscribeNext:^(id _Nullable x) {
typeof(weakSelf) __strong self = weakSelf;
NSLog(@"222222222222222");
NSLog(@"contentOffsetY發(fā)生了變化%f",self.tableView.contentOffset.y);
}];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.tableView.frame = CGRectMake(0, CGRectGetMaxY(self.myView.frame), self.view.bounds.size.height, self.view.bounds.size.height - CGRectGetMaxY(self.myView.frame));
}
相應的懶加載為
#pragma mark - lazy load
- (UIView *)myView {
if (_myView) {
return _myView;
}
_myView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 400)];
_myView.backgroundColor = [UIColor yellowColor];
return _myView;
}
- (UITableView *)tableView {
if (_tableView) {
return _tableView;
}
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 400, self.view.bounds.size.width, 200) style:UITableViewStylePlain];
_tableView.backgroundColor = [UIColor clearColor];
_tableView.showsVerticalScrollIndicator = YES;
_tableView.delegate = self;
_tableView.dataSource = self;
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
return _tableView;
}
實現(xiàn)UITableView的delegate/datasource協(xié)議
#pragma mark - UITableViewDelegate/DataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 100;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"];
cell.textLabel.text = [NSString stringWithFormat:@"第%ld個cell",(long)indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
NSLog(@"點擊了第%ld個cell",(long)indexPath.row);
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 44;
}
這樣基金的界面就已經(jīng)寫好了,運行后的效果為:
此時滑動的話拢肆,上方黃色的UIView不會更換大小
為了達到我們最初的效果减响,我們的思路是在滑動的時候根據(jù)UITableView的contentOffset.y的大小與視頻高度的比較判斷來設置UITableView的偏移量
以此達到我們的效果
在viewDidLayoutSubViews中,我們設置了UITableView的頂部與myView的底部緊挨著
UITableView的滑動調(diào)用的就是UIScrollViewDelegate郭怪,前面有一篇文章專門寫了UIScrollViewDelegate中各個協(xié)議方法的調(diào)用順序支示。
ScrollView滑動協(xié)議方法探究
主要的就是在ScrollViewDidScroll協(xié)議方法中進行相應的邏輯處理
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
//獲取UITableView的偏移量
CGFloat offsetY = scrollView.contentOffset.y;
//計算UITableView的最大偏移量
CGFloat maxOffsetY = scrollView.contentSize.height -
scrollView.contentInset.top - scrollView.contentInset.bottom -
scrollView.frame.size.height;
if (offsetY > 0) {
NSLog(@"向上滑動offsetY為正值,值的大小為%f",offsetY);
} else {
NSLog(@"向下滑動offsetY為負值,值的大小為%f",offsetY);
}
CGFloat height = self.myView.bounds.size.height;
CGFloat currentHeight = self.myView.bounds.size.height;
//根據(jù)當前view的高度判斷,是否處在maxViewHeight和minViewHeight之間鄙才,如果處在之間颂鸿,需要修改view的高度,不需要改變UITableView的contentOffset
//下面的邏輯就是處在最大高度和最小高度之間攒庵,偏移多少嘴纺,就修改高度多少,這樣UITableView就不需要改變contentOffsetY
if (offsetY > 0) {
//表示向上滑動
if (currentHeight > self.minViewHeight) {
height = height - offsetY;
}
} else {
//表示向下滑動
if (currentHeight < self.maxViewHeight) {
height = height - offsetY;
}
}
//判斷height在減去offsetY之后的高度是否還處于maxViewHeight和minViewHeight之間
if (height < self.minViewHeight) {
height = self.minViewHeight;
} else if (height > self.maxViewHeight) {
height = self.maxViewHeight;
}
//當height的高度不等于currentHeight時叙甸,說明view的height發(fā)生了變化颖医,需要修改view的frame的大小,UITableView的不需要再添加代碼修改裆蒸,UITableView的frame修改我們一直放在了viewDidLoadLayoutSubViews中
if (height != currentHeight) {
self.myView.frame = CGRectMake(0, CGRectGetMinY(self.myView.frame), CGRectGetWidth(self.view.frame), height);
[self.view setNeedsLayout];
}
}
這樣的話熔萧,相應的邏輯基本上就實現(xiàn)了,但是運行之后,看到效果并不如我們所想的那樣
這樣運行的效果圖為:
從圖中可以看出佛致,view的高度變化總是快速變化贮缕,和我們預期的想法不一致
后面使用RACObserve監(jiān)聽UITableView的contentOffset屬性
[RACObserve(self.tableView, contentOffset) subscribeNext:^(id _Nullable x) {
typeof(weakSelf) __strong self = weakSelf;
NSLog(@"222222222222222");
NSLog(@"contentOffsetY發(fā)生了變化%f",self.tableView.contentOffset.y);
}];
經(jīng)過調(diào)試發(fā)現(xiàn)了邏輯上的漏洞
首先需要明確一點,對于UITableView俺榆,如果改變它的frame的位置感昼,比如向上移動100,它的contentOffsyY會保持原狀罐脊,不會發(fā)生變化
但是如果通過滑動來改變位置的話定嗓,contentOffsetY會發(fā)生一些變化
這部分可以通過自己編寫例子驗證,在Test2ViewController中我進行的這個驗證
因為只要滑動萍桌,contentOffsetY就會有變化
上面的邏輯漏洞也就不難發(fā)現(xiàn)宵溅,在
if (height != currentHeight) {
self.myView.frame = CGRectMake(0, CGRectGetMinY(self.myView.frame), CGRectGetWidth(self.view.frame), height);
[self.view setNeedsLayout];
}
這里,我們修改myView的frame之后上炎,viewDidLayoutSubViews中會跟著修改UITableView的frame恃逻,這個過程中按照我們的設想,contentOffsetY不應該發(fā)生變化藕施,甚至在滑動的過程中寇损,修改的都是view的height高度,不應該改變contentOffstY
所以裳食,最直接的就是記錄下最初滑動前UITableView的contentOffsetY矛市,之后在改變myView的frame之后,立馬使用setContentOffset設置UITableView的偏移量和滑動前相同即可
記錄滑動前的偏移量胞谈,我們可以在- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView這個方法中國呢記錄
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
NSLog(@"scrollViewWillBeginDragging...");
CGPoint p = scrollView.contentOffset;
CGFloat maxOffsetY = scrollView.contentSize.height - scrollView.contentInset.bottom - scrollView.contentInset.top - scrollView.frame.size.height;
if (p.y >= maxOffsetY) {
p.y = maxOffsetY;
}
self.scrollBeginDraggingOffset = p;
}
之后尘盼,scrollViewDidSCroll中的邏輯需要添加以下代碼
CGFloat originOffsetY = MAX(0, self.scrollBeginDraggingOffset.y);
offsetY = MIN(offsetY, maxOffsetY) - originOffsetY;
其他的相同
if (height != currentHeight) {
self.myView.frame = CGRectMake(0, CGRectGetMinY(self.myView.frame), CGRectGetWidth(self.view.frame), height);
//加一句這個代碼
[scrollView setContentOffset:CGPointMake(0, originOffsetY)];
[self.view setNeedsLayout];
}
這樣運行后,最終的效果圖
和我們預期的結果一致
總結
github上Demo地址
相應的實現(xiàn)文件名稱為:PullAndScrollViewController