iOS实现3D卡片折叠效果

haoxubin 7年前
   <p><strong>介绍</strong></p>    <p>最近在开发的过程中需要用到3D卡片的特效。因此,研究了一下如何在iOS中使用透视图影。这边文章主要是学习iOS中的3D变幻,然后做一个卡片折叠效果,有点类似于笔记本电脑的折叠效果。在研究3D变换的时候,遇到了一些问题,在这里记录一下。</p>    <p><strong>实现效果</strong></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/cf9bccdfe85eea5ca3d190eaa252305e.gif"></p>    <p style="text-align:center">效果图.gif</p>    <p><strong>基础知识</strong></p>    <ul>     <li>UIView与CALayer</li>    </ul>    <p>每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。一个 CALayer 的 frame 是由它的 anchorPoint, position, bounds, transform 共同决定的,而一个 View 的 frame 只是简单的返回 CALayer的 frame,同样 UIView 的 center和 bounds 也是返回 Layer 的一些属性。</p>    <ul>     <li>CALayer属性介绍</li>    </ul>    <p>UIView有frame、bounds、center三个属性,CALayer也有类似的属性,分别为frame、bounds、position、anchorPoint。frame 和 bounds比较好理解,bounds可以视为x坐标和y坐标都为0的frame,这里主要是学习position、anchorPoint 两个属性。</p>    <pre>  <code class="language-objectivec">@property CGPoint position;  @property CGPoint anchorPoint;</code></pre>    <p>position是layer中的anchorPoint在superLayer中的位置坐标,关系如下图所示:</p>    <p>positon为(100,100),anchorPoint为(0.0,0.0)</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ca111fd4a5f6485e8d1aeb6249861e6c.png"></p>    <p>positon为(100,100),anchorPoint为(0.5,0.5)</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8b72c5f12578e64997d96560393f27bf.png"></p>    <p>positon为(100,100),anchorPoint为(1.0,1.0)</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d4a7ba18cd96363cb3444faa2684a216.png"></p>    <p>frame、position与anchorPoint有以下关系:</p>    <pre>  <code class="language-objectivec">frame.origin.x = position.x - anchorPoint.x * bounds.size.width;  frame.origin.y = position.y - anchorPoint.y * bounds.size.height;</code></pre>    <ul>     <li>CALayer透视投影变换</li>    </ul>    <p>CALayer默认使用正交投影,因此没有远小近大效果,而且没有明确的API可以使用透视投影矩阵,但是我们可以通过矩阵连乘自己构造透视投影矩阵。</p>    <p>CATransform3D 的透视效果通过一个矩阵中一个很简单的元素来控制 <strong> <em>m34</em> </strong></p>    <p>。 <strong> <em>m34</em> </strong> 用于按比例缩放X和Y的值来计算到底要离视角多远。 <strong> <em>m34</em> </strong> 的默认值是0,我们可以通过设置 <strong> <em>m34</em> </strong> 为 <strong> <em>-1.0 / disZ</em> </strong> 来应用透视效果, <strong> <em>disZ</em> </strong> 代表视角相机和屏幕之间的距离,以像素为单位。</p>    <pre>  <code class="language-objectivec">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));  }</code></pre>    <p><strong>实现过程</strong></p>    <p>1、新建一个QMView的试图,初始化的时候为它添加上下两个试图。这里我们设置topview视图的锚点为 CGPointMake(0.5, 0.0),是因为我们希望topview视图绕着顶边旋转。我们设置bottomView视图的锚点为 CGPointMake(0.5, 1.0),是因为我们希望bottomView视图绕着底边旋转。锚点的x坐标设置相同,我们是希望上下两个视图水平方向的变换规律相同。 <strong> <em>CALayer仿射变换的时候,是根据锚点进行相关变换的</em> </strong> 。</p>    <pre>  <code class="language-objectivec">- (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;  }</code></pre>    <p>2、将位置偏移量转化为角度,每个layer最大旋转角度为90度。</p>    <pre>  <code class="language-objectivec">CGFloat thelta = M_PI/2*(_offset/(_originFrame.size.height/2));</code></pre>    <p>3、根据角度构建绕x轴(1.0,0.0,0.0)旋转的矩阵。这里注意两个视图旋转的方向不同。</p>    <pre>  <code class="language-objectivec">CATransform3D transform = CATransform3DMakeRotation(-thelta, 1, 0, 0);  CATransform3D transform1 = CATransform3DMakeRotation(thelta, 1, 0, 0);</code></pre>    <p>4、构建透视投影矩阵,并应用在两个UIView的CALayer上。</p>    <pre>  <code class="language-objectivec">_topView.layer.transform = CATransform3DPerspect(transform, CGPointZero, kDistanceZ);  _bottomView.layer.transform = CATransform3DPerspect(transform1, CGPointZero, kDistanceZ);</code></pre>    <p>5、变换后位置的修正。CALayer进行仿射变换之后,它的宽高会发生变化。我们底部视图需要调整位置,才能连接到顶部视图底部。因此,我们需要重设底部视图的position。我们可以先得到变换后的顶部试图的高度,这个高度也就是底部视图的顶部。然后根据 <em>position.y = frame.origin.y + anchorPoint.y * bounds.size.height</em> 计算出顶部视图的position。最后修正整个QMView的高度。</p>    <pre>  <code class="language-objectivec">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;</code></pre>    <p>整个变换部分的代码:</p>    <pre>  <code class="language-objectivec">- (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;  }</code></pre>    <p><strong>探索过程</strong></p>    <p>1、旋转的过程中不修正底部视图的位置,则会出现如下效果。:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/86c113f38cf578e149e60b22926eeca8.gif"></p>    <p>2、修正坐标的时候,我们修正bottomView的frame,而不是其layer的position。</p>    <pre>  <code class="language-objectivec">- (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;  }</code></pre>    <p>会发生一下神奇的效果:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d0c44cea203658db54f7240c72a5797c.gif"></p>    <p>如果在透视变换的过程中,修改了UIView的frame,会对之后的变换产生影响,也就是说之后的变换是在此基础上变换的,这也是bottomView为什么会越来越小。之前想过每次变换前还原bottomView的frame,但是在实际做的过程中比较麻烦。</p>    <p>3、利用GLKit进行变换。根据3D透视投影的相关概念,在探索的时候就想到了用矩阵直接变换。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/24d9f94e11d8cda1ea263ae256e76f0a.png"></p>    <p style="text-align:center">变换公式.png</p>    <p>由于GLKit有相关的变换函数,在此我将CATransform3D转换为GLKMatrix4,然后利用函数 GLKMatrix4MultiplyVector4 进行变换。</p>    <pre>  <code class="language-objectivec">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;  }</code></pre>    <p>详细变换如下所示:</p>    <pre>  <code class="language-objectivec">- (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);  }</code></pre>    <p>结果还是有很大的误差,如下图所示:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/f07609506d1f10f12ee1d2d939b6cb4a.png"></p>    <p><strong>参考链接</strong></p>    <p><a href="/misc/goto?guid=4959747389647250484" rel="nofollow,noindex">http://www.cocoachina.com/industry/20121126/5178.htmls</a></p>    <p> </p>    <p>来自:http://www.jianshu.com/p/9a731a8c9e50</p>    <p> </p>