CoreAnimation中的mask与m34

朝花夕拾,再次重温CoreAnimation中两个重要的属性:maskm34,记得刚开始学习iOS开发那会,曾经在这两个属性上面折腾了好长一段时间,再次重温,感触颇多。感觉google一直是开发最好的老师,全栈工程师往往只是一个神话。闲话不多说,进入正题吧。

mask蒙版

先来看一段SDK中的介绍:

The layer’s alpha channel determines how much of the layer’s content and background shows through. Fully or partially opaque pixels allow the underlying content to show through but fully transparent pixels block that content.

翻译一下:
蒙版layer的alpha通道决定了当前layer的显示内容与背景。当前layer的显示区域为蒙版layer的alpha值为1或者大于0的区域,而蒙版layer的alpha为0区域会被裁剪掉。


说了这么多,到底什么是蒙版呢?
让我们看张效果图:
Image
原图

Image
蒙版

从上面可以看出来蒙版,其实就是另一个layer用来做裁剪用的。但是这里要注意的是,蒙版的alpha值越大,则显示的越清晰,当alpha值为0时就完成被裁剪掉了。所以如果配上阴影的话,其阴影部分也是会显示出来的。

那mask都有哪些经典的应用呢?
像刚才一样,在聊天软件中,如果想要做点特别的效果就可以用的上,上图的效果其实就是用BezierPath来画聊天窗口的箭头,附上上面效果图的源码

        let maskPath = UIBezierPath()
        maskPath.moveToPoint(CGPointMake(40, 220))
        maskPath.addQuadCurveToPoint(CGPointMake(20, 200), controlPoint: CGPointMake(20, 220))
        maskPath.addLineToPoint(CGPointMake(20, 40))
        maskPath.addQuadCurveToPoint(CGPointMake(40, 20), controlPoint: CGPointMake(20, 20))
        maskPath.addLineToPoint(CGPointMake(280, 20))
        maskPath.addQuadCurveToPoint(CGPointMake(300, 40), controlPoint: CGPointMake(300, 20))
        maskPath.addLineToPoint(CGPointMake(300, 200))
        maskPath.addQuadCurveToPoint(CGPointMake(280, 220), controlPoint: CGPointMake(300, 220))
        maskPath.addLineToPoint(CGPointMake(40, 220))
        maskPath.moveToPoint(CGPointMake(10, 220))
        maskPath.addQuadCurveToPoint(CGPointMake(30, 180), controlPoint: CGPointMake(30, 220))
        maskPath.addLineToPoint(CGPointMake(40, 210))
        maskPath.addQuadCurveToPoint(CGPointMake(10, 220), controlPoint: CGPointMake(40, 220))

        let maskLayer:CAShapeLayer = CAShapeLayer()

        maskLayer.fillColor = UIColor.whiteColor().CGColor
        maskLayer.path = maskPath.CGPath


        bgImageView?.layer.mask = maskLayer

除此之外还有一些更多经典的应用,例如我们可以利用蒙版来做卡拉OK歌词的展示,先上一张效果图
Image

源码如下:

import UIKit

class MYLRCView: UIView {

    var line:UILabel?
    var highlightLine:UILabel?
    var maskLine:UIView?


    override init(frame: CGRect) {
        super.init(frame: frame)
        initUI()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initUI()
    }

    func initUI(){
        line = UILabel(frame: self.bounds)
        line?.text = "如果那两个字没有颤抖"
        line?.textColor = UIColor.whiteColor()
        addSubview(line!)

        highlightLine = UILabel(frame: self.bounds)
        highlightLine?.text = line?.text
        highlightLine?.textColor = UIColor.purpleColor()
        addSubview(highlightLine!)

        maskLine = UIView()
        maskLine?.backgroundColor = UIColor.blackColor()

        highlightLine?.layer.mask = maskLine?.layer

        for i:Int in 0..<10{
            let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(7*Double(i) * Double(NSEC_PER_SEC)))
            dispatch_after(delayTime, dispatch_get_main_queue()) { () -> Void in
                self.readLine()
            }
        }


    }

    func readLine(){
        maskLine?.frame = CGRectMake(0, 0, 0, self.bounds.size.height)
        let totalArray:[Double] = [12.0,19.0,44.0,43.0,32.0,44.0,24.0,38.0,25.0,19.0]

        var total:Double = 0.0
        for i in totalArray{
            total += i
        }
        UIView.animateKeyframesWithDuration(3.3, delay: 0, options: UIViewKeyframeAnimationOptions.CalculationModeLinear, animations: { () -> Void in

            var offset:Double = 0.0
            for(var i:Int = 0; i < totalArray.count ; i++) {
                let consoum = totalArray[i]/total
                print(consoum)
                UIView.addKeyframeWithRelativeStartTime(offset, relativeDuration:consoum , animations: { () -> Void in
                    maskLine?.frame = CGRectMake(0, 0, self.bounds.size.width*CGFloat(i+1)/10.0, self.bounds.size.height)
                })

                offset += consoum

                print(offset)
            }

            }) { (finish) -> Void in

        }
    }

}

备注:
其实卡拉OK字幕动画的原理很简单,底部有一个white颜色字体的歌词Label,上面一个purple颜色字体的歌词Label.再为purple颜色字体的歌词Label加上一个蒙版layer,一开始蒙版layer的frame为CGRectZero,,然后通过动画修改蒙版layer的frame,逐渐覆盖整个歌词Label.

真实的卡拉OK字幕功能会比这个更加复杂,卡拉OK所使用的歌词格式通常不是LRC,而是KSC或者其它格式。其中会涉及到分词,歌词信息解析,利用iOS7的UIViewKeyframeAnimation或者直接使用CoreAnimation中的CAKeyframeAnimation来实现动画。
我上面的例子中没有做歌词的解析,直接抽取了歌词的一段内容做了展示,也展示出了歌词抑扬顿挫的情绪。

除此之外呢,FaceBook一年前开源的Shimmer,其源码中蒙版为一个渐变Layer,使用平移动画来实现的,动态高亮效果。

此外还有一些开源组件也使用了这一特性,如DGRunkeeperSwitch,不过我体验了一下这个组件,里面有一些bug,就是在拖动的时候,有时候Segment会卡在中间。


m34

先上效果图

备注:
m34其实是CATransform3D中的一个矩阵属性,m34 = -1/z,其中z为观察点在z轴上的值,而layer的z轴的位置则是通过anchorPoint来指定的.关于什么是anchorPoint, 这里就不展开讨论了。

其实这个Demo是我是些年做过的一个产品的雏形,当时是为知兵堂做的一个外包特性效果,实际效果是里面的数字为一些二战老照片,点击老照片,可以查看大图,在大图模式还可以就近查看上下左右的图片。
其实我理解的就是一个上墙的效果。
其实现的原理就是通过CATransform3D里面的m34属性来实现一个3D视差效果,达到3D上墙的效果,我在demo中使用的核心代码为:

var transform = CATransform3DIdentity;
transform.m34 = 1.0 / -800.0;
transform = CATransform3DRotate(transform, currentRan*1.2, 0, 1, 0)
onWallCollection!.layer.transform = transform

这里m34的分母越大,其透视距离就越远,看到的内容就越小。

附上源码

import UIKit

class ViewController: UIViewController,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {

    var onWallCollection:UICollectionView?
    var lastOffset:CGFloat = 0
    var currentRan:CGFloat = 0
    var timer:CADisplayLink?
    var isLeft:Bool = true

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.purpleColor()
        // Do any additional setup after loading the view, typically from a nib.
        let flow = UICollectionViewFlowLayout()
        flow.scrollDirection = .Horizontal
        flow.itemSize = CGSizeMake(80, 120)
        flow.minimumLineSpacing = 30
        flow.headerReferenceSize = CGSizeMake(self.view.bounds.size.width/2.0, self.view.bounds.size.height)
        flow.footerReferenceSize = CGSizeMake(self.view.bounds.size.width/2.0, self.view.bounds.size.height)

        onWallCollection = UICollectionView(frame: CGRectMake(-self.view.bounds.size.width/2.0, 0, self.view.bounds.size.width*2, self.view.bounds.size.height), collectionViewLayout:flow)
        onWallCollection?.registerClass(PhotoWall.self, forCellWithReuseIdentifier: "cell")
        onWallCollection?.dataSource = self
        onWallCollection?.delegate = self
        self.view.addSubview(onWallCollection!)

        timer = CADisplayLink(target: self, selector: "goBack:")
        timer?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
        timer?.paused = true

    }

    deinit{
        timer?.invalidate()
    }

    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int{
        return 120
    }

    func collectionView(collectionView: UICollectionView,
        layout collectionViewLayout: UICollectionViewLayout,
        insetForSectionAtIndex section: Int) -> UIEdgeInsets{
            return UIEdgeInsetsMake(20, 20, 20, 20)
    }

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell{
        let cell:PhotoWall = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath:indexPath) as! PhotoWall
        cell.index = indexPath.row
        return cell
    }

    func adjustUI(){
        var transform = CATransform3DIdentity;
        transform.m34 = 1.0 / -800.0;
        transform = CATransform3DRotate(transform, currentRan*1.2, 0, 1, 0)
        onWallCollection!.layer.transform = transform
    }

    func goBack(link:CADisplayLink){
        let step:CGFloat = 0.02
        if currentRan < 0{
            currentRan += step
            if currentRan >= 0{
                currentRan = 0
            }
        }
        else if currentRan > 0{
            currentRan -= step
            if currentRan <= 0{
                currentRan = 0
            }
        }

        if currentRan == 0{
            timer?.paused = true
        }

        adjustUI()

    }

    func scrollViewWillBeginDragging(scrollView: UIScrollView) {
        timer?.paused = true
        lastOffset = scrollView.contentOffset.x
    }

    func scrollViewDidScroll(scrollView: UIScrollView) {

        if scrollView.dragging {
            if scrollView.contentOffset.x < lastOffset && isLeft{
                isLeft = false
            }
            else if scrollView.contentOffset.x > lastOffset && isLeft == false{
                isLeft = true
            }

            let step:CGFloat = 0.02

            if isLeft{
                currentRan += step
            }
            else
            {
                currentRan -= step
            }

            if currentRan < -1{
                currentRan = -1
            }
            else if currentRan > 1{
                currentRan = 1
            }

            adjustUI()
            lastOffset = scrollView.contentOffset.x
        }



    }

    func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if decelerate == false{
            timer?.paused = false
        }
    }

    func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
        timer?.paused = false
    }



}
import UIKit

class PhotoWall: UICollectionViewCell {

    var label:UILabel?

    var index: Int = 0{
        didSet {label?.text = "\(index)"}
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        initUI()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initUI()
    }

    func initUI(){
        self.backgroundColor = UIColor.lightGrayColor()

        label = UILabel(frame: self.bounds)
        label?.textColor = UIColor.purpleColor()
        label?.textAlignment = .Center
        addSubview(label!)
    }
}

Demo延伸:
事实这个效果可以作为照片上墙的效果,点击里面的数字内容区域可以拉近查看距离,与查看模式。开启一种全新的浏览体验。

参考阅读

2015-09-21 22:29181