iOS實現3D卡片折疊效果

haoxubin 7年前發布 | 13K 次閱讀 iOS開發 移動開發

介紹

最近在開發的過程中需要用到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、旋轉的過程中不修正底部視圖的位置,則會出現如下效果。:

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;
}

會發生一下神奇的效果:

如果在透視變換的過程中,修改了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);
}

結果還是有很大的誤差,如下圖所示:

參考鏈接

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

 

來自:http://www.jianshu.com/p/9a731a8c9e50

 

 本文由用戶 haoxubin 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!