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

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,093评论 4 62
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,072评论 25 707
  • 近日工作生活都很繁忙,感觉压力山大,本周感觉身体里的力量降低了很多,内心也没有以往的激情,这种状态持续了4/...
    浮云狒阅读 2,828评论 0 0
  • 最受人欺负的是什么?答道“人欺负人”?我想不是。可能你会有疑问:难道还有比这更糟糕的? 对!或许你不知道、但这些都...
    Distance_0f1a阅读 298评论 0 1