个人页效果(上下滚动时菜单悬停且可左右切换)


演示
演示

本文主要介绍 上下滚动时菜单悬停在顶端,并且可以左右滑动切换的特殊视图的实现方式。涉及知识包含事件响应链,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;

七、最后再推荐一个(相当给力,可定制性强)

github -HVScrollView
感谢 S型身材的猪

思路:
首先最底部是一个全屏的scrollView,这个scrollView的作用是横向滑动,scrollView上面添加若干个tableView,然后每个tableView上设置顶部内边距,顶部由内边距空出来的地方就放headerView和菜单栏。headerView和菜单栏放在控制器 view上。当滑动tableView时,让headerView的y值随着 scrollview的偏移量时刻改变,刷新也不会有问题。

我看了下代码,相当给力,思路实现都相当好值得学习。
大家看评论的话,作者有这么一句话:
不知道为什么网上几个人封装都去用collectionView搞那么复杂。

为什么这么复杂呢?一个功能又是响应链的打断,又是模拟滚动,view还一层套一层。针对一种特殊需求的出现,前期并没有太多的开源代码供大家学习和研究,在时间的紧逼之下,大家都选择了自己的处理方式,随着后期的迭代,为了达到需要的效果,各个流派在原有基础上做出了相应处理。这些方案也许并不是最好的,最简单的,但它们都包含了作者的智慧与思想。对于这些作者我们应心存感激,因为他们开源,他们无私的分享了自己的思路,这些如今看来也许并不友好的实现方式却是在我们最迷茫最需要时给了我们帮助与启发。也正因为有他们的方案来保证项目的正常上线,我们才敢尝试更简单更高效的方法。最后感谢开源。

本文所列举的一些DEMO都有自己的实现方式,大家可研究学习一下选择最适合的。感谢这些开源作者。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这里是一个简单的 HTML 文件,包含了实现上述功能的代码。 ```html <!DOCTYPE html> <html> <head> <title>Image Manipulation</title> <style> #container { position: relative; width: 600px; height: 400px; border: 1px solid black; margin: 20px auto; } #image { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); transition: all 0.5s ease-in-out; } #menu { display: flex; justify-content: space-between; align-items: center; width: 600px; margin: 20px auto; } .menu-item { padding: 10px; border: 1px solid black; cursor: pointer; } #text { font-size: 24px; text-align: center; margin: 20px auto; } #outer { position: relative; width: 600px; height: 400px; border: 1px solid black; margin: 20px auto; } #inner { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 200px; height: 200px; border: 1px solid black; cursor: move; } #scrollbar { width: 20px; height: 300px; background-color: gray; position: absolute; right: 0; top: 50%; transform: translateY(-50%); cursor: pointer; } #thumb { width: 20px; height: 50px; background-color: white; position: absolute; top: 0; left: 0; cursor: pointer; } </style> </head> <body> <div id="container"> <img id="image" src="image1.jpg" width="300" height="200"> </div> <div id="menu"> <div class="menu-item" onclick="changeText('Option 1')">Option 1</div> <div class="menu-item" onclick="changeText('Option 2')">Option 2</div> <div class="menu-item" onclick="changeText('Option 3')">Option 3</div> </div> <div id="text">Default Text</div> <div id="outer"> <div id="inner"></div> <div id="scrollbar"> <div id="thumb"></div> </div> </div> <script> // Image manipulation var image = document.getElementById('image'); image.addEventListener('mouseenter', function() { this.src = 'image2.jpg'; }); image.addEventListener('mouseleave', function() { this.src = 'image1.jpg'; }); image.addEventListener('mousemove', function(e) { this.style.left = e.clientX - this.width / 2 + 'px'; this.style.top = e.clientY - this.height / 2 + 'px'; }); document.addEventListener('keydown', function(e) { var key = e.key; switch (key) { case 'ArrowUp': image.style.top = parseInt(image.style.top) - 10 + 'px'; break; case 'ArrowDown': image.style.top = parseInt(image.style.top) + 10 + 'px'; break; case 'ArrowLeft': image.style.left = parseInt(image.style.left) - 10 + 'px'; break; case 'ArrowRight': image.style.left = parseInt(image.style.left) + 10 + 'px'; break; } }); // Menu manipulation function changeText(text) { var textDiv = document.getElementById('text'); textDiv.innerHTML = text; } // Div dragging var outer = document.getElementById('outer'); var inner = document.getElementById('inner'); var thumb = document.getElementById('thumb'); var isDragging = false; var lastX, lastY; inner.addEventListener('mousedown', function(e) { isDragging = true; lastX = e.clientX; lastY = e.clientY; }); document.addEventListener('mousemove', function(e) { if (isDragging) { var deltaX = e.clientX - lastX; var deltaY = e.clientY - lastY; inner.style.left = parseInt(inner.style.left) + deltaX + 'px'; inner.style.top = parseInt(inner.style.top) + deltaY + 'px'; lastX = e.clientX; lastY = e.clientY; var outerRect = outer.getBoundingClientRect(); var innerRect = inner.getBoundingClientRect(); if (innerRect.left < outerRect.left) { inner.style.left = outerRect.left + 'px'; } if (innerRect.right > outerRect.right) { inner.style.left = outerRect.right - inner.offsetWidth + 'px'; } if (innerRect.top < outerRect.top) { inner.style.top = outerRect.top + 'px'; } if (innerRect.bottom > outerRect.bottom) { inner.style.top = outerRect.bottom - inner.offsetHeight + 'px'; } } }); document.addEventListener('mouseup', function() { isDragging = false; }); // Scrollbar manipulation var scrollbar = document.getElementById('scrollbar'); var thumb = document.getElementById('thumb'); var image = document.getElementById('image'); var minHeight = 100; var maxHeight = 400; thumb.addEventListener('mousedown', function(e) { e.preventDefault(); var startY = e.clientY; var startHeight = thumb.offsetHeight; document.addEventListener('mousemove', resizeImage); document.addEventListener('mouseup', removeListeners); function resizeImage(e) { var deltaY = e.clientY - startY; var newHeight = startHeight + deltaY; if (newHeight < minHeight) { newHeight = minHeight; } else if (newHeight > maxHeight) { newHeight = maxHeight; } image.style.height = newHeight + 'px'; var thumbHeight = newHeight / maxHeight * scrollbar.offsetHeight; thumb.style.height = thumbHeight + 'px'; var thumbTop = newHeight / maxHeight * (scrollbar.offsetHeight - thumbHeight); thumb.style.top = thumbTop + 'px'; } function removeListeners() { document.removeEventListener('mousemove', resizeImage); document.removeEventListener('mouseup', removeListeners); } }); </script> </body> </html> ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值