头部视图拉伸放大效果实现原理解析

96
Tony_Yang
2017.08.24 19:27* 字数 869
stretchableView.gif

在很多APP中大家应该都见过一些类似个人主页的页面,下边是tableview列表,上边的头部视图可以拉伸放大.

在一番研究实现了这个拉伸效果后,顺便把这个功能进行了封装,使用时只需两行代码

1.初始化调用

stretchableView = LPStretchableHeaderView(stretchableView: bgImageView)

2.代理方法实现:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    stretchableView.scrollViewDidScroll(scrollView)
}

github代码demo


下边是实现原理的解析,大神请绕路~~~

实现原理解析

一:设置头部视图
创建imageView,并添加到控制器的view上

由于在往下拖动列表时,头部视图的y值是没有跟着下移的,所以肯定不能让它作为tableview的tableHeaderView,只能把上边的图片视图添加到Controller的view上

二:设置tableview
  • 创建一个跟头部视图同样大小的空视图headerView作为tableview的tableHeaderView,来填充头部图片视图区域
  • 把tableview的背景颜色设置为clearColor,这样就可以看到下面的头部图片了

这样一来,我们的列表视图的实际大小就占据了整个屏幕,并且不影响看到下面的头部图片,而且在头部图片区域拖动的时候(实际拖动的是列表的tableHeaderView)也可以触发列表的滚动事件,同时上滑的时候列表的顶部滚动区域也达到了导航栏位置,一石好几鸟啊~😎

三:头部视图添加子控件

一般在头部视图的图片上方,还会显示昵称、头像等信息,我们需要把这些子控件添加到刚才创建的空视图headerView

注意:不要添加到头部视图的imageView中!

因为稍后我们在在拉伸列表时,会改变imageView的frame,但是并没有改变其内的子控件的frame,所以子控件位置会发生错乱。

现在控件布局如下:

override func viewDidLoad() {
    super.viewDidLoad()
    
    // 头部图片视图
    bgImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: SCREEN_WIDTH * imageRatio))
    bgImageView.image = UIImage(named: "123")
    view.addSubview(bgImageView)
    
    // 列表
    tableView = UITableView(frame: CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: SCREEN_HEIGHT))
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    tableView.dataSource = self
    tableView.delegate = self
    tableView.showsVerticalScrollIndicator = false
    tableView.backgroundColor = UIColor.clear // 注意要清除列表的背景颜色
    view.addSubview(tableView)
    
    // 创建一个空白view来进行填充tableHeaderView
    let headerView = UIView(frame: bgImageView.bounds)
    tableView.tableHeaderView = headerView
    
    // 添加label子控件
    let nameLabel = UILabel(frame: CGRect(x: 0, y: 150, width: bgImageView.width, height: 40))
    nameLabel.text = "哈哈哈😆"
    nameLabel.textAlignment = .center
    nameLabel.textColor = UIColor.white
    headerView.addSubview(nameLabel) // 注意要把子控件添加到headerView中
    
    // 导航栏
    makeNavView()
}
三:实现滚动拉伸放大效果
  • 下拉时通过tableview在y轴的偏移量来决定头部图片的高度拉伸多少
  • 通过图片拉伸的高度及图片原来的宽高比例计算出要拉伸的宽度
  • 通过图片拉伸后的宽度计算出x值应向左的偏移量

1)首先,我们会在头部视图初始化的时候记录它的原始frame,这个后边会用到

// 图片的原始frame
originFrame = bgImageView.frame

2)取出列表y值的偏移量

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let yOffset = scrollView.contentOffset.y
}

3)图片拉伸后的高度

// 下拉时yOffset值是负数,所以需要减
frame.size.height = originFrame.size.height - yOffset

4)图片拉伸后的宽度

// 通过图片的宽高比imageRatio及拉伸后的高度等比计算新宽度
frame.size.width = frame.size.height / imageRatio

5)x值的位置重新计算

// 图片宽高同时变大后,图片会整体向右偏移,所以需要重新计算x值
frame.origin.x = originFrame.origin.x - (frame.size.width - originFrame.size.width) * 0.5

当列表上滑时,移动头部图片跟着向上移动

var frame = originFrame
frame.origin.y = originFrame.origin.y - yOffset
bgImageView.frame = frame

最终,处理代码为:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    
    let yOffset = scrollView.contentOffset.y
    
    // 头部图片拉伸设置
    if yOffset > 0 { // 上滑
        var frame = originFrame
        frame.origin.y = originFrame.origin.y - yOffset
        bgImageView.frame = frame
        
    } else { // 下拉
        var frame = originFrame
        frame.size.height = originFrame.size.height - yOffset
        frame.size.width = frame.size.height / imageRatio
        frame.origin.x = originFrame.origin.x - (frame.size.width - originFrame.size.width) * 0.5
        bgImageView.frame = frame
    }
}



封装

通过以上实现可以看出,所有的操作都是通过拿到scrollViewDidScroll回调方法中列表y轴的偏移量,然后对头部视图bgImageView的frame进行更改实现的。

所以其实没啥好封装的,如果非得封装的话,那就只需要把bgImageView控件和对它frame更改的操作拿出去就ok了。

于是我建了一个工具类LPStretchableHeaderView,实现如下:

LPStretchableHeaderView.swift文件

import UIKit

public class LPStretchableHeaderView: NSObject {

    private var stretchView = UIView()
    private var imageRatio: CGFloat
    private var originFrame = CGRect()
    
    public init(stretchableView: UIView) {
        
        stretchView = stretchableView
        originFrame = stretchableView.frame
        imageRatio = stretchableView.bounds.height / stretchableView.bounds.width
    }
    
    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        let yOffset = scrollView.contentOffset.y
        if yOffset > 0 { // 往上移动
            var frame = originFrame
            frame.origin.y = originFrame.origin.y - yOffset
            stretchView.frame = frame
        } else { // 往下移动
            var frame = originFrame
            frame.size.height = originFrame.size.height - yOffset
            frame.size.width = frame.size.height / imageRatio
            frame.origin.x = originFrame.origin.x - (frame.size.width - originFrame.size.width) * 0.5
            stretchView.frame = frame
        }
    }
}

这样,以后遇到有这种需求的页面,两行代码就搞定了~


如果感觉有帮助就点个❤️鼓励下吧😄
随笔
Web note ad 1