Swift版PhotoStackView——照片叠放视图
前言之前流行过一种图片展示视图——photo stack,即照片叠放视图。大致上是这个样子的: 最后的效果图如下
这样重复造轮子的目的是什么呢?一方面,不使用UICollectionViewLayout而是纯粹的用UIView来实现,提高灵活性,方便“私人订制”。另一方面,学习大神的源代码,从中学习一下自定义库的书写方式等。最后,swift。。天杀的swift,是谁说swift对新手友好来着
(图片有些卡,测试运行时还是非常流畅的) 思路
代码computed property & stored propertyoc中可以声明属性然后覆写setter或getter,从而实现赋值或取值时进行一些操作的功能,如下代码 @property(strong,nonatomic) NSString *someString
- (void)setSomeString() {...}
- (NSString *)someString() {...}
swift中相对应的写法,目前我知道的有两种,一种是使用computed property 的set和get,缺点是必须同时声明一个stored property(?可以不用吗,求科普),很像oc2.0之前的属性。另一种是使用监听器(didSet和willSet),缺点是只能对setter操作,不能对getter操作。 由上,该类用到的属性如下(部分): //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:首先移除当前所有的子视图,接下来遍历新数组,那句判断 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)
}
//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分别指哪个吗? /** 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)
}
有了这些方法,我们就能使用动画后处理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,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),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…以后多接触,说不定就能喜欢上这门语言了:] (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |