最近工作不忙的时候会偷懒玩一会手机,会发现各大App中的一些小细节很吸引人,这次看到一个很有意思的效果,其实这个效果应用的已经很普遍,就是响应scrollView的下滑,NavigationBar从无到有(其实就是透明度从0到1的过程),看起来总是简单的,实现起来总会遇到点麻烦,这次也一样,经过查阅相关技术博客以及花点时间研究原理,也慢慢的理解实现了出来,个人还是觉得了解原理要比直接拿来代码要好的多,大体效果如下:
这里先附上代码的GitHub:https://github.com/YRunIntoLove/YSinaNavigationBarDemo
直接设置NavigationBar的背景颜色透明度----fail
首先想到的必然是直接修改背景(background)的alpha(透明度属性),即在scrollViewDidScroll中根据比例设置响应的透明度即可
self.navigationController?.navigationBar.backgroundColor = UIColor.orangeColor().colorWithAlphaComponent(alpha)
如果这么设置了,那么大家就和楼主一样想的太过简单了,尝试过后发现根本没有任何的效果,透明度没有任何的变化
设置NavigationBar的alpha----fail
如果设置背景颜色不能完成,那么我直接设置NavigationBar的透明度不就好了嘛
self.navigationController?.navigationBar.alpha = 0.3
这样子做当然可以,但是会发现一个小小的问题,就是贴在NavigationBar上的Title以及相关响应按钮等相关组件也会根据父视图的透明度变透明了,这似乎也不是咱们理想中的效果
利用runtime的关联属性,为NavigationBar添加一层自定义的视图层----Success
没有办法,打开百度一搜,果然会发现有很多这样子的尝试,最终还是需要动用runtime来自定义添加一个视图最为合适,动态关联属性算是runtime中很简单的知识了,花点时间了解一下也就会理解了,也可以在前面的博客
iOS开发----runtime关联对象(动态添加属性)中稍微的回顾一下。
首先需要新建一个NavigationBar的类目(拓展),因为最近学习Swift,代替Objective-C中类目的是Swift中的extension,需要在拓展中添加一个属性,说是属性,实际上是添加了一个get和set方法,自定义一个视图
var key:String = "CoverView"
extension UINavigationBar
{
var coverView:UIView?{
set{
//runtime添加动态关联的属性
objc_setAssociatedObject(self, &key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get{
//runtime读取动态关联的属性
return objc_getAssociatedObject(self, &key) as? UIView
}
}
/**
* 设置 背景色
*/
@available(iOS 8.0,*)
func setViewColor(color:UIColor)
{
//如果覆盖图层为nil
if(self.coverView == nil)
{
//设置背景图片及度量
self.setBackgroundImage(UIImage(), forBarMetrics: .Default)
//去除自定义背景图后形成的下端黑色横线
self.shadowImage = UIImage()
//设置图层的frame
let view = UIView(frame: CGRect(x: 0, y: -20, width: UIScreen.mainScreen().bounds.width, height: CGRectGetHeight(self.frame) + 20))
view.userInteractionEnabled = false//人机不交互
view.autoresizingMask = [.FlexibleWidth , .FlexibleHeight]//自适应宽度和高度
//将图层添加到导航Bar的底层
self.insertSubview(view, atIndex: 0)
//因为这里不是一个真正的属性,是在runtime时进行关联的属性,所以相关属性的修改需要实例对象来"赋值"
self.coverView = view
}
self.coverView?.backgroundColor = color
}
这里说一下,其实Xcode提供给大家一个查看UI层次的功能<#楼主才知道,- - 感觉对Xcode的了解太差劲了#> Debug -> View Debuging -> Capture View Hierarchy
它可以让我们看到不少组件中私有属性,在这里可以看到NavigationBar的子视图中有这个一个子视图(_ UINavigationBarBackground),他的子视图中有一个UIImageView,也就是当我们设置背景图片或者背景色的时候,应该就是这个视图在响应。
当然设置透明度用上面的方法是可以的,在颜色中添加透明度即可,但是习惯性的还是写了一个接口
/**
* 设置透明度
*/
@available (iOS 8.0, *)
func setViewAlpha(alpha:CGFloat)
{
//如果view = self.coverView不成立,就return
guard let view = self.coverView else
{
return
}
self.coverView!.backgroundColor = view.backgroundColor?.colorWithAlphaComponent(alpha)
}
上面的效果因为push之后出现的NavigationBar的颜色不一样,所需的效果也不一样,所以为了避免影响之后NavigationBar的效果,需要写一个移除该图层的方法,如下
/**
* 清除图层,视图消失时需要调用该方法,不然会影响其他页面的效果
*/
@available (iOS 8.0, *)
func relieveCover()
{
self.setBackgroundImage(nil, forBarMetrics: .Default)
coverView?.removeFromSuperview()
coverView = nil
}
首先需要定义几个相关的属性
class CustomHeaderView: UIView {
/// 代理
weak var delegate:CustomHeaderViewDelegate?
///底层控制ImageView缩放的View,后面通过更改它的frame属性来实现圆滑效果
var contentView:UIView! = UIView()
/// 存放外部传入的视图,即ImageView
var subView:UIView
/// 最大的下拉距离
var maxContentOff:CGFloat
/// 起点的纵坐标
private let originY:CGFloat = -64
接着实现自定义构造方法
init(subView:UIView,maxContentOff:CGFloat,headerViewSize: CGSize,delegate: CustomHeaderViewDelegate)
{
self.subView = subView//当前的imageView
self.delegate = delegate
self.maxContentOff = maxContentOff > 0 ? -maxContentOff : maxContentOff//因为向下滑动是负数,进行数字正负转换
super.init(frame: CGRectMake(0, 0, headerViewSize.width, headerViewSize.height))
//开始自动布局设置,意思是自动将subView的frame与superView相一致
subView.autoresizingMask = [.FlexibleTopMargin,.FlexibleBottomMargin,.FlexibleLeftMargin,.FlexibleRightMargin,.FlexibleWidth,.FlexibleHeight]
//此视图不显示越界的视图
self.clipsToBounds = false
self.contentView.frame = self.bounds
self.contentView.addSubview(subView)
//存放ImageView的视图需要显示越界的视图
self.contentView.clipsToBounds = true
self.addSubview(contentView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
因为涉及到往外传值,所以必须使用回调,这里使用的是Delegate(委托)回调,当然是可以使用闭包传值的,定义协议如下:
protocol CustomHeaderViewDelegate : class
{
/**
滚动已经到达最大偏移量,需要锁定滚动视图
:param: customHeaderView
:param: maxContentOffSet 最大偏移量
*/
@available(iOS 8.0,*)
func customHeaderView(customHeaderView:CustomHeaderView,lockScrollView maxContentOffSet:CGFloat)
/**
滚动过程中修改导航Bar的透明度
:param: customHeaderView
:param: alpha 透明度
*/
@available(iOS 8.0,*)
func customHeaderView(customHeaderView:CustomHeaderView,shouldChangeBarAlpha alpha:CGFloat)
}
最后就是需要一个对外开放的方法,设置之前说的相关contentView的frame实现圆滑效果以及触发Delegate方法
// MARK: - 对外接口
func layoutHeaderWillScroll(offSet:CGPoint)
{
//获取垂直偏移量
let contentOffY = offSet.y
//如果偏移量大于最大偏移量,因为是负数,所以是小于
if(contentOffY < maxContentOff)
{
//锁定坐标
self.delegate?.customHeaderView(self, lockScrollView: maxContentOff)
}
else if(contentOffY < 0)//如果小于0,表示headerView还显示在ScrollView中
{
var rect = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)
rect.origin.y += contentOffY ;
rect.size.height -= contentOffY;
self.contentView.frame = rect;
}
//64 + 当前的垂直偏移量
let alpha = (-originY + contentOffY) / self.frame.size.height
//设置透明度
self.delegate?.customHeaderView(self, shouldChangeBarAlpha: alpha)
}
在主ViewController中使用即可
定义初始化的属性以及在ViewDidLoad中进行初始化
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource,CustomHeaderViewDelegate{
var tableView: UITableView!
var imageView: UIImageView!
var imageHeight:CGFloat?
var imageDistance:CGFloat?
let barColor = UIColor.orangeColor()
override func viewDidLoad() {
super.viewDidLoad()
//设置导航栏的属性
self.navigationController?.navigationBar.setViewColor(barColor.colorWithAlphaComponent(0.0))
//设置列表属性
tableView = UITableView(frame: self.view.bounds)
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "Cell")
tableView.dataSource = self
self.view.addSubview(tableView)
//设置显示图片的视图
imageView = UIImageView(frame: CGRectMake(0, 0, self.view.bounds.size.width, 100))
imageView.contentMode = .ScaleAspectFill
imageView.image = UIImage(named: "backGround.jpg")
let customHeaderView = CustomHeaderView(subView: imageView, maxContentOff: -120, headerViewSize: CGSize(width: self.view.bounds.size.width,height: 100),delegate:self)
tableView.tableHeaderView = customHeaderView
}
为了不影响其他的控制器导航栏,不要忘记在消失的时候取消ScrollView的代理以及对NavigationBar图层的去除,
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
tableView.delegate = self;
}
override func viewWillDisappear(animated: Bool)
{
tableView.delegate = nil
super.viewWillDisappear(animated)
self.navigationController?.navigationBar.relieveCover()
}
在tableView中实现ScrollViewDidScroll:协议方法,调用headerView的对外接口即可
//MARK: - UIScrollView Delegate
func scrollViewDidScroll(scrollView: UIScrollView){
//获得当前的自定义HeaderView对象
let customView:CustomHeaderView = (scrollView as! UITableView).tableHeaderView as! CustomHeaderView
//设置滚动
customView.layoutHeaderWillScroll(scrollView.contentOffset)
}
实现CustomHeaderView的代理方法
//MARK: - CustomHeaderViewDelegate
func customHeaderView(customHeaderView: CustomHeaderView, lockScrollView maxContentOffSet: CGFloat) {
//锁定滚动视图
self.tableView.contentOffset.y = maxContentOffSet
}
func customHeaderView(customHeaderView: CustomHeaderView, shouldChangeBarAlpha alpha:CGFloat) {
//设置透明度
self.navigationController?.navigationBar.setViewColor(self.barColor.colorWithAlphaComponent(alpha))
}