iOS开发 | 动画之分解/重组动效

这里写图片描述
效果大致如上:

示例的Git地址:https://github.com/Goddreamwt/iOSAnimationSample

最近项目中有需要UIView的分解重组动画,网上找了好久,就找到一篇介绍相关效果的文档:重组/分解动画 UIView Refactor/Destruct Animation - 简书

参照作者介绍的gitHub介绍,大致实现了项目中的需求,先分享一下。
原gitHub地址

因为原作者是使用Swift写的,所以要用到OC和Swift混编(我是用OC),分享一下OC和Swift混编设置讲解博客OC项目中使用Swift

下面是OC方式使用

UIViewRefactorAndDestructExtension.swift

注意原gitHub上的UIViewRefactorAndDestructExtension.swift文件在OC里面无法直接调用,做了一下修改。

UIViewRefactorAndDestructExtension.swift文件:把新建一个swift文件,把代码贴到里面即可。也可以直接去git上面下载,链接再文章的最前面。

//
//  UIViewRefactorExtension.swift
//
//  Created by seedante on 15/11/8.
//  Copyright © 2015年 seedante. All rights reserved.
//

import Foundation
import UIKit

enum SDERefactorDirection{
    case Horizontal
    case Vertical
    case Diagonal
    case Custom
}

extension CGRect{
    var centerPoint: CGPoint{
        return CGPoint(x: origin.x + size.width / 2, y: origin.y + size.height / 2)
    }
}

extension UIView {
    //MARK: Refactor

    func refactor(){
        refactorWithNewFrame(destinationFrame: nil, piecesRegion: nil, shiningColor: nil)
    }
    //
    func refactorAll(destinationFrame: CGRect, jumpRect: CGRect, shiningColor: UIColor, direction: String, animationTime: TimeInterval, ratio: CGFloat, enableBigRegion: Bool){
        var direc: SDERefactorDirection!
        switch direction {
        case "Horizontal":
            direc = SDERefactorDirection.Horizontal
        case "Vertical":
            direc = SDERefactorDirection.Vertical
        case "Diagonal":
            direc = SDERefactorDirection.Diagonal
        default:
            direc = SDERefactorDirection.Custom

        }

        refactorWithNewFrame(destinationFrame: destinationFrame, piecesRegion: jumpRect, shiningColor: shiningColor, direction: direc, refactorTime: animationTime, pieceRatio: ratio, enableBigRegion: enableBigRegion)
    }

    func customRefactor(){
        //DIY...
    }

    /**
     它是移动动画的替代方法。 注意:自动布局无法正常工作。
           - 参数destinationFrame:要更改的框架。 如果没有指定此参数,它将自动重构自身。 我建议你从CGRect更改这个? 使用CGRect时。
           - 参数jumpRect:所有片段出现的区域,如果nil是视图的2X帧。
           - 参数shiningColor:如果指定此参数,则添加如电焊接灯。
           - 参数方向:动画方向
           - 参数animationTime:动画的总时间
           - 参数比:要查看哪个片段的比例,这里的比例用于宽度和高度。
           - 参数enableBigRegion:如果启用它,您将获得2X或4X大小的通用件。 我喜欢这个,它可以减少动画的时间。 我建议只要启用它,如果视图足够大。
     **/
    func refactorWithNewFrame(destinationFrame: CGRect?, piecesRegion jumpRect: CGRect?, shiningColor: UIColor? = UIColor.clear, direction: SDERefactorDirection = .Horizontal, refactorTime animationTime: TimeInterval = 0.5, pieceRatio ratio: CGFloat = 0.05, enableBigRegion: Bool = false){

        guard let _ = self.superview else{
            return
        }

        if direction == .Custom{
            customRefactor()
            return
        }

        if destinationFrame != nil{
            self.translatesAutoresizingMaskIntoConstraints = false
            self.frame = destinationFrame!
        }

        let fromViewSnapshot = self.snapshotView(afterScreenUpdates: false)
        UIView.animate(withDuration: 0.3) { 
            self.alpha = 0

        }

        let origin = self.frame.origin
        let size = self.frame.size
        let pieceWidth: CGFloat = size.width * ratio
        let pieceHeight: CGFloat = size.height * ratio
        let (column, row) = columnAndrow(size: size, ratio: ratio)
        let delayDelta: Double = (direction == .Diagonal) ? animationTime / Double(column + row - 1) : animationTime / Double(column * row)
        let piecesRect = filterRect(jumpRect: jumpRect)

        var snapshots: [UIView] = []
        var ignoreIndexSet:Set<Int> = []
        var index = 0
        var delay: TimeInterval = 0
        var cleanTime: TimeInterval = 0

        for y in stride(from: 0, to: size.height, by: pieceHeight) {
            for x in stride(from: 0, to: size.width, by: pieceWidth){
                index += 1
                if ignoreIndexSet.contains(index){
                    continue
                }

                let indexOffset = ignoreIndexSet.reduce(0, {
                    delta, element in
                    let gap = element < index ? delta + 1 : delta
                    return gap
                })

                let (snapshotRegion, addedSet) = snapshotInfo(enableBigRegion: enableBigRegion, index: index, xy: (x, y), widthXheight: (pieceWidth, pieceHeight), columnXrow: (column, row))
                let initialFrame = randomRectFrom(sourceRect: piecesRect, regionSize: snapshotRegion.size)
                let finalFrame = CGRect(origin: CGPoint(x: (x + origin.x), y: (y + origin.y)), size: snapshotRegion.size)
                if addedSet.count > 0{
                    ignoreIndexSet.formUnion(addedSet)
                }

                let snapshot = fromViewSnapshot?.resizableSnapshotView(from: snapshotRegion, afterScreenUpdates: false, withCapInsets: .zero)
                self.superview!.addSubview(snapshot!)
                snapshots.append(snapshot!)
                snapshot?.frame = initialFrame
                snapshot?.alpha = 0.0

                switch direction{
                case .Horizontal:
                    let x = index % column == 0 ? column : index % column
                    let y = (index - x + column) / column
                    delay = delayDelta * Double(x * row + y - indexOffset)
                case .Vertical:
                    delay = delayDelta * Double(index - indexOffset)
                case .Diagonal:
                    delay = delayDelta * Double(diagonalIndexFor(index: index, columnXrow: (column, row)))
                case .Custom: break
                }

                let duration: TimeInterval = 0.2 + 0.1 * Double(UInt32(arc4random()) % UInt32(3))
                cleanTime = (delay + duration + 0.2 > cleanTime) ? delay + duration + 0.2 : cleanTime

                //UIView块动画比Core Animation在这方面表现更好。
                let randomScale =  CGFloat(UInt32(arc4random()) % UInt32(5)) / 10
                snapshot?.transform = CGAffineTransform.identity.translatedBy(x: randomScale, y: randomScale)
                UIView.animate(withDuration: 0.1, delay: delay, options: UIViewAnimationOptions.beginFromCurrentState, animations: {
                    snapshot?.alpha = 1
                    }, completion: { _ in

                        UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: {
                            snapshot?.transform = CGAffineTransform.identity
                            snapshot?.frame = finalFrame
                            }, completion: nil)
                })

                //添加重构动画在(抓拍,延迟时间:延迟,持续时间:持续时间,初步框架:初始帧,最后一帧:一帧)
                if shiningColor != nil{
                    addShiningAnimationOn(snapshot: snapshot!, delayTime: delay, shiningColor: shiningColor!)
                }

            }
        }
        //不能依赖于dispatch_after,这不能保证确切的执行时间
        self.perform(#selector(cleanUp(snapshots:)), with: snapshots, afterDelay: cleanTime)
    }

    //MARK: 毁坏
    func destruct(){
        destructWithDirection()
    }

    func destructAll(direction: String, animationTime: TimeInterval, ratio: CGFloat){
        var direc: SDERefactorDirection!
        switch direction {
        case "Horizontal":
            direc = SDERefactorDirection.Horizontal
        case "Vertical":
            direc = SDERefactorDirection.Vertical
        case "Diagonal":
            direc = SDERefactorDirection.Diagonal
        default:
            direc = SDERefactorDirection.Custom

        }
        destructWithDirection(direction: direc, animationTime: animationTime, pieceRatio: ratio)
    }

    /**
     它是消失动画的替代方案。 在视图中添加一个破坏动画,并从其超视图中删除它。

     - 参数方向:动画方向
     - 参数animationTime:动画时间
     - 参数比:查看比例。 它适用于宽度和高度两者。
     */
    func destructWithDirection(direction: SDERefactorDirection = .Diagonal, animationTime: TimeInterval = 0.5, pieceRatio ratio: CGFloat = 0.05){
        guard let _ = self.superview else{
            return
        }

        if direction == .Custom{
            //DIY
            return
        }

        let fromViewSnapshot = self.snapshotView(afterScreenUpdates: false)
        self.alpha = 0

        let origin = self.frame.origin
        let size = self.frame.size
        let pieceWidth = size.width * ratio
        let pieceHeight = size.height * ratio
        let (column, row) = columnAndrow(size: size, ratio: ratio)
        //Here 0.3 is the animation time of single piece
        let totalTime: TimeInterval = animationTime - 0.3
        let delayDelta: Double = (direction == .Diagonal) ? totalTime / Double(column + row - 1) : totalTime / Double(column * row)

        var snapshots: [UIView] = []
        var delay: TimeInterval = 0
        var windUpTime: TimeInterval = 0
        var index = 0
//CGFloat(0).stride(to: size.height, by: pieceHeight)
        for y in stride(from: 0, to: size.height, by: pieceHeight) {
            for x in stride(from: 0, to: size.width, by: pieceWidth){
                index += 1

                var regionWidth = pieceWidth
                var regionHeight = pieceHeight
                if x + regionWidth > size.width{
                    regionWidth = size.width - x
                }
                if y + regionHeight > size.height{
                    regionHeight = size.height - y
                }

                let snapshotRegion = CGRect(x: x, y: y, width: regionWidth, height: regionHeight)
                let snapshot = fromViewSnapshot?.resizableSnapshotView(from: snapshotRegion, afterScreenUpdates: false, withCapInsets: .zero)
                let snapshotFrame = CGRect(x: x + origin.x, y: y + origin.y, width: regionWidth, height: regionHeight)

                self.superview?.addSubview(snapshot!)
                snapshot?.frame = snapshotFrame
                snapshots.append(snapshot!)

                switch direction{
                case .Horizontal:
                    let x = index % column == 0 ? column : index % column
                    let y = (index - x + column) / column
                    delay = delayDelta * Double(x * row + y)
                case .Vertical:
                    delay = delayDelta * Double(index)
                case .Diagonal:
                    delay = delayDelta * Double(diagonalIndexFor(index: index, columnXrow: (column, row)))
                case .Custom: break
                }

                addAnnihilationAnimationaOn(snapshot: snapshot!, delayTime: delay)
                windUpTime = (delay + 0.3 > windUpTime) ? delay + 0.3 : windUpTime
            }
        }

        self.perform(#selector(windUp(snapshots:)), with: snapshots, afterDelay: windUpTime + 0.1)
    }


    //MARK: Private Helper
    private func columnAndrow(size: CGSize, ratio: CGFloat) -> (Int, Int){
        /*
        你不能依赖ceil,比如 Int(ceil(size.width / width)),有时候是以下结果是+1。
        */
        var rowCount = 0

        for _ in stride(from: 0, to: size.height, by: size.height * ratio){
            rowCount += 1
        }

        var columnCount = 0

        for _ in stride(from: 0, to: size.width, by: size.width * ratio){
            columnCount += 1
        }

        return (columnCount, rowCount)
    }

    private func diagonalIndexFor(index: Int, columnXrow:(Int, Int)) -> Int{

        let (column, _) = columnXrow
        let x = index % column == 0 ? column : index % column
        let y = (index - x + column) / column
        let DiagonalIndex = x + y - 1
        return DiagonalIndex
    }

    /*
     - 参数jumpRect:指定所有片段的区域
     - 返回:jumpRect与屏幕相交,如果不指定jumpRect,则为UIView的2X框。
    */
    private func filterRect(jumpRect: CGRect?) -> CGRect{
        var piecesRect = self.frame
        let screenRect = UIScreen.main.bounds
        if jumpRect != nil{
            if screenRect.contains(jumpRect!){
                piecesRect = jumpRect!
            }else{
                if !screenRect.intersection(jumpRect!).isNull{
                    piecesRect = screenRect.intersection(jumpRect!)
                }else{
                    let bigRect = self.frame.insetBy(dx: -self.frame.size.width/2, dy: -self.frame.size.height/2)
                    piecesRect = screenRect.intersection(bigRect)
                }
            }
        }else{
            let bigRect = self.frame.insetBy(dx: -self.frame.size.width/2, dy: -self.frame.size.height/2)
            piecesRect = screenRect.intersection(bigRect)
        }

        return piecesRect

    }

    private func snapshotInfo(enableBigRegion: Bool, index: Int, xy: (CGFloat, CGFloat), widthXheight: (CGFloat, CGFloat), columnXrow: (Int, Int)) -> (CGRect, Set<Int>){
        /*
        这个方法怎么办? 集成分区以减少总动画时间。
        _______       _______      ____      ____       _______      _______
        |__|__|  >>>  |     |  or  |__|  >>> |__|  or   |__|__|  >>> |_____|
        |__|__|  >>>  |_____|      |__|      |__|

        */
        let (pieceWidth, pieceHeight) = widthXheight

        let isBigRegion = enableBigRegion ? Int(UInt32(arc4random()) % UInt32(2)) == 1 : false
        var regionWidth = isBigRegion ? 2.0 * pieceWidth : pieceWidth
        var regionHeith = isBigRegion ? 2.0 * pieceHeight : pieceHeight

        let (regionX, regionY) = xy

        let size = self.frame.size
        if regionX + regionWidth >= size.width{
            regionWidth = size.width - regionX
        }
        if regionY + regionHeith >= size.height{
            regionHeith = size.height - regionY
        }

        let (column, _) = columnXrow
        var ignoreIndexSet: Set<Int> = []

        if isBigRegion {
            if regionX + pieceWidth < size.width{
                ignoreIndexSet.insert(index + 1)
            }

            if regionY + pieceHeight < size.height{
                ignoreIndexSet.insert(index + column)

                if regionX + pieceWidth < size.width{
                    ignoreIndexSet.insert(index + column + 1)
                }
            }
        }

        let regionOrigin: CGPoint = CGPoint(x: regionX, y: regionY)
        let regionSize: CGSize = CGSize(width: regionWidth, height: regionHeith)
        let snapshotRegion = CGRect(origin: regionOrigin, size: regionSize)

        return (snapshotRegion, ignoreIndexSet)
    }

    private func randomRectFrom(sourceRect: CGRect, regionSize: CGSize) -> CGRect {
        //现在的方法就像它的名字。
        let randomX: CGFloat = sourceRect.origin.x + CGFloat(UInt32(arc4random()) % UInt32(sourceRect.size.width))
        let randomY: CGFloat = sourceRect.origin.y + CGFloat(UInt32(arc4random()) % UInt32(sourceRect.size.height))

        let initialFrame = CGRect(x: randomX, y: randomY, width: regionSize.width, height: regionSize.height)

        return initialFrame
    }

    private func addRefactorAnimationOn(snapshot: UIView, delayTime: TimeInterval, duration: TimeInterval, initialFrame: CGRect, finalFrame: CGRect){
        let opaAni = CABasicAnimation(keyPath: "opacity")
        opaAni.fromValue = 0
        opaAni.toValue = 1
        opaAni.duration = 0.1
        opaAni.fillMode = kCAFillModeForwards
        opaAni.isRemovedOnCompletion = false
        opaAni.beginTime = CACurrentMediaTime() + delayTime
        snapshot.layer.add(opaAni, forKey: nil)

        let moveAni = CABasicAnimation(keyPath: "position")
        moveAni.fromValue = NSValue(cgPoint: initialFrame.centerPoint)
        moveAni.toValue = NSValue(cgPoint: finalFrame.centerPoint)
        moveAni.duration = duration
        moveAni.beginTime = CACurrentMediaTime() + delayTime
        moveAni.fillMode = kCAFillModeForwards
        moveAni.isRemovedOnCompletion = false
        snapshot.layer.add(moveAni, forKey: nil)

    }

    private func addShiningAnimationOn(snapshot: UIView, delayTime: TimeInterval, shiningColor: UIColor, shadowRadius: CGFloat = 10.0){
        snapshot.layer.shadowColor = shiningColor.cgColor
        snapshot.layer.shadowRadius = shadowRadius
        snapshot.layer.shadowPath = UIBezierPath(rect: snapshot.bounds).cgPath

        let opaKeyAni = CAKeyframeAnimation(keyPath: "shadowOpacity")
        opaKeyAni.values = [0.0, 0.1, 1.0, 0.8, 0.0]
        opaKeyAni.keyTimes = [0.0, 0.5, 0.7, 0.99, 1.0]
        opaKeyAni.duration = 0.3
        opaKeyAni.beginTime = CACurrentMediaTime() + delayTime

        snapshot.layer.add(opaKeyAni, forKey: nil)

    }

    private func addAnnihilationAnimationaOn(snapshot: UIView, delayTime: TimeInterval){
        let delta = CGFloat(UInt32(arc4random()) % UInt32(30))
        let end = CGPoint(x: snapshot.center.x - delta, y: snapshot.center.y - delta)

        UIView.animate(withDuration: 0.3, delay: delayTime, options: .curveEaseInOut, animations: {
            snapshot.alpha = 0
            snapshot.center = end
            }, completion: nil)
    }

    @objc private func cleanUp(snapshots: [UIView]){
        self.alpha = 1
        for snapshot in snapshots{
            snapshot.removeFromSuperview()
        }
    }

    @objc private func windUp(snapshots: [UIView]){
        self.removeFromSuperview()
        for snapshot in snapshots{
            snapshot.removeFromSuperview()
        }
    }
}

OC示例:UIViewController.m文件代码

#import "ArchivesViewController.h"
#import "SafeMedication-Swift.h"

@interface ArchivesViewController ()

@property(nonatomic,strong)UIView * testView;

@end

@implementation ArchivesViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor =[UIColor blackColor];

    self.testView =[[UIView alloc]initWithFrame:CGRectMake(self.view.frame.size.width/2-75, 250, 150, 150)];
    [self.view addSubview:self.testView];

    UIImageView * qrCodeImageView =[[UIImageView alloc]initWithFrame:CGRectMake(0, 0, self.testView.frame.size.width, self.testView.frame.size.height)];
    qrCodeImageView.image =[UIImage imageNamed:@"qrCode"];
    [self.testView addSubview:qrCodeImageView];

    UIButton * xiaoShiBtn =[[UIButton alloc]initWithFrame:CGRectMake(VS_Screen_Width/2-50, 500, 100, 30)];
    [xiaoShiBtn setTitle:@"重组" forState:UIControlStateNormal];
    [xiaoShiBtn setBackgroundColor:VS_ColerSky_Blue];
    [xiaoShiBtn addTarget:self action:@selector(xiaoShiBtnClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:xiaoShiBtn];
}

-(void)xiaoShiBtnClick:(UIButton *)btn{

    [self.testView refactor];
    [self.testView refactorAllWithDestinationFrame:CGRectMake(self.testView.frame.origin.x, self.testView.frame.origin.y, self.testView.frame.size.width, self.testView.frame.size.height) jumpRect:CGRectMake(250, 100, self.testView.frame.size.width, self.testView.frame.size.height) shiningColor:VS_ColerSky_Blue direction:@"Horizontal" animationTime:0.4f ratio:0.05f enableBigRegion:NO];

//    [self.testView destruct];
//    [self.testView destructAllWithDirection:@"Diagonal" animationTime:0.5f ratio:0.05f];
}

Swift演示:TestViewController.swift文件

import UIKit
@objc
class TestViewController: UIViewController {

    private lazy var createView: UIView = {
        let createView = UIView(frame: CGRect(x: 100, y: 300, width: 100, height: 100))
        createView.backgroundColor = UIColor.blue
        return createView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.white
        view.addSubview(createView)
        let but = UIButton(type: .system)
        but.frame = CGRect(x: 0, y: 110, width: 50, height: 30)
        but.setTitle("消失", for: .normal)
        but.addTarget(self, action: #selector(addd(sender:)), for: .touchUpInside)
        view.addSubview(but)

        let cutbut = UIButton(type: .system)
        cutbut.frame = CGRect(x: 100, y: 110, width: 50, height: 30)
        cutbut.setTitle("重组", for: .normal)

        cutbut.addTarget(self, action: #selector(cutaddd(sender:)), for: .touchUpInside)
        view.addSubview(cutbut)

    }

    func cutaddd(sender: UIButton){
        createView.refactor()
        createView.refactorWithNewFrame(destinationFrame: nil, piecesRegion: nil, shiningColor: nil, direction: .Horizontal, refactorTime: 0.6, pieceRatio: 0.05, enableBigRegion:false)

    }

    func addd(sender:UIButton) {
        createView.destruct()
        createView.destructWithDirection(direction: .Diagonal, animationTime: 0.5, pieceRatio: 0.05)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值