iOS实现3D卡片折叠效果

介绍

最近在开发的过程中需要用到3D卡片的特效。因此,研究了一下如何在iOS中使用透视图影。这边文章主要是学习iOS中的3D变幻,然后做一个卡片折叠效果,有点类似于笔记本电脑的折叠效果。在研究3D变换的时候,遇到了一些问题,在这里记录一下。

实现效果

效果图.gif

基础知识

  • UIView与CALayer

每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。一个 CALayer 的 frame 是由它的 anchorPoint, position, bounds, transform 共同决定的,而一个 View 的 frame 只是简单的返回 CALayer的 frame,同样 UIView 的 center和 bounds 也是返回 Layer 的一些属性。

  • CALayer属性介绍

UIView有frame、bounds、center三个属性,CALayer也有类似的属性,分别为frame、bounds、position、anchorPoint。frame 和 bounds比较好理解,bounds可以视为x坐标和y坐标都为0的frame,这里主要是学习position、anchorPoint 两个属性。

@property CGPoint position;
@property CGPoint anchorPoint;

position是layer中的anchorPoint在superLayer中的位置坐标,关系如下图所示:

positon为(100,100),anchorPoint为(0.0,0.0)

图片

positon为(100,100),anchorPoint为(0.5,0.5)

图片

positon为(100,100),anchorPoint为(1.0,1.0)

图片

frame、position与anchorPoint有以下关系:

frame.origin.x = position.x - anchorPoint.x * bounds.size.width;
frame.origin.y = position.y - anchorPoint.y * bounds.size.height;
  • CALayer透视投影变换

CALayer默认使用正交投影,因此没有远小近大效果,而且没有明确的API可以使用透视投影矩阵,但是我们可以通过矩阵连乘自己构造透视投影矩阵。
CATransform3D 的透视效果通过一个矩阵中一个很简单的元素来控制m34
m34 用于按比例缩放X和Y的值来计算到底要离视角多远。m34 的默认值是0,我们可以通过设置 m34-1.0 / disZ 来应用透视效果,disZ 代表视角相机和屏幕之间的距离,以像素为单位。

CATransform3D CATransform3DMakePerspective(CGPoint center, float disZ)
{
    CATransform3D transToCenter = CATransform3DMakeTranslation(-center.x, -center.y, 0);
    CATransform3D transBack = CATransform3DMakeTranslation(center.x, center.y, 0);
    CATransform3D scale = CATransform3DIdentity;
    scale.m34 = -1.0f/disZ;
    return CATransform3DConcat(CATransform3DConcat(transToCenter, scale), transBack);
}

CATransform3D CATransform3DPerspect(CATransform3D t, CGPoint center, float disZ)
{
    return CATransform3DConcat(t, CATransform3DMakePerspective(center, disZ));
}

实现过程

1、新建一个QMView的试图,初始化的时候为它添加上下两个试图。这里我们设置topview视图的锚点为 CGPointMake(0.5, 0.0),是因为我们希望topview视图绕着顶边旋转。我们设置bottomView视图的锚点为 CGPointMake(0.5, 1.0),是因为我们希望bottomView视图绕着底边旋转。锚点的x坐标设置相同,我们是希望上下两个视图水平方向的变换规律相同。CALayer仿射变换的时候,是根据锚点进行相关变换的

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        // [self setBackgroundColor:[UIColor greenColor]];
        _originFrame = frame;
        
        // 上部试图
        _topView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height/2)];
        _topView.layer.anchorPoint = CGPointMake(0.5, 0.0);
        _topView.layer.position = CGPointMake(frame.size.width/2, 0);
        _topView.backgroundColor = [UIColor orangeColor];
        
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
        label.text = @"CALayer默认使用正交投影,因此没有远小近大效果,而且没有明确的API可以使用透视投影矩阵,但是我们可以通过矩阵连乘自己构造透视投影矩阵。CATransform3D的透视效果通过一个矩阵中一个很简单的元素来控制";
        label.numberOfLines = 0;
        [_topView addSubview:label];
    
        // 底部视图
        _bottomView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height/2)];
        _bottomView.layer.anchorPoint = CGPointMake(0.5, 1.0);
        _bottomView.layer.position = CGPointMake(frame.size.width/2, frame.size.height);
        _bottomView.backgroundColor = [UIColor blueColor];
        
        UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
        label1.text = @"CALayer默认使用正交投影,因此没有远小近大效果,而且没有明确的API可以使用透视投影矩阵,但是我们可以通过矩阵连乘自己构造透视投影矩阵。CATransform3D的透视效果通过一个矩阵中一个很简单的元素来控制";
        label1.numberOfLines = 0;
        [_bottomView addSubview:label1];
        
        [self addSubview:_bottomView];
        [self addSubview:_topView];
    }
    
    return self;
}

2、将位置偏移量转化为角度,每个layer最大旋转角度为90度。

CGFloat thelta = M_PI/2*(_offset/(_originFrame.size.height/2));

3、根据角度构建绕x轴(1.0,0.0,0.0)旋转的矩阵。这里注意两个视图旋转的方向不同。

CATransform3D transform = CATransform3DMakeRotation(-thelta, 1, 0, 0);
CATransform3D transform1 = CATransform3DMakeRotation(thelta, 1, 0, 0);

4、构建透视投影矩阵,并应用在两个UIView的CALayer上。

_topView.layer.transform = CATransform3DPerspect(transform, CGPointZero, kDistanceZ);
_bottomView.layer.transform = CATransform3DPerspect(transform1, CGPointZero, kDistanceZ);

5、变换后位置的修正。CALayer进行仿射变换之后,它的宽高会发生变化。我们底部视图需要调整位置,才能连接到顶部视图底部。因此,我们需要重设底部视图的position。我们可以先得到变换后的顶部试图的高度,这个高度也就是底部视图的顶部。然后根据 position.y = frame.origin.y + anchorPoint.y * bounds.size.height 计算出顶部视图的position。最后修正整个QMView的高度。

CGFloat position = _topView.layer.frame.size.height + 1.0 * _bottomView.layer.frame.size.height;
_bottomView.layer.position = CGPointMake(_originFrame.size.width/2, position);

CGRect rect = self.frame;
rect.size.height = _topView.layer.frame.size.height + _bottomView.layer.frame.size.height;
self.frame = rect;

整个变换部分的代码:

- (void)setOffset:(CGFloat)offset
{
    if (offset < 0 || offset > _originFrame.size.height/2) {
        return;
    }

    _offset = offset;
    CGFloat thelta = M_PI/2*(_offset/(_originFrame.size.height/2));
    CATransform3D transform = CATransform3DMakeRotation(-thelta, 1, 0, 0);
    CATransform3D transform1 = CATransform3DMakeRotation(thelta, 1, 0, 0);

    _topView.layer.transform = CATransform3DPerspect(transform, CGPointZero, kDistanceZ);
    _bottomView.layer.transform = CATransform3DPerspect(transform1, CGPointZero, kDistanceZ);

    //position.x = frame.origin.x + anchorPoint.x * bounds.size.width;
    //position.y = frame.origin.y + anchorPoint.y * bounds.size.height;
    CGFloat position = _topView.layer.frame.size.height + 1.0 * _bottomView.layer.frame.size.height;
    _bottomView.layer.position = CGPointMake(_originFrame.size.width/2, position);

    CGRect rect = self.frame;
    rect.size.height = _topView.layer.frame.size.height + _bottomView.layer.frame.size.height;
    self.frame = rect;
}

探索过程

1、旋转的过程中不修正底部视图的位置,则会出现如下效果。:

未命名.gif

2、修正坐标的时候,我们修正bottomView的frame,而不是其layer的position。

- (void)setOffset:(CGFloat)offset
{
    if (offset < 0 || offset > _originFrame.size.height/2) {
        return;
    }
    
    _offset = offset;
    CGFloat thelta = M_PI/2*(_offset/(_originFrame.size.height/2));
    CATransform3D transform = CATransform3DMakeRotation(-thelta, 1, 0, 0);
    CATransform3D transform1 = CATransform3DMakeRotation(thelta, 1, 0, 0);
    
    _topView.layer.transform = CATransform3DPerspect(transform, CGPointZero, kDistanceZ);
    _bottomView.layer.transform = CATransform3DPerspect(transform1, CGPointZero, kDistanceZ);
    
    
    CGRect rect = _bottomView.frame;
    rect.origin.y = _topView.frame.size.height;
    _bottomView.frame = rect;
}

会发生一下神奇的效果:

未命名.gif

如果在透视变换的过程中,修改了UIView的frame,会对之后的变换产生影响,也就是说之后的变换是在此基础上变换的,这也是bottomView为什么会越来越小。之前想过每次变换前还原bottomView的frame,但是在实际做的过程中比较麻烦。

3、利用GLKit进行变换。根据3D透视投影的相关概念,在探索的时候就想到了用矩阵直接变换。

变换公式.png

由于GLKit有相关的变换函数,在此我将CATransform3D转换为GLKMatrix4,然后利用函数 GLKMatrix4MultiplyVector4 进行变换。

GLKVector4 transform3DMultiplyVector4(CATransform3D transform, GLKVector4 vec4)
{
    GLKMatrix4 matrix = GLKMatrix4Make(transform.m11, transform.m12, transform.m13, transform.m14,
                                       transform.m21, transform.m22, transform.m23, transform.m24,
                                       transform.m31, transform.m32, transform.m33, transform.m34,
                                       transform.m41, transform.m42, transform.m43, transform.m44);

    GLKVector4 transVec4 = GLKMatrix4MultiplyVector4(matrix, vec4);
    return transVec4;
}

详细变换如下所示:

- (void)setOffset:(CGFloat)offset
{
    if (offset < 0 || offset > _originFrame.size.height/2) {
        return;
    }
   
    _offset = offset;
    CGFloat thelta = M_PI/2*(_offset/(_originFrame.size.height/2));
    CATransform3D transform = CATransform3DMakeRotation(-thelta, 1, 0, 0);
    CATransform3D transform1 = CATransform3DMakeRotation(thelta, 1, 0, 0);
    
    // 初始化高度 - 矩阵变换后的高度
    GLKVector4 top1 = transform3DMultiplyVector4(transform, GLKVector4Make(_originFrame.size.width/2, 0, 0, 1));
    GLKVector4 top2 =transform3DMultiplyVector4(transform, GLKVector4Make(_originFrame.size.width/2, _originFrame.size.height/2, 0, 1));
    
    GLKVector4 bottom1 = transform3DMultiplyVector4(transform1, GLKVector4Make(_originFrame.size.width/2, _originFrame.size.height/2, 0, 1));
    GLKVector4 bottom2 =transform3DMultiplyVector4(transform1, GLKVector4Make(_originFrame.size.width/2, _originFrame.size.height, 0, 1));
    
    _topView.layer.transform = CATransform3DPerspect(transform, CGPointZero, kDistanceZ);
    _bottomView.layer.transform = CATransform3DPerspect(transform1, CGPointZero, kDistanceZ);
    
    NSLog(@"1> %f %f", top2.y - top1.y, top1.y);
    NSLog(@"2> %f", _topView.layer.frame.size.height);
    NSLog(@"3> %f", bottom2.y - bottom1.y);
    
    //position.y = frame.origin.y + anchorPoint.y * bounds.size.height;
    CGFloat position = (top2.y - top1.y) + 1.0 * (bottom2.y - bottom1.y);
    _bottomView.layer.position = CGPointMake(_originFrame.size.width/2, position);
}

结果还是有很大的误差,如下图所示:

效果图.png

参考链接

http://www.cocoachina.com/industry/20121126/5178.htmls

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容