Swift版PhotoStackView——照片叠放视图

前言

之前流行过一种图片展示视图——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,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,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…以后多接触,说不定就能喜欢上这门语言了:]

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。