我是靠谱客的博主 称心金鱼,这篇文章主要介绍php仿小红书,【仿小红书】为图片加上带动效的小标签,现在分享给大家,希望可以做个参考。

前言

最近项目中需要模仿小红书中发笔记的相关功能,其中允许用户对图片打标签的功能是其中的重点,下面是实现这个功能的一些思路。

af17e51388d8

小红书打标签功能

功能需求分析

把玩了一下小红书,总结了一下这个打标签功能的一些需求。

点击图片弹出输入框,输入标签信息后在点击位置生成一个标签。

点击标签上的小圆点可以切换不同样式,分别有左右两个方向,直线和斜线两种样式,标签数量1~3个,最多一共12种样式。

点击标签上的文字可以对标签内容进行编辑。

长按标签任意部分可以删除标签。

拖动标签任意部分可以移动标签的位置。

在不可编辑的状态看下,点击图片可以隐藏/显示所有标签。

思路

有了需求,下面就逐个来分析实现。

1、比例坐标、ViewModel

首先这个小标签需要响应触摸事件,所以打算以继承UIView的方式来实现它,显然这个标签由原点、线条、文本等部件组成。由于上面的第一个需求中在创建一个视图时,需要确定这个标签视图的位置,考虑到图片本身是有可能缩放来适配不同尺寸设备的,所以标签视图的位置可以用比例坐标来表示,即坐标的取值为0~1,最后乘以父视图的宽高得到父视图坐标系中的准确坐标。

af17e51388d8

使用比例坐标

同时也单独用一个TagViewModel来保存表示一个标签所需的数据,创建一个标签就是创建一个TagViewModel,标签视图只需要接受并处理这个ViewModel即可生成标签。

@interface TagViewModel : NSObject

//文本数组

@property (nonatomic, strong) NSMutableArray *tagModels;

//标签相对于父视图坐标系中的相对坐标,例如(0.5, 0.5)即代表位于父视图中心

@property (nonatomic, assign) CGPoint coordinate;

//样式

@property (nonatomic, assign) TagViewStyle style;

//顺序标志

@property (nonatomic, assign) NSUInteger index;

//初始化

- (instancetype)initWithArray:(NSArray *)tagModels coordinate:(CGPoint)coordinate;

//样式相关

- (void)resetStyle;

- (void)styleToggle;

- (void)synchronizeAngle;

@end

一个TagViewModel代表一个标签,一个标签中有可能含有几段文本,而且在绘制时需要用到相应样式中角度等数据,因此再定义一个TagModel表示一段文字。

@interface TagModel : NSObject

//文本

@property (nonatomic, copy) NSString *name;

@property (nonatomic, copy) NSString *value;

//角度

@property (nonatomic, assign) CGFloat angle;

//文本位置

@property (nonatomic, assign) CGSize textSize;

@property (nonatomic, assign) CGPoint textPosition;

//初始化

- (instancetype)initWithName:(NSString *)name value:(NSString *)value;

@end

2、 图层绘制

标签视图作为提供给外部的最小单位,而其内部的组件(文本、线条、原心)由于不需要响应事件,则选择用图层来绘制,这样做比用视图来绘制会稍微更高效。

一个标签视图主要由文本、线条、圆心组成,可以使用CATextLayer和CAShapeLayer来实现,结构参考下面的示意图。绘制的步骤是首先确定标签视图的宽高,视图高度由斜线半径与文本的高度决定,而视图宽度主要由斜线半径与文本的宽度决定。

af17e51388d8

灰色区域为文本区域

然后就可以在这个确定宽高的视图中分别画出圆心、文本下的下划线图层,最后根据下划线定位文本图层的位置。其中斜线的绘制方法是根据TagModel中的角度和斜线的半径用三角函数算出起点(圆心)和终点(圆上的一点)的坐标画直线得到。

3、事件响应链

画出了图层,接下来处理触摸。在需求的第二到第五点中,都涉及到事件响应链。由于需要处理不同类型的触摸事件,在这里我用了UIGestureRecognize,给视图添加了点击、长按、滑动手势。当一个标签视图接收到点击事件时,它需要判断自己是否得处理这个点击事件,例如单击了原点区域、长按了文本区域或者只是点到了视图中的空白区域。所以在标签视图类中需要重写UIView的-pointInside: withEvent:方法和-hitTest: withEvent:方法,前者判断事件的点是否落在本视图内,后者用于向上级视图返回需要接受这个事件的视图是什么——视图自己本身、子视图或者nil。

af17e51388d8

事件响应链

代码中在-pointInside: withEvent:方法中判断点是否落在了圆心或者文本上:

//标签视图没有子视图,相当于结果由piontInside:withEvent:方法决定

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

{

UIView *view = [super hitTest:point withEvent:event];

return view;

}

//重写父类方法

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

{

if(![self centerContainsPoint:point inset:0] && ![self textLayerContainsPoint:point inset:CGPointMake(-5, -5)]){

return NO;

}

return [super pointInside:point withEvent:event];

}

//判断position是否在圆心区域内

- (BOOL)centerContainsPoint:(CGPoint)position inset:(CGFloat)insetRadius

{

CGPoint centerPosition = CGPointMake(self.layer.bounds.size.width/2, self.layer.bounds.size.height/2);

UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:centerPosition radius:kUnderLineLayerRadius+insetRadius startAngle:0 endAngle:M_PI*2 clockwise:YES];

return [path containsPoint:position];

}

//点position是否在某一个textLayer内

- (BOOL)textLayerContainsPoint:(CGPoint)point inset:(CGPoint)insetXY

{

BOOL cantainsPoint = NO;

for(CATextLayer *textLayer in _textLayers){

if(textLayer.presentationLayer.opacity == 0){

continue;

}

CGRect textRect = CGRectInset(textLayer.frame, insetXY.x, insetXY.y);

if(CGRectContainsPoint(textRect, point)){

cantainsPoint = YES;

break;

}

}

return cantainsPoint;

}

如果最后判断标签视图需要处理这个触摸时间,只需要用一开始给这个标签视图加上的各个手势,配合判断点是否在圆心和文本的方法,即可实现需求中第二到第五点的功能。

4、图层动画

在需求的第二点和第六点中,需要以动画的方式切换标签视图的样式和显示隐藏标签。动画的思路是同时给需要动画的图层添加CAAnimation,可以用CAKeyFrameAnimation或者CABasicAnimation的beginTime属性实现动画的先后次序,并用CAAnimationTransaction来做一些动画后的处理。

af17e51388d8

效果图

注意

本身在CAAnimation中对图层的可动画属性直接赋值,就会产生默认的动画,但这个动画没法进行自定义配置,像刚才提到的不同属性的先后次序就没法实现,直接修改这些图层的属性值会让动画一齐执行。例如上图中的线条是一个CAShapeLayer,代码layer.strokeEnd = 0就已经可以产生线条收回的动画,但没法让文字消失后,这个动画才发生,所以只好换个方式实现。

隐藏动画的代码:

- (void)hideWithAnimate:(BOOL)animate

{

if(_viewHidden || _animating){

return;

}

_animating = YES;

CGFloat duration = 1.f;

[self animateWithDuration:duration*3 AnimationBlock:^{

NSTimeInterval currentTime = CACurrentMediaTime();

//原点

CABasicAnimation *animation = [CABasicAnimation animation];

animation.beginTime = currentTime+duration*2;

animation.duration = duration;

animation.keyPath = @"opacity";

animation.removedOnCompletion = NO;

animation.fillMode = kCAFillModeBoth;

animation.fromValue = @1;

animation.toValue = @0;

[_centerPointShapeLayer addAnimation:animation forKey:kAnimationKeyShow];

animation.fromValue = @0.3;

[_shadowPointShapeLayer addAnimation:animation forKey:kAnimationKeyShow];

//下划线

CABasicAnimation *lineAnimation = [CABasicAnimation animation];

lineAnimation.beginTime = currentTime+duration;

lineAnimation.duration = duration;

lineAnimation.keyPath = @"strokeEnd";

lineAnimation.removedOnCompletion = NO;

lineAnimation.fillMode = kCAFillModeBoth;

lineAnimation.fromValue = @1;

lineAnimation.toValue = @0;

for(CAShapeLayer *shapeLayer in _underLineLayers){

[shapeLayer addAnimation:lineAnimation forKey:kAnimationKeyShow];

}

//文字

CABasicAnimation *textAnimation = [CABasicAnimation animation];

textAnimation.beginTime = 0;

textAnimation.duration = duration;

textAnimation.keyPath = @"opacity";

textAnimation.removedOnCompletion = NO;

textAnimation.fillMode = kCAFillModeBoth;

textAnimation.fromValue = @1;

textAnimation.toValue = @0;

for(CATextLayer *textLayer in _textLayers){

[textLayer addAnimation:textAnimation forKey:kAnimationKeyShow];

}

} completeBlock:^{

_animating = NO;

_viewHidden = YES;

}];

}

- (void)animateWithDuration:(CGFloat)duration

AnimationBlock:(void(^)())doBlock

completeBlock:(void(^)())completeBlock

{

[CATransaction begin];

[CATransaction setDisableActions:NO];

[CATransaction setAnimationDuration:duration];

[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];

[CATransaction setCompletionBlock:^{

[CATransaction begin];

[CATransaction setDisableActions:YES];

if(completeBlock){

completeBlock();

}

[CATransaction commit];

}];

if(doBlock){

doBlock();

}

[CATransaction commit];

}

指定时间开始动画

animation.beginTime = CACurrentMediaTime()+duration*2 ;可以让动画在指定时间后才开始.

animation.fillMode

这是一个很有意思的属性,它的可选值定义在CAMediaTiming.h中

CA_EXTERN NSString * const kCAFillModeForwards

CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

CA_EXTERN NSString * const kCAFillModeBackwards

CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

CA_EXTERN NSString * const kCAFillModeBoth

CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

CA_EXTERN NSString * const kCAFillModeRemoved

CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);

默认情况下当给一个layer添加CAAnimation后,动画时实际上改变的是layer.presentationLayer中的属性,动画让layer得以在呈现层(presentation)上做动画,当动画完成后,动画会从layer上移除,在没有改变layer.modelLayer的属性下,layer也会在presentation上被移除。而如果设置了animation.rmovedOnCompletion = NO;,动画在完成后就不会从layer上移出,并根据fillMode属性决定如何显示layer。

kCAFillModeForwards//直到动画开始时layer都隐藏,动画完成后保持动画最后的状态

kCAFillModeBackwards//直到动画开始时layer都保持当前的状态,动画完成后移出presentation

kCAFillModeBoth//上面两者合体,直到动画开始时layer都保持当前的状态,动画完成后保持动画最后的状态

kCAFillModeRemoved//直到动画开始时layer都隐藏,动画完成后移出presentation

CATransaction

用[CATransaction begin]开始一个动画事务,并可以在完成时执行自己的代码。另外在CATransaction中可以组合多个CAAnimation,也可以嵌套CATransaction。

5、标签样式切换

在绘制图层时,主要靠每个文本(TagModel)上的角度属性来画不同方向的线条,改变标签的样式实际上就是改变文本的角度,所以实现样式切换这个功能需要做的就是配置一个不同样式、不同文本数量下对应的文本角度的配置文件。

在这里选择在plist中写好相关的样式角度,在ViewTagModel中拿自己本身的标签数据与plist中的样式数据做一个匹配,以此来确认当前的标签样式。切换样式时只需要直接改变样式,再根据样式到plist中查找对应的角度并赋值就ok了。这里的plist可以用json代替,思路是一样的,使用这种方式,可以让app访问服务器获取最新的样式来随时改变样式配置。

#pragma mark - 判断当前style

//根据标签数量进行判断

- (void)resetStyle

{

NSInteger count = _tagModels.count;

if(count == 0){

NSLog(@"_tagModels.count = 0");

return;

}

//根据标签条数拿出对应的样式数据

NSDictionary *countStyleDict = styleDict[[NSString stringWithFormat:@"%@", @(count)]];

if(!countStyleDict){

NSLog(@"styleDict not found");

return;

}

//allKeys为所有TagViewStyle

NSArray *allKeys = [countStyleDict allKeys];

//遍历TagViewStyle

for(NSInteger i=0; i

NSString *styleStr = allKeys[i];

//以此为key拿出对应style的角度

NSArray *styleArray = countStyleDict[styleStr];

if(styleArray.count == 0){

//没有角度数据

continue;

}

//无论有多少条标签,这里都只判断了第一条标签的角度

//可以考虑改为验证所有标签的角度来判断数据的合法性

NSNumber *angleNumber = (NSNumber*)styleArray[0];

if(_tagModels[0].angle == [angleNumber floatValue]){

_style = [styleStr integerValue];

NSLog(@"_style reset:%@", @(_style));

return;

}

}

}

#pragma mark - 切换当前style

- (void)styleToggle

{

//切换

_style = (_style+1)%maxStyle;

[self synchronizeAngle];

}

#pragma mark - 根据当前style更新角度

- (void)synchronizeAngle

{

NSInteger count = _tagModels.count;

if(count == 0){

NSLog(@"_tagModels.count = 0");

return;

}

//根据标签条数拿出对应的样式数据

NSDictionary *countStyleDict = styleDict[[NSString stringWithFormat:@"%@", @(count)]];

if(!countStyleDict){

NSLog(@"styleDict not found");

return;

}

//根据样式拿出角度数据数组

NSArray *styleArray = countStyleDict[[NSString stringWithFormat:@"%@", @(_style)]];

if(styleArray.count < _tagModels.count){

NSLog(@"styleArray doesn't long enough");

return;

}

//更新角度

for(NSInteger i=0; i<_tagmodels.count i>

NSNumber *angleNumber = (NSNumber*)styleArray[i];

_tagModels[i].angle = [angleNumber floatValue];

}

}

af17e51388d8

样式配置文件

这个小标签功能的介绍就到此结束,demo有空再上传。

2017.06.06更新:

抱歉这么久才更新demo,最近有点忙,没什么时间打理这里,希望还能帮上评论区的朋友。

https://github.com/Tidusww/CommonDemo.git

参考资料

最后

以上就是称心金鱼最近收集整理的关于php仿小红书,【仿小红书】为图片加上带动效的小标签的全部内容,更多相关php仿小红书,【仿小红书】为图片加上带动效内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(45)

评论列表共有 0 条评论

立即
投稿
返回
顶部