仅阐述简单的实现思路,希望可以激发对于自定义UI控件的想象
demo
重点
-
UIImageView:
-
height
属性跟随UIScrollView
的滑动而线性变化 -
contentMode
属性设定为UIViewContentModeScaleAspectFill
-
-
UIScrollView:
- 初始
contentInset
属性的height
设定为UIImageView
对象的height
- 初始
简单说就是把scene里的views设置为UIImageView在下UIScrollView在上,利用UIScrollView的scrollViewDidScroll:
来线性更改UIImageView对象的height
Storyboard的设置
先来看完整的view hierachy:
首先,先摆一个UIImageView控件在view的最上方,配置top, leading, trailing的constraints贴边,height这里设为width的9/16
UIImageView(这里的view背景色设为了深灰色)
对应的constraints
别忘了配置contentMode
为UIViewContentModeScaleAspectFill
:
接着摆一个UIScrollView,覆盖整个view,因此UIImageView是在UIScrollView的下层。此时的UIScrollView是空的,需要添加并且仅能只有一个子view,这里的子view取名container view。如此就可以在container view上添加自己想要的UI控件了,为了简化demo这里只加了俩UILabel和doge的图片。
UIScrollView的hierachy
doge is watching you
UIScrollView的constraints
关于UIScrollView需要注意的一点是它的滑动区域由contentSize
的大小决定,因此它的功能相当丰富,可以用作多张图片的横向paging浏览,也可以像本文的例子一样纵向页面的连续滑动。目前我知道的contentSize
的设置方式有两种:
- Storyboard里通过container view与UIScrollView的constraints关系让系统自动配置;
- 代码里设置
这里使用了第一种方式,也就是把container view的四边与UIScrollView边缘贴齐。但这样还没完事,因为container view还不知道自己的大小是多少。这里的做法是把container view的width设定为与根view一致,因为scene的根view会被系统配置为与手机的显示屏大小一致,这样就可以拿到container view的大小了。然后因为想突出滑动效果所以把height固定为600
。
container view的constraints
代码
按照国际惯例,先把需要操作的views拖IBOutlet
过去对应的UIViewController里,并且声明实现UIScrollViewDelegate
:
@interface ViewController : UIViewController<UIScrollViewDelegate>
@property (weak, nonatomic) IBOutlet UIImageView *myImageView;
@property (weak, nonatomic) IBOutlet UIScrollView *myScrollView;
记得把delegate
的实现对象告诉UIScrollView对象(可以放在viewDidLoad:
里):
self.myScrollView.delegate = self;
接着overrideviewDidLayoutSubviews
方法,这是在scene初始化时系统在根据Storyboard的设定layout完views之后的callback,是开发者希望在用户将要看到画面前对views作修改的好地方。它的官方解释里说到:
When the bounds change for a view controller’s view, the view adjusts the positions of its subviews and then the system calls this method.
因此除了在打开scene时会被调用之外,改变view的bound
后也会被调用,因为subviews需要根据父view的bound
来定位。
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.myScrollView.contentInset = UIEdgeInsetsMake(self.myImageView.frame.size.height, 0, 0, 0);
}
这里提一下contentOffset
和contentInset
的区别:
-
contentOffset
:CGPoint
结构体,即二维坐标系的坐标(x, y)
,对于UIScrollView是指content
左上角在其bound里的坐标。因为滑动UIScrollView实质是在移动content
,所以contentOffset
会随着滑动而变化,例如上滑的话contentOffset.y
是减小,下滑就是增加。 -
contentInset
:UIEdgeInsets
结构体,与CSS里的padding
类似。简单说就是给content
填充的空间,这些空间分上下左右四个维度且不会随着滑动而改变。
这里给myScrollView
添加与myImageView.frame.size.height
等高的上部contentInset
是为了看到下层的myImageView
。
前面的铺垫终于要发挥作用啦~实现delegate
协议里的scrollViewDidScroll:
方法。每次滑动myScrollView
时,系统都会到此callback,让开发者可以根据滑动做相应的逻辑!
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offsetY = scrollView.contentOffset.y;
NSLog(@"offset: %f", offsetY);
NSLog(@"inset: %f", scrollView.contentInset.top);
CGRect myImageViewFrame = self.myImageView.frame;
if (offsetY < 0) {
self.myImageView.frame = CGRectMake(myImageViewFrame.origin.x, myImageViewFrame.origin.y, myImageViewFrame.size.width, -offsetY * 1.2);
}
}
这里有趣的一点是改变contentInset
会影响contentOffset
,因此程序在运行了viewDidLayoutSubviews
后再运行scrollViewDidScroll:
。由此进一步知道contentOffset
是以内容部分的坐标为准。
scene初始化时的log
offsetY
小于0是myScrollView
的内容坐标低于myScrollView.bound
上边缘的时候,此处即是低于显示屏上边缘。为了实现上滑开始阶段myImageView
有一个缩小的效果,因此把它的height
设为-offsetY * 1.2
。如果只需要纵向压缩的效果,设为-offsetY
即可。
由于前面已经把myImageView
的contentMode
设为UIViewContentModeScaleAspectFill
,其作用是让照片按比例填满myImageView
,因此height
小于照片填满时的height
时,图片会纵向缩窄,所以上滑看起来像是以1/2的速度缩窄,反之则等比放大,即下滑拉到尽头时的弹性效果~
后记
其实这UX效果很大程度参考了Stack Overflow的一篇回答。本以为需要更复杂些的代码逻辑,没想到一个简单的contentMode
能起到这么大作用。
觉得UIScrollView还有好多有意思的API,有空再去学习下~