分页控制器半自动滑动(仿36氪)

在很多APP里都能看到屏幕上方有一条用于显示分页控制器的View,一般分页控制器栏的总体宽度都会超过屏幕宽度,为了让用户减少手动滑动分页控制器栏的操作,所以分页控制器的半自动滑动就必不可少了。(代码采用双语方式)

36氪客户端.png
腾讯新闻客户端.png

该功能点主要有五个:
1.选中标题和上一个选中标题互换字体颜色。
2.底部线条永远跟随选中标题,并且页面滑动时线条要向对应的分栏标题方向滑动。
3.点击标题按钮时,判断点击标题的临近标题按钮是否在屏幕内,不在则滑动显示临近标题。
4.滑动页面切换标题栏时,判断滑到的标题栏的临近标题按钮是否在屏幕内,不在则滑动显示临近标题。
5.不同长度的标题如何动态显示按钮和控制底部线条滑动。
实际效果如下:

segmentDemoShow.gif

功能点一,很容易用一个按钮属性记录上一个选中按钮就可以搞定了,然后每次点击后就重新设置上次选中按钮,设置后再将当前选中按钮赋值给选中按钮属性记录,周而复始即可。

功能点二,因为页面取屏幕宽度,底部线条要移动的范围是下一个按钮的宽度,所以需要换算移动比例,此处命名scrollGap为页面滑动距离,scrollGap<0,页面向左滑动。页面滑动时监听方法

- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
func scrollViewDidScroll(_ scrollView: UIScrollView) 

通过计算获取到页面滑动距离,设置底部线条的位置。

if (scrollGap < 0) { // 页面向左滑动 底部线向右滑动
        CGFloat scale = -scrollGap*2/Screen_Width;
        CGFloat gap = ([self.buttonWidths[index] floatValue]+[self.buttonWidths[index+1] floatValue]-2*self.bottomLineWidth)/2 + self.bottomLineWidth;
        
        if (scale <= 1) {
            bottomLine.frame = CGRectMake(bottomLine.frame.origin.x, bottomLine.frame.origin.y, self.bottomLineWidth+scale*gap, 2);
        }
        else {
            bottomLine.frame = CGRectMake(self.bottomLastX+(scale-1)*gap, bottomLine.frame.origin.y, self.bottomLineWidth+gap-(scale-1)*gap, 2);
        }
        
    }
    else { // 页面向右滑动 底部线向左滑动
        CGFloat scale = scrollGap*2/Screen_Width;
        CGFloat gap = ([self.buttonWidths[index] floatValue]+[self.buttonWidths[index-1] floatValue]-2*self.bottomLineWidth)/2 + self.bottomLineWidth;
        if (scale <= 1) {
            bottomLine.frame = CGRectMake(self.bottomLastX-scale*gap, bottomLine.frame.origin.y, self.bottomLineWidth+scale*gap, 2);
            
        }
        else {
            bottomLine.frame = CGRectMake(self.bottomLastX-gap, bottomLine.frame.origin.y, self.bottomLineWidth+gap-(scale-1)*gap, 2);
        }
    }
if scrollGap < 0 { // 页面向左滑动 底部线向右滑动
            let scale : CGFloat = -scrollGap*2.0/Screen_Width
            let gap : CGFloat = ((buttonWidths[index] as! CGFloat)+(buttonWidths[index+1] as! CGFloat)-2*self.bottomLineWidth)/2 + bottomLineWidth;
            
            if scale <= 1 {
                bottomLine.frame = CGRect.init(x: bottomLine.frame.origin.x, y:  bottomLine.frame.origin.y, width: bottomLineWidth+scale*gap, height: 2.0)
            }
            else {
                bottomLine.frame = CGRect.init(x: bottomLastX+(scale-1)*gap, y: bottomLine.frame.origin.y, width: bottomLineWidth+gap-(scale-1)*gap, height: 2.0)
            }
            
        }
        else { // 页面向右滑动 底部线向左滑动
            let scale : CGFloat = scrollGap*2.0/Screen_Width;
            let gap : CGFloat = ((buttonWidths[index] as! CGFloat)+(buttonWidths[index-1] as! CGFloat)-2*bottomLineWidth)/2 + bottomLineWidth;
            if (scale <= 1) {
                bottomLine.frame = CGRect.init(x: bottomLastX-scale*gap, y: bottomLine.frame.origin.y, width: bottomLineWidth+scale*gap, height: 2.0)
            }
            else {
                bottomLine.frame = CGRect.init(x: bottomLastX-gap, y: bottomLine.frame.origin.y, width: bottomLineWidth+gap-(scale-1)*gap, height: 2.0)
            }
        }

功能点三,判断相邻的标题按钮是否在屏幕内,这本身需要两个判断,一个是判断当前标题按钮距离屏幕左侧近还是右侧近,如果是距离左侧近,那只需要判断当前按钮左侧的标题按钮是否在,反之判断右侧的。
我们通过当前点击按钮的center.x和其所在scrollView容器的contentOffset.x比较判断它的位置。当距离左侧近时,又有两种情况,当前按钮完全在屏幕内,当前按钮未完全在屏幕内。但是我们不用去判断而是统一为如果当前按钮不是最左侧按钮,那么它的做边距距离屏幕左边距是否有一个标题按钮的距离,如果有那么它临近的按钮自然就在屏幕上,否则就不完全在屏幕上。
接下来判断临近的按钮是否在屏幕内。

分页控制器示意图.png

如图,

CGFloat leftGap = CGRectGetMinX(button.frame) - contentOffsetX;
let leftGap : CGFloat = button.frame.minX - contentOffsetX

leftGap是点击按钮左边距离当前scrollView在屏幕中最左边的距离,有正负值,那么要判断点击按钮临近左侧按钮是否在屏幕内,首先判断leftGap < self.buttonWidth的条件,为true那么临近的按钮未完全显示,否则完全显示不用做滑动处理。我们做的滑动处理都是在条件为true的基础上。右侧同理。

CGFloat contentOffsetX = backScrollView.contentOffset.x;
    CGFloat gap = button.center.x - contentOffsetX;
    
    if (gap < Screen_Width/2) { // 按钮距离屏幕左边近
        // 按钮左边距距离屏幕左边距的距离
        CGFloat leftGap = CGRectGetMinX(button.frame) - contentOffsetX;
       
        if (index == 0) { // 按钮为最左侧按钮 只把自己显示全即可
            [self reSetTitleScrollViewOffsetWithX:contentOffsetX+leftGap];
            
        } else {
            CGFloat leftButtonWidth = [self.buttonWidths[index-1] floatValue];
            if ( leftGap < leftButtonWidth){ // 按钮距离左边距不足临近按钮距离 此时需要移动
                    NSLog(@"向右滑动显示左侧按钮");
            [self reSetTitleScrollViewOffsetWithX:contentOffsetX+leftGap-leftButtonWidth];
            }
        }
        
    } else if (gap > Screen_Width/2){
    
        // 按钮右边距距离屏幕右边距的距离
        CGFloat rightGap = contentOffsetX+Screen_Width-CGRectGetMaxX(button.frame);
        
            if (index == self.titles.count-1) { // 当前是最右侧按钮
                [self reSetTitleScrollViewOffsetWithX:contentOffsetX-rightGap];
                
            } else {
                
                CGFloat rightButtonWidth = [self.buttonWidths[index+1] floatValue];
                if (rightGap < rightButtonWidth) { // 按钮距离右边距不足一个按钮距离 此时需要移动
                 [self reSetTitleScrollViewOffsetWithX:contentOffsetX+rightButtonWidth-rightGap];
            }
            
        }
    }

- (void)reSetTitleScrollViewOffsetWithX:(CGFloat)pointX{
    [UIView animateWithDuration:0.3 animations:^{
        backScrollView.contentOffset = CGPointMake(pointX, 0);
    }];
}
let contentOffsetX : CGFloat = backScrollView.contentOffset.x
        let gap : CGFloat = button.center.x - contentOffsetX
        
        if gap < Screen_Width/2 { // 按钮距离屏幕左边近
            // 按钮左边距距离屏幕左边距的距离
            let leftGap : CGFloat = button.frame.minX - contentOffsetX
            
            if index == 0 { // 按钮为最左侧按钮 只把自己显示全即可
                self.reSetTitleScrollViewOffsetWithX(pointX: contentOffsetX+leftGap)
            }
            else {
                let leftButtonWidth : CGFloat = buttonWidths[index-1] as! CGFloat
                if leftGap < leftButtonWidth{ // 按钮距离左边距不足临近按钮距离 此时需要移动
                    print("向右滑动显示左侧按钮")
                    self.reSetTitleScrollViewOffsetWithX(pointX: contentOffsetX+leftGap-leftButtonWidth)
                }
            }
        }
        else if gap > Screen_Width/2 {
            // 按钮右边距距离屏幕右边距的距离
            let rightGap : CGFloat = contentOffsetX+Screen_Width-button.frame.maxX
            if index == titles.count-1 { // 当前是最右侧按钮
                self.reSetTitleScrollViewOffsetWithX(pointX: contentOffsetX-rightGap)
            }
            else {
                let rightButtonWidth : CGFloat = buttonWidths[index+1] as! CGFloat
                if rightGap < rightButtonWidth { // 按钮距离右边距不足一个按钮距离 此时需要移动
                    self.reSetTitleScrollViewOffsetWithX(pointX: contentOffsetX+rightButtonWidth-rightGap)
                }
            }
        }
func reSetTitleScrollViewOffsetWithX(pointX : CGFloat) {
        UIView.animate(withDuration: 0.3) { 
            backScrollView.contentOffset = CGPoint.init(x: pointX, y: 0)
        }
    }

因为这种分栏标题一屏显示的标题一般都大于3个,所以如果按钮居中,那么两侧的按钮一定都在屏幕内,所以不用做处理。

功能点四,通过页面滑动完成的监听方法,

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) 

获取当前页面滑动到的位置index,有了index就可以做类似功能三的判断了。

详细可下载demo查看:
OC版demo下载地址
Swift版demo下载地址 GitHub给个Star噢!
喜欢就点个赞呗!
欢迎大家提出更好的改进意见和建议,一起进步!

MARK

OC版做了优化,可配置isBackGroud熟悉开关选择不同显示样式,显示如下:


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

推荐阅读更多精彩内容