UIImageView与UIScrollView的一点UX效果实现

下拉弹性效果 | 上拉慢速压缩效果

Posted by DesmondDAI on August 27, 2016


仅阐述简单的实现思路,希望可以激发对于自定义UI控件的想象


demo

自制demo效果

重点

  • UIImageView:
    • height属性跟随UIScrollView的滑动而线性变化
    • contentMode属性设定为UIViewContentModeScaleAspectFill
  • UIScrollView:
    • 初始contentInset属性的height设定为UIImageView对象的height

简单说就是把scene里的views设置为UIImageView在下UIScrollView在上,利用UIScrollViewscrollViewDidScroll:来线性更改UIImageView对象的height

Storyboard的设置

先来看完整的view hierachy:

成品的view hierachy

首先,先摆一个UIImageView控件在view的最上方,配置top, leading, trailingconstraints贴边,height这里设为width的9/16

UIImageView的配置 UIImageView(这里的view背景色设为了深灰色)

UIImageView的constraints 对应的constraints

别忘了配置contentModeUIViewContentModeScaleAspectFill: UIImageView的contentMode

接着摆一个UIScrollView,覆盖整个view,因此UIImageView是在UIScrollView的下层。此时的UIScrollView是空的,需要添加并且仅能只有一个子view,这里的子view取名container view。如此就可以在container view上添加自己想要的UI控件了,为了简化demo这里只加了俩UILabeldoge的图片。:full_moon_with_face:

UIScrollView的hierachy UIScrollView的hierachy

UIScrollView的内部 doge is watching you

UIScrollView的constraints UIScrollView的constraints

关于UIScrollView需要注意的一点是它的滑动区域由contentSize的大小决定,因此它的功能相当丰富,可以用作多张图片的横向paging浏览,也可以像本文的例子一样纵向页面的连续滑动。目前我知道的contentSize的设置方式有两种:

  • Storyboard里通过container viewUIScrollViewconstraints关系让系统自动配置;
  • 代码里设置

这里使用了第一种方式,也就是把container view的四边与UIScrollView边缘贴齐。但这样还没完事,因为container view还不知道自己的大小是多少。这里的做法是把container viewwidth设定为与根view一致,因为scene的根view会被系统配置为与手机的显示屏大小一致,这样就可以拿到container view的大小了。然后因为想突出滑动效果所以把height固定为600

container view的设置 container view的constraints

代码

按照国际惯例,先把需要操作的viewsIBOutlet过去对应的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时会被调用之外,改变viewbound后也会被调用,因为subviews需要根据父viewbound来定位。

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    self.myScrollView.contentInset = UIEdgeInsetsMake(self.myImageView.frame.size.height, 0, 0, 0);
}

这里提一下contentOffsetcontentInset的区别:

  • contentOffsetCGPoint结构体,即二维坐标系的坐标(x, y),对于UIScrollView是指content左上角在其bound里的坐标。因为滑动UIScrollView实质是在移动content,所以contentOffset会随着滑动而变化,例如上滑的话contentOffset.y是减小,下滑就是增加。
  • contentInsetUIEdgeInsets结构体,与CSS里的padding类似。简单说就是给content填充的空间,这些空间分上下左右四个维度且不会随着滑动而改变。

这里给myScrollView添加与myImageView.frame.size.height等高的上部contentInset是为了看到下层的myImageView

前面的铺垫终于要发挥作用啦~实现delegate协议里的scrollViewDidScroll:方法。每次滑动myScrollView时,系统都会到此callback,让开发者可以根据滑动做相应的逻辑!:smiling_imp:

- (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是以内容部分的坐标为准。 offset_inset scene初始化时的log

offsetY小于0是myScrollView的内容坐标低于myScrollView.bound上边缘的时候,此处即是低于显示屏上边缘。为了实现上滑开始阶段myImageView有一个缩小的效果,因此把它的height设为-offsetY * 1.2。如果只需要纵向压缩的效果,设为-offsetY即可。

由于前面已经把myImageViewcontentMode设为UIViewContentModeScaleAspectFill,其作用是让照片按比例填满myImageView,因此height小于照片填满时的height时,图片会纵向缩窄,所以上滑看起来像是以1/2的速度缩窄,反之则等比放大,即下滑拉到尽头时的弹性效果~

后记

其实这UX效果很大程度参考了Stack Overflow的一篇回答。本以为需要更复杂些的代码逻辑,没想到一个简单的contentMode能起到这么大作用。

觉得UIScrollView还有好多有意思的API,有空再去学习下~

参考