消失了好久,大家放心,我还活着。
要问我为什么消失了这么久,如果你知道什么叫封闭开发或许你会懂我。
然而最近一直也没时间搞什么飞机,也没有什么能拿出来跟大家分享的,就把最近开发过程中写的一些小东西贴出来给大家看吧。
因为东西比较少,而且没有什么新鲜的技术点,所以老司机先把效果图放出来,这样的话如果你不感兴趣可能看到这就够了。
在这篇文章中,你会看到以下内容:
- 滑动验证视图
- 继承UIControl重新实现一个Slider
- 步进Slider
滑动验证视图
看到这了相信你可能是对这个滑动验证有些兴趣。
之所以写这个控件,是因为需求用到了,然而当前有没有相应的类库能让我拿来直接用。
所有效果是仿照日常网页中的效果去做的,所以我们还是应该首先分析一下我们需要什么。
- 首先,我们需要一张底图
- 第二,我们要从底图上截取一小部分作为一个滑块
- 最后,当滑块位置改变并且最终与截取的位置重合时应该验证成功
需求在这了,可能唯一的技术点就在于如何截取图片了。
#pragma mark - 截取当前image对象rect区域内的图像
- (UIImage *)dw_SubImageWithRect:(CGRect)rect {
///防止处理过image的scale不为1情况rect错误
CGFloat scale = self.scale;
CGRect scaleRect = CGRectMake(rect.origin.x * scale, rect.origin.y * scale, rect.size.width * scale, rect.size.height * scale);
CGImageRef newImageRef = CGImageCreateWithImageInRect(self.CGImage, scaleRect);
UIImage *newImage = [[UIImage imageWithCGImage:newImageRef] dw_RescaleImageToSize:rect.size];
CGImageRelease(newImageRef);
return newImage;
}
#pragma mark - 压缩图片至指定尺寸
- (UIImage *)dw_RescaleImageToSize:(CGSize)size
{
CGRect rect = (CGRect){CGPointZero, size};
UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
[self drawInRect:rect];
UIImage *resImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return resImage;
}
这些代码是老司机写在一个UIImage的category里面的,所以你应该知道所有的self都是一个UIImage实例。恩,在这个分类里面老司机封装了很多UIImage常用的方法,如下:
///高性能按图片名称检索本地图片
+(UIImage *)dw_ImageNamed:(NSString *)name;
///高性能返回无延迟立即解压的图片实例
+(UIImage *)dw_ImageWithUrl:(NSURL *)url;
///转换图片为Base64字符串
-(NSString *)dw_ImageToBase64String;
///Base64转换为图片
+ (UIImage *)dw_ImageWithBase64String:(NSString *)base64String;
///取图片某点颜色
-(UIColor *)dw_ColorAtPoint:(CGPoint)point;
///按给定颜色生层图片
+(UIImage *)dw_ImageWithColor:(UIColor *)color;
///以灰色空间生成图片
-(UIImage *)dw_ConvertToGrayImage;
///生成图片的反色图片对象
-(UIImage *)dw_ConvertToReversedColor;
///以给定颜色生成图像剪影
-(UIImage *)dw_ConvertToSketchWithColor:(UIColor *)color;
///生成处理每像素颜色后的图片
-(UIImage *)dw_ConvertImageWithPixelHandler:(void(^)(UInt8 * pixel,int x,int y))handler;
///获取带圆角的图片
-(UIImage *)dw_CornerRadius:(CGFloat)radius withWidth:(CGFloat)width contentMode:(DWContentMode)mode;
///按给定path剪裁图片
-(UIImage *)dw_ClipImageWithPath:(UIBezierPath *)path mode:(DWContentMode)mode;
///获取旋转角度的图片
-(UIImage *)dw_RotateImageWithAngle:(CGFloat)angle;
///按给定的方向旋转图片
-(UIImage*)dw_RotateWithOrient:(UIImageOrientation)orient;
///垂直翻转
-(UIImage *)dw_FlipVertical;
///水平翻转
-(UIImage *)dw_FlipHorizontal;
///压缩图片至指定尺寸
-(UIImage *)dw_RescaleImageToSize:(CGSize)size;
///压缩图片至指定像素
-(UIImage *)dw_RescaleImageToPX:(CGFloat )toPX;
///纠正图片方向
-(UIImage *)dw_FixOrientation;
///截取当前image对象rect区域内的图像
-(UIImage *)dw_SubImageWithRect:(CGRect)rect;
///在指定的size里面生成一个平铺的图片
-(UIImage *)dw_GetTiledImageWithSize:(CGSize)size;
///UIView转化为UIImage
+(UIImage *)dw_ImageFromView:(UIView *)view;
///将两个图片生成一张图片
+(UIImage*)dw_MergeImage:(UIImage*)firstImage withImage:(UIImage*)secondImage;
这个分类还是比较全的,老司机还是比较推荐的,给个传送门,觉得好的话给我个star吧。
所以说借助这个分类,你应该可以从一整张图片上截取一部分图片了,接下来你只需要:
- 随意生成一个区域将它定为验证区域,并在该区域覆盖滑块形状的白色半透明的覆盖层
- 创建一个与上面的区域形状相同的Layer,将截取好的图片赋给Layer,同时用贝塞尔曲线将Layer绘制成滑块的形状
- 最后当验证视图滑块的位置改变至验证区域时,验证成功即可。
思路比较简单,实现起来也比较简单,300多行代码,老司机就直接放全部代码了:
-(instancetype)initWithFrame:(CGRect)frame {
NSAssert((frame.size.width >= 100 && frame.size.height >= 40), @"To get a better experience,you may set the width more than 100 and height more than 50.");
if (self = [super initWithFrame:frame]) {
_useRandomValue = YES;
_targetValue = DWSlideCaptchaUndefineValue;
_thumbCenterY = DWSlideCaptchaUndefineValue;
_tolerance = DWSlideCaptchaUndefineValue;
_thumbSize = puzzlePath().bounds.size;
}
return self;
}
-(instancetype)initWithFrame:(CGRect)frame bgImage:(UIImage *)bgImage {
if (self = [self initWithFrame:frame]) {
[self beginConfiguration];
self.bgImage = bgImage;
[self commitConfiguration];
}
return self;
}
-(void)beginConfiguration {
_configurating = YES;
self.resetTargetPoint = YES;
}
-(void)commitConfiguration {
if (!self.configurating) {
return;
}
_configurating = NO;
self.layer.contents = (id)self.bgImage.CGImage;
[self handlePositionLayer];
[self handleThumbLayer];
[self hideThumbWithAnimated:NO];
}
-(void)reset {
_successed = NO;
[self beginConfiguration];
[self commitConfiguration];
}
-(void)indentifyWithAnimated:(BOOL)animated result:(void(^)(BOOL success))result {
BOOL isSuccess = fabs(self.targetPoint.x - self.currentPoint.x) < self.tolerance;
isSuccess &= fabs(self.targetPoint.y - self.currentPoint.y) < self.tolerance;
_successed = isSuccess;
_indentified = YES;
if (isSuccess) {
if (animated) {
if (self.delegate && [self.delegate respondsToSelector:@selector(dw_CaptchaView:animationWillStartWithSuccess:)]) {///动画开始回调
[self.delegate dw_CaptchaView:self animationWillStartWithSuccess:YES];
}
if (!self.successAnimation) {///未指定动画使用默认动画
[self.layer addAnimation:defaultSuccessAnimaiton(self) forKey:@"successAnimation"];
[self hideThumbWithAnimated:NO];
DWLayerTransactionWithAnimation(NO, ^(){
self.positionLayer.opacity = 0;
});
} else {///使用指定动画
[self.thumbLayer addAnimation:self.successAnimation forKey:@"successAnimation"];
}
}
if (result) result(YES);
} else {
if (animated) {
if (self.delegate && [self.delegate respondsToSelector:@selector(dw_CaptchaView:animationWillStartWithSuccess:)]) {///动画开始回调
[self.delegate dw_CaptchaView:self animationWillStartWithSuccess:YES];
}
if (!self.failAnimation) {///未指定动画则使用默认动画
[self.thumbLayer addAnimation:defaultFailAnimation(self) forKey:@"failAnimation"];
} else {///使用指定动画
[self.thumbLayer addAnimation:self.failAnimation forKey:@"failAnimation"];
}
}
if (result) result(NO);
}
}
-(void)moveToPoint:(CGPoint)point animated:(BOOL)animated {
if (self.successed) {
return;
}
_indentified = NO;
point = fixPointWithLimit(point, self.validSize, self.thumbSize);
self.currentPoint = point;
if (self.thumbLayer.opacity != 1) {
[self showThumbWithAnimated:YES];
}
DWLayerTransactionWithAnimation(animated, ^(){
self.thumbLayer.position = transformLocation2Center(point, self.thumbSize);
});
}
-(void)setValue:(CGFloat)value animated:(BOOL)animated {
CGFloat x = value * self.validSize.width + self.thumbSize.width / 2;
CGFloat y = self.targetPoint.y + self.thumbSize.height / 2;
[self moveToPoint:CGPointMake(x, y) animated:animated];
}
-(void)showThumbWithAnimated:(BOOL)animated {
DWLayerTransactionWithAnimation(animated,^(){
self.thumbLayer.opacity = 1;
});
}
-(void)hideThumbWithAnimated:(BOOL)animated {
DWLayerTransactionWithAnimation(animated, ^(){
self.thumbLayer.opacity = 0;
});
}
#pragma mark --- tool Method ---
-(void)handleThumbLayer {
UIImage * thumbImage = [self.bgImage dw_SubImageWithRect:self.positionLayer.frame];
thumbImage = [thumbImage dw_ClipImageWithPath:self.thumbShape mode:(DWContentModeScaleToFill)];
self.thumbLayer.contents = (id)thumbImage.CGImage;
self.thumbLayer.frame = self.positionLayer.frame;
[self setValue:0 animated:NO];
if (!self.thumbLayer.superlayer) {
[self.layer addSublayer:self.thumbLayer];
}
}
-(void)handlePositionLayer {
UIBezierPath * path = [self.thumbShape copy];
self.positionLayer.fillColor = [UIColor colorWithWhite:1 alpha:0.7].CGColor;
self.positionLayer.path = path.CGPath;
self.positionLayer.frame = CGRectMake(self.targetPoint.x, self.targetPoint.y, (int)path.bounds.size.width, (int)path.bounds.size.height);
if (!self.positionLayer.superlayer) {
[self.layer addSublayer:self.positionLayer];
}
}
#pragma mark --- animation delegate ---
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
if (self.isSuccessed) {
if (self.delegate && [self.delegate respondsToSelector:@selector(dw_CaptchaView:animationCompletionWithSuccess:)]) {
[self.delegate dw_CaptchaView:self animationCompletionWithSuccess:YES];
}
} else {
if (self.delegate && [self.delegate respondsToSelector:@selector(dw_CaptchaView:animationCompletionWithSuccess:)]) {
[self.delegate dw_CaptchaView:self animationCompletionWithSuccess:NO];
}
}
}
#pragma mark --- 内联方法 ---
///默认滑块形状
static inline UIBezierPath * puzzlePath (){
UIBezierPath * path = [UIBezierPath bezierPathWithPathMaker:^(DWPathMaker *maker) {
maker.MoveTo(0, 8).
AddLineTo(12, 8).AddArcWithPoint(12, 8, 20, 8, 5, YES, YES).AddLineTo(32, 8).
AddLineTo(32, 20).AddArcWithPoint(32, 20, 32, 28, 5, YES, YES).AddLineTo(32, 40).
AddLineTo(20, 40).AddArcWithPoint(20, 40, 12, 40, 5, NO, YES).AddLineTo(0, 40).
AddLineTo(0, 28).AddArcWithPoint(0, 28, 0, 20, 5, NO, YES).ClosePath();
}];
return path;
}
///指定尺寸内的随机点
static inline CGPoint randomPointInSize(CGSize size) {
CGPoint point = CGPointZero;
point.x = randomValueInLength((int)size.width);
point.y = randomValueInLength((int)size.height);
return point;
}
///指定范围内的随机值
static inline int randomValueInLength(int length) {
return arc4random() % ((int)(length + 1));
}
///修正centerY值合适的值
static inline CGFloat fixCenterYWithSize(CGSize thumbSize,CGSize validSize,CGFloat centerY) {
CGFloat y = centerY - thumbSize.height / 2;
return fixValueWithLimit(y, validSize.height);
}
///将值修正至指定范围
static inline CGFloat fixValueWithLimit(CGFloat value,CGFloat limitLength) {
return value < 0 ? 0 : (value > limitLength ? limitLength : value);
}
///将点修正值有效范围内
static inline CGPoint fixPointWithLimit(CGPoint point,CGSize validSize,CGSize thumbSize) {
CGFloat x = point.x - thumbSize.width / 2;
CGFloat y = point.y - thumbSize.height / 2;
return CGPointMake(fixValueWithLimit(x, validSize.width), fixValueWithLimit(y, validSize.height));
}
///将验证位置转换为layer中心点
static inline CGPoint transformLocation2Center(CGPoint origin,CGSize thumbSize) {
return CGPointMake(origin.x + thumbSize.width / 2, origin.y + thumbSize.height / 2);
}
///Point转value
static inline NSValue * valueOfPoint(CGPoint point) {
return [NSValue valueWithCGPoint:point];
}
///默认成功动画
static inline CAAnimation * defaultSuccessAnimaiton(id<CAAnimationDelegate> delegate) {
CABasicAnimation * animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
animation.duration = 0.2;
animation.autoreverses = YES;
animation.fromValue = @1;
animation.toValue = @0;
animation.removedOnCompletion = YES;
animation.delegate = delegate;
return animation;
}
///默认失败动画
static inline CAAnimation * defaultFailAnimation(id<CAAnimationDelegate> delegate) {
DWSlideCaptchaView * captcha = (DWSlideCaptchaView *)delegate;
CAKeyframeAnimation * animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
CGFloat a = 3;
CGPoint Cp = captcha.thumbLayer.position;
CGPoint Lp = CGPointMake(Cp.x - a, Cp.y);
CGPoint Rp = CGPointMake(Cp.x + a, Cp.y);
animation.values = @[valueOfPoint(Cp),valueOfPoint(Lp),valueOfPoint(Rp),valueOfPoint(Cp)];
animation.repeatCount = 2;
animation.removedOnCompletion = YES;
animation.duration = 0.2;
animation.delegate = captcha;
return animation;
}
#pragma mark --- setter/getter ---
-(CAShapeLayer *)positionLayer {
if (!_positionLayer) {
_positionLayer = [CAShapeLayer layer];
}
return _positionLayer;
}
-(CAShapeLayer *)thumbLayer {
if (!_thumbLayer) {
_thumbLayer = [CAShapeLayer layer];
}
return _thumbLayer;
}
-(void)setThumbShape:(UIBezierPath *)thumbShape {
SafeConfiguration
CGSize size = thumbShape.bounds.size;
if (!(size.width >= 40 && size.height >= 40)) {
NSAssert(NO, @"To get a better experience,the width and height of thumbShape both should be more than 40.");
return;
}
_thumbShape = thumbShape;
_thumbSize = size;
}
-(UIBezierPath *)thumbShape {
if (!_thumbShape) {
return puzzlePath();
}
return _thumbShape;
}
-(void)setTargetValue:(CGFloat)targetValue {
SafeConfiguration
_targetValue = fixValueWithLimit(targetValue, 1);
}
-(void)setThumbCenterY:(CGFloat)thumbCenterY {
SafeConfiguration
_thumbCenterY = thumbCenterY;
}
-(void)setUseRandomValue:(BOOL)useRandomValue {
SafeConfiguration
_useRandomValue = useRandomValue;
}
-(void)setTolerance:(CGFloat)tolerance {
SafeConfiguration
_tolerance = tolerance;
}
-(CGFloat)tolerance {
if (_tolerance < 0) {
return 3;
}
return _tolerance;
}
-(void)setSuccessAnimation:(CAAnimation *)successAnimation {
SafeConfiguration
_successAnimation = successAnimation;
_successAnimation.delegate = self;
}
-(void)setFailAnimation:(CAAnimation *)failAnimation {
SafeConfiguration
_failAnimation = failAnimation;
_failAnimation.delegate = self;
}
-(void)setBgImage:(UIImage *)bgImage {
SafeConfiguration
if (bgImage) {
_bgImage = [bgImage dw_RescaleImageToSize:self.frame.size];
} else {
_bgImage = nil;
}
}
-(void)setThumbSize:(CGSize)thumbSize {
SafeConfiguration
if (!CGSizeEqualToSize(_thumbSize, thumbSize)) {
_thumbSize = thumbSize;
}
}
-(CGPoint)targetPoint {
if (!self.resetTargetPoint) {
return _targetPoint;
}
self.resetTargetPoint = NO;
if (self.useRandomValue) {
_targetPoint = randomPointInSize(self.validSize);
return _targetPoint;
}
CGFloat x = (self.targetValue != DWSlideCaptchaUndefineValue) ? self.targetValue : randomValueInLength((int)self.validSize.width);
CGFloat y = (self.thumbCenterY != DWSlideCaptchaUndefineValue) ? fixCenterYWithSize(self.thumbSize, self.validSize, self.thumbCenterY) : randomValueInLength((int)self.validSize.height);
_targetPoint = CGPointMake(x, y);
return _targetPoint;
}
-(CGSize)validSize {
return CGSizeMake(self.bounds.size.width - self.thumbSize.width, self.bounds.size.height - self.thumbSize.height);
}
恩,这个库老司机不仅上传到GitHub上面,还让他支持了Cocoapods,你可以通过 pod 'DWSlideCaptchaView', '~> 1.0.2'
来集成这个视图,觉得好请Star。
说到这里老司机说一下,现在你可以通过pod search wicky
命令搜索老司机上传到Cocoapods上面的类库,目前有三个:
老司机这里还是主推DWCoreTextLabel,他是一个基于CoreText的异步绘制的图文混排控件,并且支持图片的异步加载与缓存,基本上可以完美的实现图文混排需求。你值得使用。
DWCheckBox就是单选复选框了,也是一个快捷使用并且有着高定制型的类库。
继承UIControl重新实现一个Slider
广告打完了咱们来看第二环节,slider。
最初的时候其实我就是想实现后面那个步进Slider,最初的想法继承UISlider去重写,奈何转了一大圈,各种私有属性用一遍也无法完美的完成我的需求。
主要是由于UISlider中对于滑块和滑竿的定制性很困难,所以自己重写一个Slider吧。
所以为什么想到继承自UIControl去写呢?第一是UISlider继承自UIControl,第二是UIControl封装了-addTarget:selector:events
以及事件追踪的一系列方法。
其实UIControl有四个核心的方法,是用于控制事件追踪的。
///判断是否开始事件追踪
-(BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
///判断事件追踪是否继续
-(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
///事件追踪取消时处理
-(void)cancelTrackingWithEvent:(UIEvent *)event;
///事件追踪结束时处理
-(void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
使用方法无非就是判断当视图接收到事件是如何追踪,可以看一下老司机写Slider的处理。
#pragma mark --- tracking Method ---
-(BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint location = [touch locationInView:self];
location = [self.thumbLayer convertPoint:location fromLayer:self.layer];
if ([PathWithBounds(self.thumbLayer.bounds, FitCornerRadius(self.thumbLayer, self.thumbCornerRadius)) containsPoint:location]) {
self.clickOnThumb = YES;
return YES;
}
location = [self.trackBgLayer convertPoint:location fromLayer:self.thumbLayer];
if ([PathWithBounds(self.trackBgLayer.bounds, FitCornerRadius(self.trackBgLayer, self.trackCornerRadius)) containsPoint:location]) {
self.clickOnThumb = NO;
return YES;
}
return NO;
}
-(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint location = [touch locationInView:self];
CGFloat margin = FitMarginForThumb(self.thumbSize, [self thumbMarginForBounds:self.bounds]);
location.x -= margin;
CGFloat actualW = CGRectGetWidth([self trackRectForBounds:self.bounds]) - margin * 2;
if (location.x < 0) {
location.x = 0;
} else if (location.x > actualW) {
location.x = actualW;
}
CGFloat percent = location.x / actualW;
CGFloat value = self.minimumValue + (self.maximumValue - self.minimumValue) * percent;
if (value == self.value) {
return YES;
}
_value = value;
[self sendActionsForControlEvents:UIControlEventValueChanged];
if (self.clickOnThumb) {
[self updateValueAnimated:NO];
return YES;
} else {
[self updateValueAnimated:YES];
self.clickOnThumb = NO;
return NO;
}
}
-(void)cancelTrackingWithEvent:(UIEvent *)event {
self.clickOnThumb = NO;
}
-(void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
self.clickOnThumb = NO;
}
处理过事件追踪,我们只要处理好视图相关的内容即可。
此处可以分为两种思路,一种是通过DrawRect方法去追踪行为后不断绘制,另一种是通过Layer展示各个图层并追踪行为。这里呢,老司机更加推荐使用Layer去处理图层,因为本身DrawRect方法中的代码是使用CPU进行预算然后将bitmap提交给GPU,他处理绘制的速度远不如CALayer直接使用GPU来的快。
图层的绘制老司机在CoreAnimation系列中已经写得很细了,在这也就不多写了。
老司机重写的DWSlider是一个UISlider的替换类,它具备UISlider的所有功能,并且还能自由定制你的Slider的各个属性,相比UISlider来讲可玩性更强,老司机这里放一个传送门。
步进Slider
DWStepSlider是一个分段的Slider,继承自DWSlider。
主要是实现分段的Slider至实现,主要思想还是通过更改事件追踪后的赋值。
#pragma mark --- tracking Method ---
-(BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint location = [touch locationInView:self];
location = [self.thumbLayer convertPoint:location fromLayer:self.layer];
if ([PathWithBounds(self.thumbLayer.bounds, FitCornerRadius(self.thumbLayer, self.thumbCornerRadius)) containsPoint:location]) {
self.clickOnThumb = YES;
return YES;
}
location = [self.trackBgLayer convertPoint:location fromLayer:self.thumbLayer];
if ([PathWithBounds(self.trackBgLayer.bounds, FitCornerRadius(self.trackBgLayer, self.trackCornerRadius)) containsPoint:location]) {
self.clickOnThumb = NO;
return YES;
}
return NO;
}
-(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint location = [touch locationInView:self];
CGFloat margin = FitMarginForThumb(self.thumbSize, [self thumbMarginForBounds:self.bounds]);
location.x -= margin;
CGFloat actualW = CGRectGetWidth([self trackRectForBounds:self.bounds]) - margin * 2;
if (location.x < 0) {
location.x = 0;
} else if (location.x > actualW) {
location.x = actualW;
}
CGFloat percent = location.x / actualW;
CGFloat value = self.minimumValue + (self.maximumValue - self.minimumValue) * percent;
if (value == self.value) {
return YES;
}
[self setValue:value updateThumb:NO];
[self sendActionsForControlEvents:UIControlEventValueChanged];
if (self.clickOnThumb) {
[self updateValueAnimated:NO];
return YES;
} else {
[self setValue:FixValue(value, _nodes.count) updateThumb:NO];
[self updateValueAnimated:YES];
return NO;
}
}
-(void)cancelTrackingWithEvent:(UIEvent *)event {
if (self.clickOnThumb) {
self.value = FixValue(self.value, _nodes.count);
self.clickOnThumb = NO;
}
}
-(void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
if (self.clickOnThumb) {
self.value = FixValue(self.value, _nodes.count);
self.clickOnThumb = NO;
}
}
至于图形还是CAShapeLayer的各种形状,老司机也早就说过了,还是传送门吧。
好吧,今天其实也没什么新鲜内容,毕竟都是一些UI控件的封装。
不过也是捋一下思路,控件要如何封装,所以还是不要脸的发出来了。
喜欢哪个给哪个Star吧恩,就是这么好意思