Swift版PhotoStackView——照片叠放视图
2015-06-02 22:52
399 查看
前言
之前流行过一种图片展示视图——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不变,否则为其重新生产一个旋转角度(产生照片堆效果),这样做保证了添加照片时原先的照片形状不会变。
Set up & Touches
初始化的工作非常简单,一方面为属性设置默认值,另一方面添加手势监听。//MARK: Set up override init(frame: CGRect) { super.init(frame: frame) setup() } required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } func setup() { //default value borderWidth = 5.0 showBorder = true rotationOffset = 4.0 let panGR = UIPanGestureRecognizer(target: self, action: Selector("handlePan:")) addGestureRecognizer(panGR) let tapGR = UITapGestureRecognizer(target: self, action: Selector("handleTap:")) addGestureRecognizer(tapGR) reloadData() } override func sendActionsForControlEvents(controlEvents: UIControlEvents) { super.sendActionsForControlEvents(controlEvents) highlighted = (controlEvents == .TouchDown) }
sendActionsForControlEvents是自定义UIControl时可能会使用或重写的方法,作用是发送事件。为了说明这一点,这里顺带附上view的触摸方法:
//MARK: Touch Methods override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) { super.touchesBegan(touches, withEvent: event) sendActionsForControlEvents(.TouchDown) } override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) { super.touchesMoved(touches, withEvent: event) sendActionsForControlEvents(.TouchDragInside) } override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) { super.touchesEnded(touches, withEvent: event) sendActionsForControlEvents(.TouchCancel) }
例如,当用户点击该控件时,发送UIControlEventTouchDown事件,这样使用该控件的人就可以通过addTarget:selector:forControlEvent:方法对此事件添加监听了。我们平时最经常使用的button不就是对TouchUpInside事件进行监听的吗。
reloadData
刷新视图时,要做的事情有:重新获取size,计算frame,添加border,设置images/** use this method to reload photo stack view when data has changed */ func reloadData() { if dataSource == nil { photoImages = nil return } if let number = dataSource?.numberOfPhotosInStackView(self) { var images = [UIView]() let border = borderImage?.resizableImageWithCapInsets(UIEdgeInsets(top: borderWidth, left: borderWidth, bottom: borderWidth, right: borderWidth)) let topIndex = indexOfTopPhoto() for i in 0..<number { if let image = dataSource?.stackView(self, imageAtIndex: i) { //add image view for every image let imageView = UIImageView(image: image) var viewFrame = CGRectMake(0, 0, image.size.width, image.size.height) if let ds = dataSource where ds.respondsToSelector(Selector("stackView:sizeOfPhotoAtIndex:")) { let size = ds.stackView!(self, sizeOfPhotoAtIndex: i) viewFrame.size = size } imageView.frame = viewFrame let view = UIView(frame: viewFrame) //add border for view if showBorder { if let b = border { viewFrame.origin = CGPoint(x: borderWidth, y: borderWidth) imageView.frame = viewFrame view.frame = CGRect(x: 0, y: 0, width: imageView.frame.width + 2 * borderWidth, height: imageView.frame.height + 2 * borderWidth) let backgroundImage = UIImageView(image: b) backgroundImage.frame = view.frame view.addSubview(backgroundImage) } else { view.layer.borderWidth = borderWidth view.layer.borderColor = UIColor.whiteColor().CGColor } } view.addSubview(imageView) //add view to array images.append(view) view.tag = i view.center = CGPoint(x: CGRectGetMidX(bounds), y: CGRectGetMidY(bounds)) } } photoImages = images goToImageAtIndex(topIndex) } }
这里要干的仅有前三件事,添加photos的任务交给photoImages的setter去做,这个之前已经说过了。
逻辑相关
在谈这个之前,让我们先来了解一下view的组织方式。如果一个view内有若干个subview,你知道subviews.lastObject和subviews.firstObject分别指哪个吗?事实上,越是靠近我们的,下标识越小。换句话说,subviews[0]指的是子视图中位于最底层的,被其他子视图遮住了的那一个,而subviews.lastObject指的则是最顶层的,能被我们看到(一般来说)的。
这样,很容易能得出下面几个函数
/** find the index of top photo :returns: index of top photo */ func indexOfTopPhoto() -> Int { if let images = photoImages, let photo = topPhoto() { if let index = find(images, photo) { return index } } return 0 } /** get the top photo on photo stack :returns: current first photo */ func topPhoto() -> UIView? { if subviews.count == 0 { return nil } return subviews[subviews.count - 1] as? UIView } /** jump to photo at index */ func goToImageAtIndex(index: Int) { if let photos = photoImages { for view in photos { if let idx = find(photos, view) where idx < index { sendSubviewToBack(view) } } } makeStraight(topPhoto()!, animated: false) }
swift知识补充:swift中没有indexForObject:这样的方法,众所周知,swift更多的是“函数式编程”,因此Int、Float甚至Array、Dictionary都是结构体(虽然和类挺相似),我们也是使用一些函数来对这些结构体操作。例如这里取代上述方法的是find()函数
有了这些方法,我们就能使用动画后处理view的层级关系了。
动画相关
这里用到的动画相关的都非常简单,所以直接上代码://MARK: Animations func returnToCenter(view: UIView) { UIView.animateWithDuration(0.2, animations: { () -> Void in view.center = CGPoint(x: CGRectGetMidX(self.bounds), y: CGRectGetMidY(self.bounds)) }) } func flickAway(view: UIView, withVelocity velocity: CGPoint) { if let del = delegate where del.respondsToSelector(Selector("stackView:willFlickAwayPhotoFromIndex:toIndex:")) { let from = indexOfTopPhoto() var to = from + 1 if let number = dataSource?.numberOfPhotosInStackView(self) where to >= number { to = 0 } del.stackView!(self, willFlickAwayPhotoFromIndex: from, toIndex: to) } let width = CGRectGetWidth(bounds) let height = CGRectGetHeight(bounds) var xPosition: CGFloat = CGRectGetMidX(bounds) var yPosition: CGFloat = CGRectGetMidY(bounds) if velocity.x > 0 { xPosition = CGRectGetMidX(bounds) + width } else if velocity.x < 0 { xPosition = CGRectGetMidX(bounds) - width } if velocity.y > 0 { yPosition = CGRectGetMidY(bounds) + height } else if velocity.y < 0 { yPosition = CGRectGetMidY(bounds) - height } UIView.animateWithDuration(0.1, animations: { () -> Void in view.center = CGPoint(x: xPosition, y: yPosition) }) { (finished) -> Void in self.makeCrooked(view, animated: true) self.sendSubviewToBack(view) self.makeStraight(self.topPhoto()!, animated: true) self .returnToCenter(view) if let del = self.delegate where del.respondsToSelector("stackView:didRevealPhotoAtIndex:") { del.stackView!(self, didRevealPhotoAtIndex:self.indexOfTopPhoto()) } } } func rotate(degree: Int, onView view: UIView, animated: Bool) { let radian = CGFloat(degree) * CGFloat(M_PI) / 180 if animated { UIView.animateWithDuration(0.2, animations: { () -> Void in view.transform = CGAffineTransformMakeRotation(radian) }) } else { view.transform = CGAffineTransformMakeRotation(radian) } } func makeCrooked(view: UIView, animated: Bool) { let min = Int(-rotationOffset) let max = Int(rotationOffset) let scope = UInt32(max - min - 1) let randomDegree = Int(arc4random_uniform(scope)) let degree: Int = min + randomDegree rotate(degree, onView: view, animated: animated) } func makeStraight(view: UIView, animated: Bool) { rotate(0, onView: view, animated: animated) }
swift相关:一直不是太明白swift中可选值的意义是什么。到是给我们带来了不少麻烦,因为不怎么想全都用强解(!),所以用了大量if let解包的方式。其中oc中很简单就能完成的操作:
if (self.delegate responseToSelector:@selector(@"someMethod")) { [self.delegate someMethod]; }
到了swift中
if let del = delegate where del.responseToSelector(Selector("someMethod") { del.someMethod() }
且不说swift的Selector机制,每次都这样写可真是要累死人了:[
手势
和之前说的一样,pan手势中要做的就是通知代理,让view随手指移动,释放手指后根据velocity将view归位或切换到最后一张。//MARK: Gesture Recognizer func handlePan(recognizer: UIPanGestureRecognizer) { if let topPhoto = self.topPhoto() { let velocity = recognizer.velocityInView(recognizer.view) let translation = recognizer.translationInView(recognizer.view!) if recognizer.state == .Began { sendActionsForControlEvents(.TouchCancel) if let del = delegate where del.respondsToSelector(Selector("stackView:willBeginDraggingPhotoAtIndex")) { del.stackView!(self, willBeginDraggingPhotoAtIndex: self.indexOfTopPhoto()) } } else if recognizer.state == .Changed { topPhoto.center = CGPoint(x: topPhoto.center.x + translation.x, y: topPhoto.center.y + translation.y) recognizer.setTranslation(CGPoint.zeroPoint, inView: recognizer.view) } else if recognizer.state == .Ended || recognizer.state == .Cancelled { if abs(velocity.x) > 200 { flickAway(topPhoto, withVelocity: velocity) } else { returnToCenter(topPhoto) } } } } func handleTap(recognizer: UIGestureRecognizer) { sendActionsForControlEvents(.TouchUpInside) if let del = delegate where del.respondsToSelector(Selector("stackView:didSelectPhotoAtIndex:")) { del.stackView!(self, didSelectPhotoAtIndex: self.indexOfTopPhoto()) } }
Show All Images
创建一层黑色的遮罩(view),然后将每个view从自己原来的位置移到计算好的新位置,为了产生顺序关系,为每个view 的动画设置各自的delay。点击黑色遮罩后所有view回位,view消失。func showAllPhotos() { let screenBounds = UIScreen.mainScreen().bounds let maskView = UIView(frame: screenBounds) maskView.backgroundColor = UIColor.blackColor() maskView.alpha = 0 UIApplication.sharedApplication().keyWindow?.addSubview(maskView) UIView.animateWithDuration(0.1, delay: 0.0, options: nil, animations: { () -> Void in maskView.alpha = 1.0 }) { (_) -> Void in } let column = 3 let imageWidth = 80 let padding = (Int(screenBounds.width) - column * imageWidth) / (column + 1) if let photos = photoImages { for view in photos { //set the initial location view.removeFromSuperview() maskView.addSubview(view) view.frame = frame if let index = find(photos, view) { UIView.animateWithDuration(0.1, delay: NSTimeInterval(Double(index) * 0.1), options: nil, animations: { () -> Void in view.frame = CGRect(x: padding + (index % column) * (imageWidth + padding), y: padding + (index / column) * (padding + imageWidth), width: imageWidth, height: imageWidth) }, completion: { (finished) -> Void in }) } } } let tapGR = UITapGestureRecognizer(target: self, action: Selector("removeMaskView:")) maskView.addGestureRecognizer(tapGR) }
func removeMaskView(recognizer: UITapGestureRecognizer) { let maskView = recognizer.view! for i in stride(from: maskView.subviews.count - 1, through: 0, by: -1) { let photo = maskView.subviews[i] as? UIView UIView.animateWithDuration(0.25, animations: { () -> Void in photo?.frame = self.frame }, completion: { (_) -> Void in }) } UIView.animateWithDuration(0.25, animations: { () -> Void in maskView.alpha = 0.0 }) { (_) -> Void in maskView.removeFromSuperview() self.reloadData() } }
源代码
本文的源代码可以从这里下载总结
刚接触swift不久就直接写东西,确实不好写,swift为了安全做了很多努力,但却增加了一些注意事项。一个Int乘以一个Double都报错的“强类型“检查实在不习惯。但是不得不说这门语言确实有意思,省略小括号,不用分号,switch,where…以后多接触,说不定就能喜欢上这门语言了:]相关文章推荐
- Swift#使用字典实现属性列表的存储
- 奔五的人学iOS:用swift实现获取拼音首字母,支持取一句话中每字拼音首字母
- ?Swift获取手机设备信息
- Swift 代码分享——Calculator without MVC
- Swift的属性,方法,下标脚本以及继承
- Swift中的设计模式
- swift 点击cell没反应,点击后应该跳到指定页面
- 【swift】15-0601 枚举类型
- The Swift Programming Language - Closures
- Swift & the Objective-C Runtime
- Swift学习笔记-函数和闭包(2)
- Swift学习笔记-函数和闭包(1)
- 获取当前时间 swift
- swift 笔记
- Swift学习笔记-判断字符出现的次数
- Swift面向对象-枚举
- 【swift】15-0530 闭包
- 【swift】15-0529 In-Out参数 函数类型
- Swift开发教程--实现UITableView报错does not conform to protocol 'UITableViewDataSource‘
- 从0开始学习Swift开发IOS应用(5)——Button