LMDropdownView 源码分析

LMDropdownView是一个简单的下拉视图,灵感来自于Tappy。实现了背景模糊+3D效果。使用了Core Animation的关键帧动画,可以很方便地更改菜单内容视图。

并且在展示和收起菜单视图时还有很不错的弹动效果。

模糊+3D效果下拉菜单视图--LMDropdownView

github源码:https://github.com/lminhtm/LMDropdownView

分析过程源码: https://github.com/kakukeme/iOS-Source-Code-Analyze

/*!
 *  A simple dropdown view inspired by Tappy.
 *  LMDropdownView是一个简单的下拉视图,灵感来自于Tappy。
 */
@interface LMDropdownView : NSObject

/*!
 *  The closed scale of container view.
 *  Set it to 1 to disable container scale animation.
 *  设为1,关闭容器view缩放动画
 */
@property (nonatomic, assign) CGFloat closedScale;

/**
 A boolean indicates whether container view should be blurred. Default is YES
 容器view是否使用模糊效果,默认yes;
 */
@property (nonatomic, assign) BOOL shouldBlurContainerView;

/*!
 *  The blur radius of container view.
 *  容器view的模糊半径
 */
@property (nonatomic, assign) CGFloat blurRadius;

/*!
 *  The alpha of black mask button.
 *  背景遮罩透明度
 */
@property (nonatomic, assign) CGFloat blackMaskAlpha;

/*!
 *  The animation duration.
 *  动画持续时间
 */
@property (nonatomic, assign) CGFloat animationDuration;

/*!
 *  The animation bounce height of content view.
 *  内容view的拉伸高度;使用关键帧动画实现Spring弹簧动画效果
 */
@property (nonatomic, assign) CGFloat animationBounceHeight;

/*!
 *  The animation direction.
 *  动画方向,顶部向下,底部向上;
 */
@property (nonatomic, assign) LMDropdownViewDirection direction;

/*!
 *  The background color of content view.
 *  内容view 背景色
 */
@property (nonatomic, strong) UIColor *contentBackgroundColor;

/*!
 *  The current dropdown view state.
 *  当前dropdown view 的状态;
 */
@property (nonatomic, assign, readonly) LMDropdownViewState currentState;

/*!
 *  A boolean indicates whether dropdown is open.
 *  标志dropdown是否打开;
 */
@property (nonatomic, assign, readonly) BOOL isOpen;

/*!
 *  The dropdown view delegate.
 *  代理
 */
@property (nonatomic, weak) id delegate;

/**
 *  The callback when dropdown view did show in the container view.
 *  dropdown在容器view中已经显示后的回调
 */
@property (nonatomic, copy) dispatch_block_t didShowHandler;

/**
 *  The callback when dropdown view did hide in the container view.
 *  隐藏的回调
 */
@property (nonatomic, copy) dispatch_block_t didHideHandler;

/*!
 *  Convenience constructor for LMDropdownView.
 *  类方法,方便的构造方法
 */
+ (instancetype)dropdownView;

/*!
 *  Show dropdown view.
 *  显示方法,指定容器view,内容view;
 *
 *  @param containerView The containerView to contain.
 *  @param contentView   The contentView to show.
 *  @param origin        The origin point in the container coordinator system.
 */
- (void)showInView:(UIView *)containerView withContentView:(UIView *)contentView atOrigin:(CGPoint)origin;

/*!
 *  Show dropdown view from navigation controller.
 *  从导航控制器navigation下显示;
 *
 *  @param navigationController The navigation controller to show from.
 *  @param contentView          The contentView to show.
 */
- (void)showFromNavigationController:(UINavigationController *)navigationController withContentView:(UIView *)contentView;

/*!
 *  Hide dropdown view.
 *  隐藏;
 */
- (void)hide;

/*!
 *  Force hide dropdown view.
 *  强制隐藏,没有hide里的动画了;
 */
- (void)forceHide;

@end

1、init中初始化一些属性变量;同时添加了屏幕旋转的通知UIDeviceOrientationDidChangeNotification;
屏幕旋转了,就强制hide,处理一些代理、回调;

2、显示方法-[LMDropdownView showInView:withContentView:atOrigin:]中调用
-[LMDropdownView setupContentView:inView:atOrigin:]设置 containerView 和 contentView;

// 视图层次

// 1、mainView是scrollView;
[containerView addSubview:self.mainView];

// 2、设置了截图背景containerImage
[self.mainView addSubview:self.containerWrapperView];

// 3、背景遮罩点击事件
[self.mainView addSubview:self.backgroundButton];

// 4、内容包裹view的frame;
// content包裹view高度
CGFloat contentWrapperViewHeight = CGRectGetHeight(contentView.frame) + self.animationBounceHeight;
switch (self.direction) {
    case LMDropdownViewDirectionTop:
        contentView.frame = CGRectMake(0, self.animationBounceHeight, W(contentView), H(contentView));
        
        // 开始设置view,没出现位置开始,方便动画
        self.contentWrapperView.frame = CGRectMake(origin.x,
                                                   origin.y - contentWrapperViewHeight,
                                                   W(contentView),
                                                   contentWrapperViewHeight);
        break;
    case LMDropdownViewDirectionBottom:
        // 往下点,
        contentView.frame = CGRectMake(0, 0, W(contentView), H(contentView));
        self.contentWrapperView.frame = CGRectMake(origin.x,
                                                   origin.y + contentWrapperViewHeight,
                                                   W(contentView),
                                                   contentWrapperViewHeight);
        break;
    default:
        break;
}
[self.contentWrapperView addSubview:contentView];
[self.mainView addSubview:self.contentWrapperView];

// 5、内容包裹view,位置记录
// 内容包裹view,动画开始位置中心
originContentCenter = CGPointMake(midx(self.contentWrapperView), midy(self.contentWrapperView));

// 内容包裹view,动画终点位置中心
if (self.direction == LMDropdownViewDirectionTop) {
    desContentCenter = CGPointMake(midx(self.contentWrapperView), origin.y + contentWrapperViewHeight/2 - self.animationBounceHeight);
}
else {
    desContentCenter = CGPointMake(midx(self.contentWrapperView), origin.y + contentWrapperViewHeight/2);
}

3、关键帧动画;

- (void)addContentAnimationForState:(LMDropdownViewState)state
{
    CAKeyframeAnimation *contentBounceAnim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    contentBounceAnim.duration = self.animationDuration;
    contentBounceAnim.removedOnCompletion = NO;
    contentBounceAnim.fillMode = kCAFillModeForwards;
    contentBounceAnim.values = [self contentPositionValuesForState:state];
    contentBounceAnim.timingFunctions = [self contentTimingFunctionsForState:state];
    contentBounceAnim.keyTimes = [self contentKeyTimesForState:state];
    
    [self.contentWrapperView.layer addAnimation:contentBounceAnim forKey:nil];
    [self.contentWrapperView.layer setValue:[contentBounceAnim.values lastObject] forKeyPath:@"position"];
}

- (void)addContainerAnimationForState:(LMDropdownViewState)state
{
    CAKeyframeAnimation *containerScaleAnim = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
    containerScaleAnim.duration = self.animationDuration;
    containerScaleAnim.removedOnCompletion = NO;
    containerScaleAnim.fillMode = kCAFillModeForwards;
    containerScaleAnim.values = [self containerTransformValuesForState:state];
    containerScaleAnim.timingFunctions = [self containerTimingFunctionsForState:state];
    containerScaleAnim.keyTimes = [self containerKeyTimesForState:state];
    
    [self.containerWrapperView.layer addAnimation:containerScaleAnim forKey:nil];
    [self.containerWrapperView.layer setValue:[containerScaleAnim.values lastObject] forKeyPath:@"transform"];
}

// 开始动画 二维码扫描关键帧;
CAKeyframeAnimation *animationMove = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"];
animationMove.values = @[@(0.0),@(_scanFrameView.tf_height-_scannerView.tf_height),@(0.0)];
animationMove.duration = 2.5f;
animationMove.repeatCount = CGFLOAT_MAX;
animationMove.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[self.scannerView.layer addAnimation:animationMove forKey:nil];

4、使用NSMutableArray,保存关键帧动画执行values

/** content 内容view, 不同状态的,各种位置保存下 */
- (NSArray *)contentPositionValuesForState:(LMDropdownViewState)state
{
    CGPoint currentContentCenter = self.contentWrapperView.layer.position; // position 为中心点
    
    NSMutableArray *values = [NSMutableArray new];
    [values addObject:[NSValue valueWithCGPoint:currentContentCenter]];
    
    if (state == LMDropdownViewStateWillOpen || state == LMDropdownViewStateDidOpen)    // show
    {
        if (self.direction == LMDropdownViewDirectionTop) {
            // 向下拉伸点,类似弹簧
            [values addObject:[NSValue valueWithCGPoint:CGPointMake(currentContentCenter.x, desContentCenter.y + self.animationBounceHeight)]];
        }
        else {
            [values addObject:[NSValue valueWithCGPoint:CGPointMake(currentContentCenter.x, desContentCenter.y - self.animationBounceHeight)]];
        }
        [values addObject:[NSValue valueWithCGPoint:CGPointMake(currentContentCenter.x, desContentCenter.y)]];
    }
    else    // hide
    {
        if (self.direction == LMDropdownViewDirectionTop) {
            [values addObject:[NSValue valueWithCGPoint:CGPointMake(currentContentCenter.x, currentContentCenter.y + self.animationBounceHeight)]]; // 关闭时,往下伸一下,作出弹簧效果;
        }
        else {
            [values addObject:[NSValue valueWithCGPoint:CGPointMake(currentContentCenter.x, currentContentCenter.y - self.animationBounceHeight)]];
        }
        // 最终隐藏点;
        [values addObject:[NSValue valueWithCGPoint:CGPointMake(currentContentCenter.x, originContentCenter.y)]];
    }
    
    return values;
}


- (NSArray *)containerTransformValuesForState:(LMDropdownViewState)state
{
    CATransform3D transform = self.containerWrapperView.layer.transform;
    
    NSMutableArray *values = [NSMutableArray new];
    [values addObject:[NSValue valueWithCATransform3D:transform]];
    
    if (state == LMDropdownViewStateWillOpen || state == LMDropdownViewStateDidOpen)
    {
        CGFloat scale = self.closedScale - kDefaultAnimationBounceScale;
        [values addObject:[NSValue valueWithCATransform3D:CATransform3DScale(transform, scale, scale, scale)]];
        [values addObject:[NSValue valueWithCATransform3D:CATransform3DScale(transform, self.closedScale, self.closedScale, self.closedScale)]];
    }
    else
    {
        CGFloat scale = 1 - kDefaultAnimationBounceScale;
        [values addObject:[NSValue valueWithCATransform3D:CATransform3DScale(transform, scale, scale, scale)]];
        [values addObject:[NSValue valueWithCATransform3D:CATransform3DIdentity]];
    }
    
    return values;      // CATransform3DScale 3d缩放动画吗
}

5、支持retina截屏,模糊处理

#pragma mark - CREATE IMAGE

/** 截图,支持retina */
+ (UIImage *)imageFromView:(UIView *)theView withSize:(CGSize)size
{
    UIGraphicsBeginImageContext(size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // -renderInContext: renders in the coordinate space of the layer,
    // so we must first apply the layer's geometry to the graphics context
    CGContextSaveGState(context);
    // Center the context around the window's anchor point
    CGContextTranslateCTM(context, size.width/2, size.height/2);
    // Apply the window's transform about the anchor point
    CGContextConcatCTM(context, [theView transform]);
    // Offset by the portion of the bounds left of and above the anchor point
    CGContextTranslateCTM(context,
                          -[theView bounds].size.width * [[theView layer] anchorPoint].x,
                          -[theView bounds].size.height * [[theView layer] anchorPoint].y);
    
    //  [theView.layer renderInContext:context];
    [theView drawViewHierarchyInRect:[theView bounds] afterScreenUpdates:NO];
    
    // Restore the context
    CGContextRestoreGState(context);
    
    UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return theImage;
}


#pragma mark - CUSTOMIZE IMAGE

/** 模糊效果 */
- (UIImage *)blurredImageWithRadius:(CGFloat)radius
                         iterations:(NSUInteger)iterations
                          tintColor:(UIColor *)tintColor
{
    //image must be nonzero size
    if (floorf(self.size.width) * floorf(self.size.height) <= 0.0f) return self;
    
    //boxsize must be an odd integer
    uint32_t boxSize = (uint32_t)(radius * self.scale);
    if (boxSize % 2 == 0) boxSize ++;
    
    //create image buffers
    CGImageRef imageRef = self.CGImage;
    vImage_Buffer buffer1, buffer2;
    buffer1.width = buffer2.width = CGImageGetWidth(imageRef);
    buffer1.height = buffer2.height = CGImageGetHeight(imageRef);
    buffer1.rowBytes = buffer2.rowBytes = CGImageGetBytesPerRow(imageRef);
    size_t bytes = buffer1.rowBytes * buffer1.height;
    buffer1.data = malloc(bytes);
    buffer2.data = malloc(bytes);
    
    //create temp buffer
    void *tempBuffer = malloc((size_t)vImageBoxConvolve_ARGB8888(&buffer1, &buffer2, NULL, 0, 0, boxSize, boxSize,
                                                                 NULL, kvImageEdgeExtend + kvImageGetTempBufferSize));
    
    //copy image data
    CFDataRef dataSource = CGDataProviderCopyData(CGImageGetDataProvider(imageRef));
    memcpy(buffer1.data, CFDataGetBytePtr(dataSource), bytes);
    CFRelease(dataSource);
    
    for (NSUInteger i = 0; i < iterations; i++)
    {
        //perform blur
        vImageBoxConvolve_ARGB8888(&buffer1, &buffer2, tempBuffer, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
        
        //swap buffers
        void *temp = buffer1.data;
        buffer1.data = buffer2.data;
        buffer2.data = temp;
    }
    
    //free buffers
    free(buffer2.data);
    free(tempBuffer);
    
    //create image context from buffer
    CGContextRef ctx = CGBitmapContextCreate(buffer1.data, buffer1.width, buffer1.height,
                                             8, buffer1.rowBytes, CGImageGetColorSpace(imageRef),
                                             CGImageGetBitmapInfo(imageRef));
    
    //apply tint
    if (tintColor && CGColorGetAlpha(tintColor.CGColor) > 0.0f)
    {
        CGContextSetFillColorWithColor(ctx, [tintColor colorWithAlphaComponent:0.25].CGColor);
        CGContextSetBlendMode(ctx, kCGBlendModePlusLighter);
        CGContextFillRect(ctx, CGRectMake(0, 0, buffer1.width, buffer1.height));
    }
    
    //create image from context
    imageRef = CGBitmapContextCreateImage(ctx);
    UIImage *image = [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation];
    CGImageRelease(imageRef);
    CGContextRelease(ctx);
    free(buffer1.data);
    return image;
}

你可能感兴趣的:(LMDropdownView 源码分析)