头部视图拉伸放大效果实现原理解析
在很多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
}
}
}
这样,以后遇到有这种需求的页面,两行代码就搞定了~