前言
之前流行过一种图片展示视图——photo stack,即照片叠放视图。大致上是这个样子的:
(图片出自code4app)
现在我们已经能够使用UICollectionViewLayout来实现这种视图了。Apple给的示例代码中就有这样一个layout,并且示例代码中不仅仅是展示这样的视图,还有非常棒的layout过度动画(结合手势)。在这之前,也有非常多的开源代码能实现这样的效果。本文正是借鉴了开源的源代码“PhotoStackView”,使用Objective-C实现,并且带手势移动图片的功能。由于这是上一学期课设的时候拿来用的库,结果现在找不到了,无法给出链接,还望见谅。
最后的效果图如下
移动、添加与删除
下一张
这样重复造轮子的目的是什么呢?一方面,不使用UICollectionViewLayout而是纯粹的用UIView来实现,提高灵活性,方便“私人订制”。另一方面,学习大神的源代码,从中学习一下自定义库的书写方式等。最后,swift。。天杀的swift,是谁说swift对新手友好来着
当然,这里也不是简单的对源代码的搬运、抄袭与翻译,我还根据自己的需要给他改了一个bug,添加了一些功能,比如全屏展示与返回等。示例图如下:
- 全屏展示
(图片有些卡,测试运行时还是非常流畅的)
思路
- 使用若干和自己一样大的view来装载图片,每次添加新图片到视图上时为其随机旋转一定角度,把最前面的这张旋转角度设为0。
- “view装载图片”的意思是,view里面放一个imageView,为什么不直接用imageVIew呢?因为我们要放边框,边框可以是图片,另外在highlighted状态下也可以用图片或自定义颜色铺在图片上,因此使用一个view统一管理
- 添加pan手势,当手指移动时让view跟着做相应平移,手指松开时根据velocity决定返回还是移到最后
- 综上,我们还需要不少辅助方法,例如获取顶部图片和其下标、使用动画移动view、让view旋转到某一度数等
代码
computed property & stored property
oc中可以声明属性然后覆写setter或getter,从而实现赋值或取值时进行一些操作的功能,如下代码
@property(strong, nonatomic) NSString *someString
- (void)setSomeString() {
...}
- (NSString *)someString() {
...}
swift中相对应的写法,目前我知道的有两种,一种是使用computed property 的set和get,缺点是必须同时声明一个stored property(?可以不用吗,求科普),很像oc2.0之前的属性。另一种是使用监听器(didSet和willSet),缺点是只能对setter操作,不能对getter操作。
但是,要想实现重写父类的setSomeThing这样的功能,只能通过监听器的方法。否则会报错
由上,该类用到的属性如下(部分):
//MARK: computed property
var s_rotationOffset: CGFloat = 0.0
/// the scope of offset of rotation on every photo except the first one. default is 4.0.
/// ie, 4.0 means rotate iamge with degree between (-4.0, 4.0)
var rotationOffset: CGFloat {
set {
if s_rotationOffset == newValue {
return
}
s_rotationOffset = newValue
reloadData()
}
get {
return s_rotationOffset
}
}
var s_photoImages: [UIView]?
var photoImages: [UIView]? {
set {
//remove all subview and prepare to re-add all images from data source
for view in subviews {
view.removeFromSuperview()
}
if let images = newValue {
for view in images {
//keep the original transfrom for the existing images
if let index = find(images, view), count = s_photoImages?.count where index < count {
let existingView = s_photoImages![index]
view.transform = existingView.transform
} else {
makeCrooked(view, animated: false)
}
insertSubview(view, atIndex: 0)
}
}
s_photoImages = newValue
}
get {
return s_photoImages
}
}
override var highlighted: Bool {
didSet {
let photo = self.topPhoto()?.subviews.last as! UIImageView
if highlighted {
let view = UIView(frame: self.bounds)
view.backgroundColor = self.highlightColor
photo.addSubview(view)
photo.bringSubviewToFront(view)
} else {
photo.subviews.last?.removeFromSuperview()
}
}
}
override var frame: CGRect {
didSet {
if CGRectEqualToRect(oldValue, self.frame) {
return
}
reloadData()
}
}
上面大部分还有其他省略的大都是一样的思路:设置新值时调用reloadData刷新,主要是上面那个photoImage数组的setter:首先移除当前所有的子视图,接下来遍历新数组,那句判断if let index = find(images, view), count = s_photoImages?.count where index < count
的作用是判断此次循环中view是否是之前已经添加到界面上的,如果是,则保留其transform不变,否则为其重新生产一个旋转角度(产生照片堆效果)&#