效果直接放这了,先看效果再上菜。
?
目录:
一.功能实现说明
二.实现效果核心代码片段
三.拓展思考方面细节
一.功能实现说明
1.遇到的问题说明 不知大家有没有遇到这样的功能,即如开篇的图所示,在一个输入框中去实现放按钮的效果,也就是在同一个输入框无论是TextFeild还是TextView中,支持放按钮,也支持通过输入信息去搜索的功能实现。
2.对该问题的思考历程并抛出新问题 遇到这样的问题,首先我们的解决方案有2个,1个是把输入框作为假输入框,即按钮是真的,输入框中的要输入的光标是假的; 还有一个方案是把按钮做成假的,输入框是真的,然后把假按钮放入输入框内,即可。 我的思考是,如果采用方案1,首先如果输入框是固定不动的话,那么做一个光标闪一下现一下的动画,然后做一个自定义键盘配合监听其代理方法,即可解决这个问题。但是,光标可能会移动,且随时插入按钮之间,此时该方案就很明显要被pass掉了。
3.解决问题采取的方案 那么,如果采用方案二,假按钮从何而来。答案不言自明,即富文本中来操作。因为作为一个按钮要实现点击和点击后不同的效果,则可以用富文本的 NSLinkAttributeName属性,来设置不同颜色从而实现按钮的点击效果。
好了,方案通过思考并验证后,那么如何实现呢,接下来我们接着说说这块的思考。
二.实现效果核心代码片段
1.整体框架说明实现 首先,先来介绍说明一下整体实现该方案的框架,主要涉及2个管理类,即DDRTSelectedManager 和 DDRTGroupItemManager。 其中DDRTSelectedManager主要实现的是在前一个页面中列表多选的功能,其实与本节富文本研究是作为配角而存在; 而DDRTGroupItemManager则是实现本节中TextView中富文本点击事件的核心管理类。 接下来,我们逐个来说说!
?2.扩展选择实现类逻辑 首先,先来说说DDRTSelectedManager中主要实现的是前面页面选项卡选择的问题,即类似于购物车的问题。 如下代码所示:
// 获取数据源的方法 -(void)initAllDatas;
// 更新某一行选中状态 -(NSMutableArray *)updateItemManager:(NSIndexPath *)indexPath;
// 遍历到所有选中的数据 -(NSMutableArray *)getAllSelectedDatas;
主要目标是通过数据源来解决TableViewCell的重用问题,只是通过改变数据源来实现列表的及时刷新实现。
3.Model包装类的实现
在Model包装类中的实现方式,是通过在Model普通属性之外,定义如下所示的属性
// 当前字符所处字符串实际开始位置 @property(nonatomic,assign)NSInteger nameLocation; // 名字的实际长度 @property(nonatomic,assign)NSInteger nameLength; // 名字的实际长度 + 3(2个空格 + 1个逗号) @property(nonatomic,assign)NSInteger nameAppendAfterLength;
// 是否被点击,即将删除的情形 @property(nonatomic,assign)BOOL isClick;
从而实现所有群组数组中记录的Mode中当前的name字段拼接后其所处的字符串中的长度和位置,以方便后面渲染到页面中可以精确控制每个假按钮的分隔点。
???????
4.群组成员按钮管理类的实现 很明显群组成员按钮管理类是本次实现功能的核心代码,如下所示, 4.1.首先我们先来分析一下,在该管理类中实现的几个接口方法。
// 选中的状态 -(void)selectedItemState:(DDRTGroupMemberModel *)model; // 未选中的状态 -(void)cancleItemState:(DDRTGroupMemberModel *)model;
-(void)addGroupItem:(DDRTGroupMemberModel *)model;
-(void)deleteGroupItem:(DDRTGroupMemberModel *)model;
// 返回表示有没有可删除的内容,没有的话,把最后一个选中,并返回NO,默认返回YES -(BOOL)deleteSelectedGroupArr;
// 根据传入的下标,获得到对应数组的位置 -(NSInteger)indexOfGroupLoctaion:(NSInteger)location;
// 获取被处理过的富文本字符串 -(NSMutableAttributedString *)getAttributeAllMemberStr;
// 第一次进来时,更新所有数据源的点击状态为普通状态 -(void)updateAllItemStateToNomal;
4.2.富文本字符串处理的细节核心代码 这里每次更新成员数组状态,如个数,群成员按钮状态时,都要调用该方法,去重新获取被渲染后的富文本。 图中主要注意2点,即第一点是要区分按钮的点击位置和实际所占用位置的不同,因为具体实现时,按钮和按钮之间还会有其他字符作为分隔;以及最后一个按钮实现时是没有字符分隔的。 第二点是对按钮的2种状态进行区分显示的逻辑。
// 获取被处理过的富文本字符串 -(NSMutableAttributedString *)getAttributeAllMemberStr { ? ?? ? ? // 清空字符串 ? ? [self.allGroupMemberStr replaceCharactersInRange:NSMakeRange(0, self.allGroupMemberStr.mutableString.length) withString:@""]; ? ?? ? ? if (self.groupMemberArr && self.groupMemberArr.count) { ? ? ? ? NSInteger totalLocation = 0; ? ? ? ? for (int index = 0; index < self.groupMemberArr.count; index ++) { ? ? ? ? ? ? DDRTGroupMemberModel *memberModel = self.groupMemberArr[index]; ? ? ? ? ? ? memberModel.nameLocation = totalLocation; ? ? ? ? ? ? NSString *nameStr; ? ? ? ? ? ? NSMutableAttributedString *attrStr; ? ? ? ? ? ? BOOL isLast = NO; ? ? ? ? ? ? if (index == self.groupMemberArr.count - 1) {// 最后一位的话,不拼接对应的字符串 ? ? ? ? ? ? ? ? nameStr = memberModel.personName; ? ? ? ? ? ? ? ? isLast = YES; ? ? ? ? ? ? } ? ? ? ? ? ? else { ? ? ? ? ? ? ? ?? ? ? ? ? ? ? ? ? nameStr = [NSString stringWithFormat:@"%@%@",memberModel.personName,kAppendStrSign]; ? ? ? ? ? ? ? ? isLast = NO; ? ? ? ? ? ? } ? ? ? ? ? ? totalLocation = totalLocation + nameStr.length; ? ? ? ? ? ?? ? ? ? ? ? ?? ? ? ? ? ? ? if (memberModel.isClick == YES) { ? ? ? ? ? ? ? ? attrStr = [self getSubStringClick:nameStr andFont:kRichTextViewFont andIsLast:isLast]; ? ? ? ? ? ? } ? ? ? ? ? ? else { ? ? ? ? ? ? ? ? attrStr = [self getSubStringNormal:nameStr andFont:kRichTextViewFont andIsLast:isLast]; ? ? ? ? ? ? } ? ? ? ? ? ?? ? ? ? ? ? ? [self.allGroupMemberStr appendAttributedString:attrStr];
? ? ? ? } ? ? } ? ? return self.allGroupMemberStr; }
这样一解释后,大家是不是明显清晰多了哈!
4.3.如何监听到点击点击位置改变其按钮状态 如下代码所示,在textView点击链接的代理方法中,通过点击传入点击位置,如下的location, 到我们的DDRTGroupItemManager 中的indexOfGroupLoctaion方法获得到其点击位置在群成员
数组中的下标,然后去更改对应的富文本字符串即可。
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange { ? ? DDLog(@"%lu ------ %lu",(unsigned long)characterRange.location,(unsigned long)characterRange.length); ? ? if (textView == self.groupTextView) { ? ? ? ? NSInteger location = [[NSNumber numberWithUnsignedInteger:characterRange.location] integerValue]; ? ? ? ? NSInteger clickIndex = [self.groupItemManager indexOfGroupLoctaion:location]; ? ? ? ? if (clickIndex < self.groupItemManager.groupMemberArr.count) { ? ? ? ? ? ? DDRTGroupMemberModel *model = self.groupItemManager.groupMemberArr[clickIndex]; ? ? ? ? ? ? if (model.isClick == YES) { ? ? ? ? ? ? ? ? [self.groupItemManager cancleItemState:model]; ? ? ? ? ? ? } ? ? ? ? ? ? else { ? ? ? ? ? ? ? ? [self.groupItemManager selectedItemState:model]; ? ? ? ? ? ? } ? ? ? ? ? ? self.groupTextView.attributedText = [self.groupItemManager getAttributeAllMemberStr]; ? ? ? ? ? ?? ? ? ? ? } ? ? ? ?? ? ? } ?? ? ? return YES; }?
indexOfGroupLoctaion方法实现如下
?NSInteger nowIndex = 0; ? ? if (self.groupMemberArr && self.groupMemberArr.count) { ? ? ? ? for (int index = 0; index < self.groupMemberArr.count; index ++ ) { ? ? ? ? ? ? DDRTGroupMemberModel *model = self.groupMemberArr[index]; ? ? ? ? ? ? if (model.nameLocation >= location) { ? ? ? ? ? ? ? ? nowIndex = index; ? ? ? ? ? ? ? ? break; ? ? ? ? ? ? } ? ? ? ? } ? ? } ? ? return nowIndex;
这么多方法看下来,注释写的也很明白,其实核心点主要是对每个成员选项卡按钮的增删改查工作,以及准确定位到当前选择的成员的功能实现。 具体里面内容怎么实现呢,详情可看本文末尾处的连接。 原创不易,如果帮助到了朋友们,欢迎star哈!
5.实现的相关功能汇总 这里就对该上面4里面所涉及的所有按钮功能进行一个简要说明: 5.1.在群组中的每个成员作为一个假按钮处于TextView控件中展示; 5.2.当用户点击对应按钮时,选中取消选中功能实现 5.3.当用户点击最后一个按钮末尾处,可看到光标显示(但目前无法输入文字)。此时点击键盘删除图标时,倘若有按钮被选中,则选中的按钮全部倍删除,未选中的按钮位置依次向左移动;倘若按钮没有被选中,则默认点击一次删除图标,最后一个按钮被选中,再点击一次删除图标删除最后一个按钮。 5.4.用户可以在成员列表界面选择对应的成员后跳转到富文本成员页面。而在富文本成员页面中删除对应的成员后,点击返回或者右侧的添加按钮,其成员列表中该成员也被删除。 即实现成员列表和富文本成员页面的实时更新功能。
三.拓展思考方面细节
1.解题思维说明 首先,需要说明的是,完成对本篇富文本功能实现流程后,我们会对底层的UI如按钮,Label等实现有了更清晰的理解。更接近其底层实现原理。 另外一层的思考是对于Model管理类的理解需要进一步加深,即其主要处理的不仅仅是与数据有关,也有很多业务逻辑。特别是如同本文所示的数据之间层层嵌套,且层与层之间还略有不同,如按钮有2种状态,最后一个按钮的没有分隔符,每个按钮的点击范围和渲染范围不同等等逻辑时。 我们应转变思路,通过对Model的共同数据可以处理,那么不同的数据也可以通过Model来记录。之后再配合数据源管理类来实现数据的改变,然后让底层UI去根据数据改变刷新为我们需要的UI的思考逻辑。
2.回到基本面向对象突围 对于面向对象的理解,我们需要进一步突围发展,扩张基本面。即面向对象是在我们解题过程中必备的思想,是需要我们深入其核心去理解。 比如像本篇中的面向Model和面向2个管理类的处理逻辑。每一个管理类去管理对应的分工。面向自己所解决的对象,去实现对应的方法。 而我们的控制器呢,只是拿其优势为其所用,最终实现一个个功能块。 功能块就是我们的面向对象的对象,那么我们的面向对象需要拆分给其他擅长该类的管理者,Model,帮助类,第三方库等去实现,这就是我们的面向对象思想。 好了,本篇废话有点小多,最后呢附上本文代码的仓库地址:
[[我的富文本之DDRichTextDemo](https://gitee.com/httpfdajkfihdakdjhd/ddrich-text-demo) 以及上一篇关于富文本功能实现的代码地址:[iOS富文本实现(一):私密阅读效果](CSDN)
欢迎大家评论区交流哦!
|