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