悬停视图-SFHoverTableView

项目中需要使用悬停的效果,临时找过三方就写了,后来自己尝试模仿大佬编写的效果,如果有错误请指正

QQ20190815-104001-HD.gif

git地址

  • 其中的部分代码如下:
//
//  SFAqArtController.m
//  SFHoverTableView
//
//  Created by 随风流年 on 2019/8/12.
//  Copyright © 2019 随风流年. All rights reserved.
//

#import "SFAqArtController.h"
#import "SFHeaderView.h"
#import "SPPageMenu.h"

#import "SFArtFirstController.h"
#import "SFArtSecondController.h"
#import "SFArtThirdController.h"
#import "SFArtFourController.h"

@interface SFAqArtController ()<SPPageMenuDelegate,UIScrollViewDelegate>
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) SFHeaderView *headerView;
@property (nonatomic, strong) SPPageMenu   *pageMenu;
@property (nonatomic, assign) CGFloat lastPageMenuY;
@property (nonatomic, assign) CGFloat lastPoint;

@end

@implementation SFAqArtController
- (void)leftItmeClick:(UIBarButtonItem *)item{
    [self.navigationController popViewControllerAnimated:YES];
}
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.navigationController.navigationBar.translucent = NO;
    self.navigationItem.title = @"爱奇艺";
    UIBarButtonItem *leftItem = [[UIBarButtonItem alloc]initWithTitle:@"返回" style:UIBarButtonItemStylePlain target:self action:@selector(leftItmeClick:)];
    self.navigationItem.leftBarButtonItem = leftItem;
    self.lastPageMenuY = HeaderViewH;
    // 添加一个全屏的scrollView
    [self.view addSubview:self.scrollView];
    [self.view addSubview:self.pageMenu];
    SFArtFirstController *firstVC = [[SFArtFirstController alloc]init];
    [self addChildViewController:firstVC];
    firstVC.headerView = self.headerView;
    
    [self addChildViewController:[[SFArtSecondController alloc]init]];
    [self addChildViewController:[[SFArtThirdController alloc]init]];
    [self addChildViewController:[[SFArtFourController alloc]init]];
    
    [self.scrollView addSubview:self.childViewControllers[0].view];
    //监听子控制器中scrollView正在滑动所发出的通知
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(subScrollViewDidScroll:) name:ArtChildScrollViewDidScrollNSNotification object:nil];
}
#pragma mark -- ArtChildScrollViewDidScrollNSNotification --
- (void)subScrollViewDidScroll:(NSNotification *)noti{
   
    // 取出当前正在滑动的tableView
    UIScrollView *scrollingScrollView = noti.userInfo[@"scrollingScrollView"];
    CGFloat offsetDifference = [noti.userInfo[@"offsetDifference"] floatValue];
    
    CGFloat distanceY;
    
    SFAqArtBaseController *baseVc = self.childViewControllers[self.pageMenu.selectedItemIndex];
    
    // 取出的scrollingScrollView并非是唯一的,当有多个子控制器上的scrollView同时滑动时都会发出通知来到这个方法,所以要过滤
    if (scrollingScrollView == baseVc.scrollView && baseVc.isFirstViewLoaded == NO) {
        // 让分页菜单跟随scrollView滑动
        CGRect pageMenuFrame = self.pageMenu.frame;
        
        if (pageMenuFrame.origin.y >= 0) {
            // 往上滑
            if (offsetDifference > 0) {
                NSLog(@"往上上上滑---:%.1f===%.1f"
                      ,scrollingScrollView.contentOffset.y
                      ,self.pageMenu.frame.origin.y);

                if (((scrollingScrollView.contentOffset.y+self.pageMenu.frame.origin.y)>=HeaderViewH) || scrollingScrollView.contentOffset.y < 0) {
                    // 分页菜单的y值等于当前正在滑动且显示在屏幕范围内的的scrollView的contentOffset.y的改变量(这是最难的点)
                    pageMenuFrame.origin.y += -offsetDifference;
                    if (pageMenuFrame.origin.y <= 0) {
                        pageMenuFrame.origin.y = 0;
                    }
                    
                }
            } else { // 往下滑
                NSLog(@"往下滑---:%.1f===%.1f"
                      ,scrollingScrollView.contentOffset.y
                      ,self.pageMenu.frame.origin.y);
                
                if ((scrollingScrollView.contentOffset.y+self.pageMenu.frame.origin.y)-0<HeaderViewH) {
                    pageMenuFrame.origin.y = -scrollingScrollView.contentOffset.y+HeaderViewH+0;
                }
            }
        }
        self.pageMenu.frame = pageMenuFrame;
        
        // 配置头视图的y值
        [self adjustHeaderY];
        
        // 记录分页菜单的y值改变量
        NSLog(@"分页菜单改变之前:%f===:%f===%.1f",pageMenuFrame.origin.y,self.lastPageMenuY,self.pageMenu.frame.origin.y);
        distanceY = pageMenuFrame.origin.y - self.lastPageMenuY;
        self.lastPageMenuY = self.pageMenu.frame.origin.y;
        NSLog(@"分页菜单改变之后之后之后:%f",self.lastPageMenuY);

        // 让其余控制器的scrollView跟随当前正在滑动的scrollView滑动
        [self followScrollingScrollView:scrollingScrollView distanceY:distanceY];
        
    }
    baseVc.isFirstViewLoaded = NO;
}
//所有子控制器上特定的scrollView同时联动
- (void)followScrollingScrollView:(UIScrollView *)scrollingScrollView distanceY:(CGFloat)distanceY{
    SFAqArtBaseController *baseVC = nil;
    for (int i = 0; i<self.childViewControllers.count; i++) {
        baseVC = self.childViewControllers[i];
        if (baseVC.scrollView == scrollingScrollView) {
            continue;
        }else{
            //除去当前正在滑动的scrollView外,其余scrollView的改变量等于分页菜单的改变量
            CGPoint contentOffSet = baseVC.scrollView.contentOffset;
            contentOffSet.y += - distanceY;
            baseVC.scrollView.contentOffset = contentOffSet;
        }
    }
}
#pragma mark - SPPageMenuDelegate -
- (void)pageMenu:(SPPageMenu *)pageMenu itemSelectedFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex{
    if (!self.childViewControllers.count) {return;}
    //如果上一次点击的button下标与当前点击的button的下标之差大于等于2,说明跨界面移动了,此时不动画
    if (labs(toIndex - fromIndex) >= 2) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.scrollView setContentOffset:CGPointMake(self->_scrollView.frame.size.width * toIndex, 0) animated:NO];
        });
    }else{
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.scrollView setContentOffset:CGPointMake(self->_scrollView.frame.size.width * toIndex, 0) animated:YES];
        });
    }
    SFAqArtBaseController *targetViewController = self.childViewControllers[toIndex];
    if (self.scrollView.dragging || self.scrollView.decelerating || self.scrollView.contentOffset.x / SCREEN_WIDTH == self.pageMenu.selectedItemIndex) {
        // 1. 切换headerView的父视图 2.将headerView的x、y值都归0
        targetViewController.headerView = self.headerView;
    }
    if ([targetViewController isViewLoaded]) {
        return;//如果已经加载过,就不再加载
    }
    //是第一次加载控制器的View,这个属性是为了防止下面的偏移量的改变导致scrollViewDidScroll
    targetViewController.isFirstViewLoaded = YES;
    targetViewController.view.frame = CGRectMake(SCREEN_WIDTH * toIndex, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
    UIScrollView *s = targetViewController.scrollView;
    CGPoint contentOffset = s.contentOffset;
    contentOffset.y = - self.pageMenu.frame.origin.y + HeaderViewH;
    if (contentOffset.y >= HeaderViewH) {
        contentOffset.y = HeaderViewH;
    }
    s.contentOffset = contentOffset;
    [self.scrollView addSubview:targetViewController.view];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
    if (scrollView == self.scrollView) {
        SFAqArtBaseController *baseVC = self.childViewControllers[self.pageMenu.selectedItemIndex];
        if ([baseVC isViewLoaded]) {
            [self.scrollView bringSubviewToFront:baseVC.view];
        }
        //如果是手指滑动
        if (scrollView.dragging || scrollView.decelerating) {
            //横向切换tableView时,头部不要跟随tableView偏移
            CGRect headerFrame = self.headerView.frame;
            headerFrame.origin.x = scrollView.contentOffset.x - SCREEN_WIDTH * self.pageMenu.selectedItemIndex;
            self.headerView.frame = headerFrame;
        }else{
            //如果不是手指滑动,通过点击pageMenu上的item滑动。这里先将headerView加到self.view上,目的是过度一下,如果不过度,点击相邻item,改变scrollView的偏移量使用了动画参数,这个动画会导致切换headerview有一个闪跳现象
            CGRect rectInView = [self.headerView convertRect:self.headerView.bounds toView:self.view];
            rectInView.origin.x = 0;
            [self adjustHeaderY];
            [self.view addSubview:self.headerView];
            self.headerView.frame = rectInView;
            
            if (scrollView.contentOffset.x / SCREEN_WIDTH == self.pageMenu.selectedItemIndex)  {
                [self.headerView removeFromSuperview];
                baseVC.headerView = self.headerView;
                [self adjustHeaderY];
            }
        }
        //如果scrollView的内容很少,在屏幕内,自动回落
        if (scrollView.contentOffset.x / SCREEN_WIDTH == self.pageMenu.selectedItemIndex) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                if (baseVC.scrollView.contentSize.height < SCREEN_HEIGHT && [baseVC isViewLoaded]) {
                    [baseVC.scrollView setContentOffset:CGPointMake(0, 0) animated:YES];
                }
            });
        }
    }
}
-(void)adjustHeaderY{
    //取出当前子控制器
    SFAqArtBaseController *baseVC = self.childViewControllers[self.pageMenu.selectedItemIndex];
    CGRect headerFrame = self.headerView.frame;
    //将pageMenu的frame切换到当前正在滑动的scrollView上
    CGRect pageMenuFrameInScrollView = [self.pageMenu convertRect:self.pageMenu.bounds toView:baseVC.scrollView];
    NSLog(@"pageMenuY:%.1f",pageMenuFrameInScrollView.origin.y);
    //每个tableView的头视图的y值都等于pageMenu的y值减去头部高度,这是为了保证头部的底部永远跟pageMenu的顶部紧贴
    headerFrame.origin.y = pageMenuFrameInScrollView.origin.y - HeaderViewH;
    self.headerView.frame = headerFrame;
    
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    if (scrollView == self.scrollView) {
        [self adjustHeaderY];
    }
    SFAqArtBaseController *baseVc = self.childViewControllers[self.pageMenu.selectedItemIndex];
    // 这个方法是因为手指拖拽了scrollView松开手指,结束减速时调用,如果是因为代码改变scrollView偏移量不会来到这个方法
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (baseVc.scrollView.contentSize.height < SCREEN_HEIGHT && [baseVc isViewLoaded]) {
            [baseVc.scrollView setContentOffset:CGPointMake(0, 0) animated:YES];
        }
    });
}
#pragma mark --- lazy ---
- (UIScrollView *)scrollView{
    if (!_scrollView) {
        _scrollView = [[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT - BottomMargin)];;
        _scrollView.delegate = self;
        _scrollView.pagingEnabled = YES;
        _scrollView.showsVerticalScrollIndicator = NO;
        _scrollView.showsHorizontalScrollIndicator = NO;
        _scrollView.contentSize = CGSizeMake(SCREEN_WIDTH * 4, 0);
    }
    return  _scrollView;
}
- (SFHeaderView *)headerView{
    if (!_headerView) {
        _headerView = [[SFHeaderView alloc]init];
        _headerView.frame = CGRectMake(0, 0, SCREEN_WIDTH, HeaderViewH);
        _headerView.backgroundColor = [UIColor greenColor];
    }
    return _headerView;
}
- (SPPageMenu *)pageMenu{
    if (!_pageMenu) {
        _pageMenu = [SPPageMenu pageMenuWithFrame:CGRectMake(0, CGRectGetMaxY(self.headerView.frame), SCREEN_WIDTH, PageMenuH) trackerStyle:SPPageMenuTrackerStyleLineAttachment];
        [_pageMenu setItems:@[@"第一页",@"第二页",@"第三页",@"第四页"] selectedItemIndex:0];
        _pageMenu.delegate = self;
        _pageMenu.itemTitleFont = [UIFont systemFontOfSize:16];
        _pageMenu.selectedItemTitleColor = [UIColor blackColor];
        _pageMenu.unSelectedItemTitleColor = [UIColor colorWithWhite:0 alpha:0.6];
        _pageMenu.tracker.backgroundColor = [UIColor orangeColor];
        _pageMenu.permutationWay = SPPageMenuPermutationWayNotScrollEqualWidths;
        _pageMenu.bridgeScrollView = self.scrollView;
    }
    return _pageMenu;
}

@end








/*
 
 UIView *redView = [[UIView alloc]initWithFrame:CGRectMake(200, 200, 100, 100)];
 redView.backgroundColor = [UIColor redColor];
 [self.view addSubview:redView];
 
 UIView *greenView = [[UIView alloc]initWithFrame:CGRectMake(100, 100, 50, 50)];
 greenView.backgroundColor = [UIColor greenColor];
 [self.view addSubview:greenView];
 
 UIView *blueView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 40, 40)];
 blueView.backgroundColor = [UIColor blueColor];
 [redView addSubview:blueView];
 
 CGRect rec = [self.view convertRect:blueView.frame fromView:redView];
 NSLog(@"rec:%@",NSStringFromCGRect(rec));//rec:{{200, 200}, {40, 40}}
 // redview中的blueview相对于self.view的位置 ✔️
 
 CGRect rec1 = [self.view convertRect:redView.frame fromView:blueView];
 NSLog(@"rec1:%@",NSStringFromCGRect(rec1));//rec1:{{400, 400}, {100, 100}} ✅
 // 蓝色view中的定义一个相对于蓝色view的frame的view,这个view相对于self.view的位置
 
 CGRect rect =  [redView convertRect:greenView.frame toView:self.view];
 NSLog(@"rect:%@",NSStringFromCGRect(rect));//rect:{{300, 300}, {50, 50}} ✅
 /// 在redView中,定义一个相对于redView(100,100),大小为(50,50)的view,这个view相对于self.view的位置
 
 
 /// 需要注意的是toview可以传nil
 CGRect rect1 =  [redView convertRect:greenView.frame toView:nil];
 NSLog(@"rect1:%@",NSStringFromCGRect(rect1));//rect1:{{100, 100}, {50, 50}} ✅
 /// 上面的代码的意思是:在redView中,定义一个目标区域,该区域相对于window的位置(nil代表的是self.view.window)
 
 CGRect rect2 =  [redView convertRect:greenView.frame toView:blueView];
 NSLog(@"rect2:%@",NSStringFromCGRect(rect2));//rect2:{{100, 100}, {50, 50}}
 //在redView中,定义一个相对于redview(100,100),大小为(50,50)的view,这个view相对于blueView的位置
 
 CGRect rect3 =  [redView convertRect:CGRectMake(80, 80, 80, 80) toView:blueView];
 NSLog(@"rect3:%@",NSStringFromCGRect(rect3));//rect3:{{80, 80}, {80, 80}}
 //在redView中,定义一个相对于redview(80,80),大小为(80,80)的view,这个view相对于blueView的位置
 
 CGRect rect4 =  [redView convertRect:CGRectMake(80, 80, 80, 80) toView:greenView];
 NSLog(@"rect4:%@",NSStringFromCGRect(rect4));//rect4:{{180, 180}, {80, 80}}
 //在redView中,定义一个相对于redview(80,80),大小为(80,80)的view,这个view相对于greenView的位置
 
 CGRect rect5 =  [redView convertRect:CGRectMake(20, 20, 80, 80) toView:greenView];
 NSLog(@"rect5:%@",NSStringFromCGRect(rect5));//rect5:{{120, 120}, {80, 80}}
 //在redView中,定义一个相对于redview(80,80),大小为(80,80)的view,这个view相对于greenView的位置
 
 CGRect newRect = [redView convertRect:greenView.bounds toView:nil];
 NSLog(@"newRect:%@",NSStringFromCGRect(newRect));//newRect:{{0, 0}, {50, 50}}
 
 CGRect newRect0 = [redView convertRect:greenView.frame toView:nil];
 NSLog(@"newRect0:%@",NSStringFromCGRect(newRect0));//newRect0:{{100, 100}, {50, 50}}
 
 CGRect newRect1 = [redView convertRect:redView.bounds toView:nil];
 NSLog(@"newRect1:%@",NSStringFromCGRect(newRect1));//newRect1:{{0, 0}, {100, 100}}
 
 CGRect newRect2 = [redView convertRect:redView.frame toView:nil];
 NSLog(@"newRect2:%@",NSStringFromCGRect(newRect2));//newRect2:{{200, 200}, {100, 100}}
 

 2019-08-12 14:54:04.798060+0800 SFHoverTableView[10946:186139] rec:{{200, 200}, {40, 40}}
 2019-08-12 14:54:04.798231+0800 SFHoverTableView[10946:186139] rec1:{{400, 400}, {100, 100}}
 2019-08-12 14:54:04.798333+0800 SFHoverTableView[10946:186139] rect:{{300, 300}, {50, 50}}
 2019-08-12 14:54:04.798420+0800 SFHoverTableView[10946:186139] rect1:{{100, 100}, {50, 50}}
 2019-08-12 14:54:04.798508+0800 SFHoverTableView[10946:186139] rect2:{{100, 100}, {50, 50}}
 2019-08-12 14:54:04.798594+0800 SFHoverTableView[10946:186139] rect3:{{80, 80}, {80, 80}}
 2019-08-12 14:54:04.798678+0800 SFHoverTableView[10946:186139] rect4:{{180, 180}, {80, 80}}
 2019-08-12 14:54:04.798758+0800 SFHoverTableView[10946:186139] rect5:{{120, 120}, {80, 80}}
 2019-08-12 14:54:04.798832+0800 SFHoverTableView[10946:186139] newRect:{{0, 0}, {50, 50}}
 2019-08-12 14:54:04.798913+0800 SFHoverTableView[10946:186139] newRect0:{{100, 100}, {50, 50}}
 2019-08-12 14:54:04.799096+0800 SFHoverTableView[10946:186139] newRect1:{{0, 0}, {100, 100}}
 2019-08-12 14:54:04.799377+0800 SFHoverTableView[10946:186139] newRect2:{{200, 200}, {100, 100}}
 


//总结:
//toView就是从左往右开始读代码,也是从左往右理解意思
//fromView就是从右往左开始读代码,也是从右往左理解意思

 */


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350