仅阐述简单的实现思路,希望可以激发对于自定义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,有空再去学习下~