tvOS視差按鈕的ObjC實現

圖片描述

介紹

蘋果在最新發布的Apple TV裏引入了有趣的圖標設計
具體說來 圖標由2-5個分層圖層構成 在圖標被選中的時候 圖標內每一個圖層進行不一樣幅度的位移 從而造成視覺上具備深度距離感的視差效果 圖標構成和效果能夠見視頻: git

視頻連接 github

這種效果很適合用於多媒體類應用 例如圖書或者電影封面 讓封面變得立體生動 然而這種效果目前只能在Apple TV的tvOS裏見到
因此 如何在iOS上作出一樣的效果呢?如今就讓咱們一塊兒研究下視差按鈕的實現原理 而且本身實現一個吧 ^_^數組

原理

假設咱們已經有了如下圖片:(你能夠從下載連接下載已經分層的四張圖片)
四張已經分層的圖片
基於這四張圖片 咱們該怎麼對其進行變換來達到tvOS的視差效果呢?
從新觀察上文中蘋果官方的例子視頻 咱們能夠得出如下結論:app

1. 總圖層旋轉

總圖層在旋轉 但不一樣於通常在屏幕平面上的旋轉 而是相對屏幕具備必定夾角的旋轉
若是不太瞭解這種旋轉是怎麼發生的 咱們能夠看一張有關 CATransform3D 的圖:
CATransform3DMakeRotation的XYZ軸
咱們經常使用的 CGRect 有 X 和 Y 兩個位置參數 而 CATransform3D 能夠理解爲在平常的兩個軸之外加了 Z 軸 方向爲從手機上表面豎直向下 如圖
這麼一來 咱們平常所見的在屏幕平面上的旋轉(好比屏幕旋轉)實際上是繞 Z 軸旋轉
而繞 X 和 Y 軸的旋轉 即是讓 CALayer 具備相對屏幕具備必定夾角的旋轉 具體表現就是如同tvOS按鈕同樣有遠近效果(其實在透視效果上仍是有些不同 後面會提怎麼解決)
CATransform3DMakeRotation 就是這麼經過旋轉角度定義的:ide

CATransform3D CATransform3DMakeRotation (
   CGFloat angle, //繞着向量(x,y,z)旋轉的角度
   CGFloat x, //x軸份量
   CGFloat y, //y軸份量
   CGFloat z //z軸份量
);

2. 分圖層移動

除去旋轉 每一個圖層都在進行不一樣半徑的圓周移動
爲何有移動?
透視是創造三維深度感受的關鍵 而透視效果最直白的話來講 就是「近大遠小」
讓咱們來看個例子吧 :-)
近大遠小的說明
若是隻有總圖層自轉 分圖層不進行移動 那麼整個按鈕雖然有自轉效果 可是看起來仍是平的
若是要保證有三維效果 就要有視差 即近大遠小 讓遠處的圖層移動的距離很小 近處的圖層移動距離很大(你們能夠自行想象一樣速度遠處近處的汽車 看起來移動的距離也不同)
所以 就要令分圖層進行圓周移動 離咱們近的圖層 圓周半徑要更大些 保證看起來移動的距離更大
咱們簡單地用 Principle 作了一個原型 大致效果應該是這個樣子的 中間的圓點是移動的軸心
簡陋的圓周移動效果動畫

3.總圖層移動

總的圖層也會移動
看到咱們剛纔那個簡陋的效果了沒?你有可能會想 爲何看起來和tvOS差異那麼大?
緣由是 tvOS實現的效果 整個按鈕並無明顯地移動 而是近似於固定在某個位置的 這麼一來 就要求咱們在分圖層移動的時候 總圖層疊加一項反方向的移動 保證按鈕固定住
因而咱們又用 Principle 作了一個原型 大致效果應該是這個樣子的
仍是很簡陋的圓周移動效果ui

4.高光移動

高光的移動方向剛好相反
高光就是咱們在tvOS的圖標上看到的白色反光 這個部分其實很簡單:
用PS畫一個白色的圓 加上模糊效果 就是一個 高光圖層
讓圖層在移動的時候於其餘圖層方向相反 即讓圖層疊加以後的效果爲 高光永遠在離咱們最近的地方 這裏提及來會有點困惑 可是用代碼實現的時候就天然明白了 ^_^atom

實現

注:實現部分限於篇幅 不可能將全部代碼都粘貼出來 只是在幾個關鍵的地方粘貼出來加以說明
完整代碼見 https://github.com/JustinFincher/JZtvOSParallaxButtonspa

1.按鈕基本

按照咱們的計劃 這個按鈕默認並無三維效果 就是不少UIImage疊加起來 只有當咱們長按的時候 纔會有各類旋轉和移動
這裏動畫方式分爲兩種 第一種是自動動畫 首先會移動和旋轉到一個特定的角度 而後便開始週期移動了 第二種是手動動畫 按鈕會跟隨手指Drag進行旋轉和移動
讓咱們先新建一個UIButton吧 :-)設計

//JZParallaxButton.h
# import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger, RotateMethodType)
{
    RotateMethodTypeAutoRotate = 0, //自動動畫
    RotateMethodTypeWithFinger, //跟隨手指
    RotateMethodTypeWithFingerReverse, //跟隨手指 但反向
};
@interface JZParallaxButton : UIButton
@end
//JZParallaxButton.m
# define LongPressInterval 0.5 //自動動畫狀態下的長按判斷時間
@interface UIButton ()<UIGestureRecognizerDelegate>
@end
@implementation JZParallaxButton
@end

寫一個本身的init方法 而後在裏面加入長按的手勢判斷

//JZParallaxButton.m
- (instancetype)initButtonWithCGRect:(CGRect)RectInfo
                      WithLayerArray:(NSMutableArray *)ArrayOfLayer
              WithRoundCornerEnabled:(BOOL)isRoundCorner
           WithCornerRadiusifEnabled:(CGFloat)Radius
                  WithRotationFrames:(int)Frames
                WithRotationInterval:(CGFloat)Interval
{
    self = [super initWithFrame:RectInfo];
    
                //...省略...
    
    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(selfLongPressed:)];
    longPress.delegate = self;
    longPress.minimumPressDuration = LongPressInterval;
    //self就是UIButton了 因此能夠對self add
    [self addGestureRecognizer:longPress];
    
    return self;
}
//JZParallaxButton.m
//長按會觸發的方法
- (void)selfLongPressed:(UILongPressGestureRecognizer *)sender
{
    CGPoint PressedPoint = [sender locationInView:self];
    NSLog(@"%F , %F",PressedPoint.x,PressedPoint.y);    
    //能夠讀取當前按下的位置
}

2.層級關係和邏輯判斷

咱們的按鈕在實現後應有如下層級:
層級關係

ParallaxButton:UIButton  //咱們創建的UIButton SubClass
|-BoundsView:UIView   //總視圖 
  |--LayerImageView1:UIImageView //分視圖 是總視圖的SubView
  |--LayerImageView2:UIImageView
  |--LayerImageView3:UIImageView
  |--LayerImageView4:UIImageView
  |--LayerImageView5:UIImageView
  |-- .... 
  |--LayerImageViewX:UIImageView

有的同窗有可能會以爲 爲何須要總視圖這個 BoundsView 呢 直接將全部的UIImageView都劃歸爲UIButton的SubView不就行了麼?
實驗過直接將UIImageView添加到UIButton爲SubView後 我有一個相對合理的解釋:
咱們以前分析原理的時候 說明實際上是隻有總圖層(即 BoundsView )在旋轉的 分圖層只需處理移動問題
若是去除了總圖層 就只能讓每一個分圖層(即 LayerImageView )在移動的同時都旋轉 這勢必帶來一個問題 那就是會有「厚度」的感受
讓咱們實驗下 若是層級關係以下圖 會是什麼結果
錯誤的顯示效果
能夠看到 這裏的圖層移動方式和原型裏的效果已經很接近了 可是由於分圖層移動半徑不一 而且沒有總圖層進行約束 致使分圖層的顯示區域不在一個長方形裏 看起來像是有厚度了同樣 整個按鈕實際看起來並無tvOS按鈕裏的那種輕盈感
所以 須要有總圖層進行約束 即將分圖層添加爲總圖層的SubView 並設置總圖層Layer的MasksToBounds爲YES 這時 全部分圖層的可見區域都受限制於總圖層 不管怎麼旋轉和移動都不會出現厚度感了
咱們如今將視圖層級須要的一些變量寫出來 而後再實現一些邏輯判斷的代碼 好比長按後須要作什麼

//JZParallaxButton.h
# import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger, ParallaxMethodType)
{
    ParallaxMethodTypeLinear = 0,
    ParallaxMethodTypeEaseIn,
    ParallaxMethodTypeEaseOut,
};

@interface JZParallaxButton : UIButton

//數組用於記錄當前Button包含的全部ImageLayer 即分圖層
@property (nonatomic,strong) NSMutableArray *LayerArray;

//button圓角
@property (nonatomic,assign) BOOL RoundCornerEnabled;

//button圓角
@property (nonatomic,assign) CGFloat RoundCornerRadius;

//是否在Parallax
@property (nonatomic,assign) BOOL isParallax;

@property (nonatomic,assign) int RotationAllSteps;
@property (nonatomic,assign) CGFloat RotationInterval;

@property (nonatomic,assign) CGFloat ScaleBase;
@property (nonatomic,assign) CGFloat ScaleAddition;

@property (nonatomic,assign) ParallaxMethodType ParallaxMethod;
@property (nonatomic,assign) RotateMethodType RotateMethod;

- (instancetype)initButtonWithCGRect:(CGRect)RectInfo
                      WithLayerArray:(NSMutableArray *)ArrayOfLayer
              WithRoundCornerEnabled:(BOOL)isRoundCorner
           WithCornerRadiusifEnabled:(CGFloat)Radius
                  WithRotationFrames:(int)Frames
                WithRotationInterval:(CGFloat)Interval;
//JZParallaxButton.m

# define RotateParameter 0.5 //用於調整旋轉幅度
# define SpotlightOutRange 20.0f //用於高光距離中心的長度
# define zPositionMax 800 //Core Layer變換時攝像機默認z軸位置

# define BoundsVieTranslation 50 //總圖層的移動幅度
# define LayerVieTranslation 20 //分圖層的移動幅度
# define LongPressInterval 0.5 //自動動畫狀態下的長按判斷時間  

@interface UIButton ()<UIGestureRecognizerDelegate>

@property (nonatomic,assign) int RotationNowStep; //記錄動畫的當前狀態
@property (nonatomic,weak)NSTimer *RotationTimer; //動畫計時器
@property (nonatomic,strong) UIImageView *SpotLightView;  //高光圖層
@property (nonatomic,strong) UIView *BoundsView; //總圖層
@property (nonatomic,assign) CGPoint TouchPointInSelf; //手指按下的時候在Button內部 的位置 用於Button設爲跟隨手指的時候
@property (nonatomic,assign) BOOL hasPreformedBeginAnimation; //判斷是否在進行動畫 防止動畫未表演完就觸發下一個動做 形成錯位
@end

@implementation JZParallaxButton
//省略 @synthesize ...

- (instancetype)initButtonWithCGRect:(CGRect)RectInfo
                      WithLayerArray:(NSMutableArray *)ArrayOfLayer
              WithRoundCornerEnabled:(BOOL)isRoundCorner
           WithCornerRadiusifEnabled:(CGFloat)Radius
                  WithRotationFrames:(int)Frames
                WithRotationInterval:(CGFloat)Interval
{
//省略以前的代碼....
    LayerArray = [[NSMutableArray alloc] initWithCapacity:[ArrayOfLayer count]];
    
    BoundsView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
    BoundsView.layer.masksToBounds = YES;
    BoundsView.layer.shouldRasterize = TRUE;
    BoundsView.layer.rasterizationScale = [[UIScreen mainScreen] scale];
    if (self.RoundCornerEnabled)
    {
        BoundsView.layer.cornerRadius = self.RoundCornerRadius;
    }
    [self addSubview:BoundsView];
    
    
    for (int i = 0; i < [ArrayOfLayer count]; i++)
    {
        UIImageView *LayerImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
        UIImage *LayerImage = [ArrayOfLayer objectAtIndex:i];
        [LayerImageView setImage:LayerImage];
        LayerImageView.layer.shouldRasterize = TRUE;
        LayerImageView.layer.rasterizationScale = [[UIScreen mainScreen] scale];
        
        //從下往上添加
        [BoundsView addSubview:LayerImageView];
        [LayerArray addObject:LayerImageView];
        
        //若是把全部分圖層都加完了
        if (i == [ArrayOfLayer count] - 1)
        {
            //在最上層添加高光分圖層
            SpotLightView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width,self.frame.size.height)];
            
            NSString *bundlePath = [[NSBundle bundleForClass:[JZParallaxButton class]]
                                    pathForResource:@"JZParallaxButton" ofType:@"bundle"];
            NSBundle *bundle = [NSBundle bundleWithPath:bundlePath];
            
            SpotLightView.image = [UIImage imageNamed:@"Spotlight" inBundle:bundle compatibleWithTraitCollection:nil];
            SpotLightView.contentMode = UIViewContentModeScaleAspectFit;
            SpotLightView.layer.masksToBounds = YES;
            [BoundsView addSubview:SpotLightView];
            SpotLightView.layer.zPosition = zPositionMax;
            [self bringSubviewToFront:SpotLightView];
            SpotLightView.alpha = 0.0;
            [LayerArray addObject:SpotLightView];
        }
}              
- (void)selfLongPressed:(UILongPressGestureRecognizer *)sender
{
    CGPoint PressedPoint = [sender locationInView:self];
    //NSLog(@"%F , %F",PressedPoint.x,PressedPoint.y);
    self.TouchPointInSelf = PressedPoint;
    
    if(sender.state == UIGestureRecognizerStateBegan)
    {
        //NSLog(@"Long Press");
        self.hasPreformedBeginAnimation = NO;
    
        switch (self.RotateMethod)
        {
            case RotateMethodTypeAutoRotate:
            {
            //長按後 若是在進行視差效果就結束 若是如今是普通狀態就開啓視差效果
                if (isParallax)
                {
                    [self EndParallax];
                }
                else
                {
                    [self BeginParallax];
                }
            }
                break;
    
            case RotateMethodTypeWithFinger:
            {
            //手動動畫結束視差效果並不依靠長按 而是經過判斷手指是否留在屏幕上
                if (!isParallax)
                {
                    [self BeginParallax];
                }
            }
                break;
        }
    }
    else if (sender.state == UIGestureRecognizerStateEnded)
    {
    //若是長按結束但仍有動畫在進行 就等待 LongPressInterval 再執行TouchUp方法 不然當即執行TouchUp
        if (self.hasPreformedBeginAnimation == NO)
        {
            [self performSelector:@selector(TouchUp) withObject:self afterDelay:LongPressInterval ];
        }
        else
        {
            [self TouchUp];
        }
    
    }
}
@end

這裏的 shouldRasterize

@property BOOL shouldRasterize
Description    A Boolean that indicates whether the layer is rendered as a bitmap before compositing. Animatable

在某些時候 例如導入的圖片分辨率較大 此時對UIImageView進行CA動畫 會出現鋸齒 這個時候 能夠經過設置 shouldRasterize 來解決

BoundsView.layer.shouldRasterize = YES;
BoundsView.layer.rasterizationScale = [[UIScreen mainScreen] scale];

固然設置 allowsEdgeAntialiasing 也是一種方法 這裏咱們對比下 能夠注意看右邊牆壁上的樹影 經過 shouldRasterize 設置後 基本上影子不會出現較大的變更:

兩種抗鋸齒的對比

3.透視效果須要實現的一個方法

//JZParallaxButton.m
//CATransform3DMake 默認給出的CATransform3D是沒有透視效果的 須要手動加入這一段 完成從 orthographic 到 perspective 的改變
//這個方法的解析能夠看 http://wonderffee.github.io/blog/2013/10/19/an-analysis-for-transform-samples-of-calayer/
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));
}

4.實現自動動畫

這部分主要內容就是 在長按後 按鈕會先進入某個特定的角度位置 而後進行自轉
這裏爲了簡單 使用了NSTimer進行每一幀的計數 但須要注意的是 NSTimer的精度不足以完成真正流暢的動畫
自動動畫裏 總圖層和分圖層的移動 旋轉都和兩個參數有關:一是當前的計數位置(即Step) 而是圖層在總按鈕裏的層級位置(即LayerArray裏的i) 經過這兩個參數進行計算CATransform3D

//  JZParallaxButton.m

#define OutTranslationParameter  (float)([LayerArray count] + i)/(float)([LayerArray count] * 2)
#define OutScaleParameter  ScaleBase+ScaleAddition/5*((float)i/(float)([LayerArray count]))

@implementation JZParallaxButton

- (void)BeginAutoRotation
{
    __weak JZParallaxButton *weakSelf = self;
    //須要到達動畫的起始位置
    
    CGFloat PIE = 0;
    CGFloat Degress = M_PI*2*PIE;
    //NSlog(@"Degress : %f PIE",PIE);
    CGFloat Sin = sin(Degress)/4;
    CGFloat Cos = cos(Degress)/4;
    
    int i =0;
    //計算初始位置的旋轉 移動 和縮放
    CATransform3D NewRotate,NewTranslation,NewScale;
    NewRotate = CATransform3DConcat(CATransform3DMakeRotation(-Sin*RotateParameter, 0, 1, 0), CATransform3DMakeRotation(Cos*RotateParameter, 1, 0, 0));
    NewTranslation = CATransform3DMakeTranslation(Sin*BoundsVieTranslation*OutTranslationParameter, Cos*BoundsVieTranslation*OutTranslationParameter, 0);
    NewScale = CATransform3DMakeScale(OutScaleParameter, OutScaleParameter, 1);
    
    //使用CATransform3DConcat將三個CATransform3D結合成一個CATransform3D
    CATransform3D TwoTransform = CATransform3DConcat(NewRotate,NewTranslation);
    CATransform3D AllTransform = CATransform3DConcat(TwoTransform,NewScale);
    //對BoundsLayer 即總圖層進行動畫
    CABasicAnimation *BoundsLayerCABasicAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
    BoundsLayerCABasicAnimation.duration = 0.4f;
    BoundsLayerCABasicAnimation.autoreverses = NO;
    BoundsLayerCABasicAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DPerspect(AllTransform, CGPointMake(0, 0), zPositionMax)];
    BoundsLayerCABasicAnimation.fromValue = [NSValue valueWithCATransform3D:BoundsView.layer.transform];
    BoundsLayerCABasicAnimation.fillMode = kCAFillModeBoth;
    BoundsLayerCABasicAnimation.removedOnCompletion = YES;
    [BoundsView.layer addAnimation:BoundsLayerCABasicAnimation forKey:@"BoundsLayerCABasicAnimation"];
    BoundsView.layer.transform = CATransform3DPerspect(AllTransform, CGPointMake(0, 0), zPositionMax);
    
    //對LayerArray內的UIImageView 即分圖層進行動畫
    for (int i = 0 ; i < [LayerArray count]; i++)
    {
        //對於不一樣的先後位置 須要移動的半徑不同
        float InTranslationParameter = [self InTranslationParameterWithLayerArray:LayerArray WithIndex:i];
        float InScaleParameter = [self InScaleParameterWithLayerArray:LayerArray WithIndex:i];
        UIImageView *LayerImageView = [LayerArray objectAtIndex:i];

        CATransform3D NewTranslation ;
        CATransform3D NewScale = CATransform3DMakeScale(InScaleParameter, InScaleParameter, 1);
        
        if (i == [LayerArray count] - 1) //是高光所在的View
        {
            NewTranslation = CATransform3DMakeTranslation(Sin*LayerVieTranslation*InTranslationParameter*SpotlightOutRange, Cos*LayerVieTranslation*InTranslationParameter*SpotlightOutRange, 0);
        }
        else //分圖層其餘的View 注意能夠看到這裏的 Translation 和高光是相反的
        {
            NewTranslation = CATransform3DMakeTranslation(-Sin*LayerVieTranslation*InTranslationParameter, -Cos*LayerVieTranslation*InTranslationParameter, 0);
        }
        
        CATransform3D NewAllTransform = CATransform3DConcat(NewTranslation,NewScale);
        
        CABasicAnimation *LayerImageViewCABasicAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
        LayerImageViewCABasicAnimation.duration = 0.4f;
        LayerImageViewCABasicAnimation.autoreverses = NO;
        LayerImageViewCABasicAnimation.toValue = [NSValue valueWithCATransform3D:NewAllTransform];
        LayerImageViewCABasicAnimation.fromValue = [NSValue valueWithCATransform3D:LayerImageView.layer.transform];
        LayerImageViewCABasicAnimation.fillMode = kCAFillModeBoth;
        LayerImageViewCABasicAnimation.removedOnCompletion = YES;
        
        
        CAAnimationGroup *animGroup = [CAAnimationGroup animation];
        animGroup.animations = [NSArray arrayWithObjects:LayerImageViewCABasicAnimation,nil];
        animGroup.duration = 0.4f;
        animGroup.removedOnCompletion = YES;
        animGroup.autoreverses = NO;
        animGroup.fillMode = kCAFillModeRemoved;
        
        [CATransaction begin];
        LayerImageView.layer.transform = CATransform3DPerspect(NewAllTransform, CGPointMake(0, 0), zPositionMax);
        [CATransaction setCompletionBlock:^
         {
             if (i == [LayerArray count] - 1)
             {
                 //簡單的週期循環
                 RotationNowStep = 0;
                 RotationTimer =  [NSTimer scheduledTimerWithTimeInterval:RotationInterval/RotationAllSteps target:weakSelf selector:@selector(RotationCreator) userInfo:nil repeats:YES];
                 weakSelf.hasPreformedBeginAnimation = YES;
             }
         }];
        [LayerImageView.layer addAnimation:animGroup forKey:@"LayerImageViewParallaxInitAnimation"];
        [CATransaction commit];
    }

    
}

//計時器每次觸發要執行的方法
- (void)RotationCreator
{
    __weak JZParallaxButton *weakSelf = self;
 
    //NSlog(@"RotationNowStep : %d of %d",RotationNowStep,RotationAllSteps);
    if (RotationNowStep == RotationAllSteps)
    {
    //一週完成 計數器置1
        RotationNowStep = 1;
    }
    else
    {
        RotationNowStep ++ ;
    }
    
    CGFloat PIE = (float)RotationNowStep/(float)RotationAllSteps;
    CGFloat Degress = M_PI*2*PIE;
    //NSlog(@"Degress : %f PIE",PIE);
    CGFloat Sin = sin(Degress)/4;
    CGFloat Cos = cos(Degress)/4;
    
    int i = 0;
    CATransform3D NewRotate,NewTranslation,NewScale;
    NewRotate = CATransform3DConcat(CATransform3DMakeRotation(-Sin*RotateParameter, 0, 1, 0), CATransform3DMakeRotation(Cos*RotateParameter, 1, 0, 0));
    NewTranslation = CATransform3DMakeTranslation(Sin*BoundsVieTranslation*OutTranslationParameter, Cos*BoundsVieTranslation*OutTranslationParameter, 0);
    NewScale = CATransform3DMakeScale(OutScaleParameter, OutScaleParameter, 1);
    
    CATransform3D TwoTransform = CATransform3DConcat(NewRotate,NewTranslation);
    CATransform3D AllTransform = CATransform3DConcat(TwoTransform,NewScale);
    BoundsView.layer.transform = CATransform3DPerspect(AllTransform, CGPointMake(0, 0), zPositionMax);
    
    for (int i = 0 ; i < [LayerArray count]; i++)
    {
        float InScaleParameter = [self InScaleParameterWithLayerArray:LayerArray WithIndex:i];
        float InTranslationParameter = [self InTranslationParameterWithLayerArray:LayerArray WithIndex:i];
        
        
        if (i == [LayerArray count] - 1) //is spotlight
        {
            UIImageView *LayerImageView = [weakSelf.LayerArray objectAtIndex:i];
            
            CATransform3D Translation = CATransform3DMakeTranslation(Sin*LayerVieTranslation*InTranslationParameter*SpotlightOutRange, Cos*LayerVieTranslation*InTranslationParameter*SpotlightOutRange,0);
            CATransform3D Scale = CATransform3DMakeScale(InScaleParameter, InScaleParameter, 1);
            CATransform3D AllTransform = CATransform3DConcat(Translation,Scale);
            
            LayerImageView.layer.transform = CATransform3DPerspect(AllTransform, CGPointMake(0, 0), zPositionMax);
        }
        else //is Parallax layer
        {
            UIImageView *LayerImageView = [weakSelf.LayerArray objectAtIndex:i];
            
            CATransform3D Translation = CATransform3DMakeTranslation(-Sin*LayerVieTranslation*InTranslationParameter, -Cos*LayerVieTranslation*InTranslationParameter, 0);
            CATransform3D Scale = CATransform3DMakeScale(InScaleParameter, InScaleParameter, 1);
            CATransform3D AllTransform = CATransform3DConcat(Translation,Scale);
            
            LayerImageView.layer.transform = CATransform3DPerspect(AllTransform, CGPointMake(0, 0), zPositionMax);
        }
        
    }

    
    
}

- (float)InTranslationParameterWithLayerArray:(NSMutableArray *)Array
                                    WithIndex:(int)i
{

    switch (ParallaxMethod)
    {
        //移動半徑和圖層層級成線性關係
        case ParallaxMethodTypeLinear:
            return (float)(i)/(float)([Array count]);
            break;
        //移動半徑和圖層層級成二次關係
        case ParallaxMethodTypeEaseIn:
            return powf((float)(i)/(float)([Array count]), 0.5f);
            break;
        //移動半徑和圖層層級成根號關係
        case ParallaxMethodTypeEaseOut:
            return powf((float)(i)/(float)([Array count]), 2.0f);
            break;
            
        default:
            return (float)(i)/(float)([Array count]);
            break;
    }
}
- (float)InScaleParameterWithLayerArray:(NSMutableArray *)Array
                                    WithIndex:(int)i
{
    
    switch (ParallaxMethod)
    {
    //縮放與圖層層級的不一樣關係
        case ParallaxMethodTypeLinear:
            return 1+ScaleAddition/10*((float)i/(float)([LayerArray count]));
            break;
            
        case ParallaxMethodTypeEaseIn:
            return 1+ScaleAddition/10*powf(((float)i/(float)([LayerArray count])), 0.5f);
            break;
            
        case ParallaxMethodTypeEaseOut:
            return 1+ScaleAddition/10*powf(((float)i/(float)([LayerArray count])), 2.0f);
            break;
            
        default:
            return 1+ScaleAddition/10*((float)i/(float)([LayerArray count]));
            break;
    }
}
@end

5.實現手動動畫

//手動動畫和自動動畫的區別是 移動的角度再也不跟進計數器計算 而是直接讀取手指的CGPoint
__weak JZParallaxButton *weakSelf = self;
    CGFloat XOffest;
    if (TouchPointInSelf.x < 0)
    {
        XOffest = - weakSelf.frame.size.width / 2;
    }else if (TouchPointInSelf.x > weakSelf.frame.size.width)
    {
        XOffest = weakSelf.frame.size.width / 2;
    }else
    {
        XOffest = TouchPointInSelf.x - weakSelf.frame.size.width / 2;
    }
    
    CGFloat YOffest;
    if (TouchPointInSelf.y < 0)
    {
        YOffest = - weakSelf.frame.size.height / 2;
    }else if (TouchPointInSelf.y > weakSelf.frame.size.height)
    {
        YOffest = weakSelf.frame.size.height / 2;
    }else
    {
        YOffest = TouchPointInSelf.y - weakSelf.frame.size.height / 2;
    }
    
    //NSLog(@"XOffest : %f , YOffest : %f",XOffest,YOffest);
    
    CGFloat XDegress = XOffest / weakSelf.frame.size.width / 2;
    CGFloat YDegress = YOffest / weakSelf.frame.size.height / 2;
    
    //NSLog(@"XDegress : %f , YDegress : %f",XDegress,YDegress);

效果

此時還有不少方法沒有實現(好比動畫結束後須要變回原有的非三維效果) 不過大致上已經能夠看到效果了 你也能夠直接將完成版的視差按鈕下載下來
Have Fun :-)

其餘資源

若是你對真正三維狀態下的按鈕仍是不太理解 能夠點擊這個連接自由查看這個三維模型 點擊播放按鈕後 經過三維場景右下角的左右切換查看按鈕的不一樣旋轉狀態 或者點擊數字1-5來快速跳轉

之外 這篇文章裏全部文件都是提供公共下載的
配圖所用Sketch文件:下載連接
三維模型文件(進入點擊Download):下載連接

做者

JustZht
Github : https://github.com/JustinFincher
Weibo : http://weibo.com/justzht

Animatious一塊兒動畫開源組

LOGO
Website : http://anius.io/
Github : https://github.com/Animatious
Weibo : http://weibo.com/u/5760273536

相關文章
相關標籤/搜索