• iOS 简易型标签的实现(UICollectionView)


    https://blog.csdn.net/sinat_39362502/article/details/80900984

    2018年07月03日 16:49:05 Recorder_MZou 阅读数:2079

    接到一个需求就是要实现标签组的显示和选择,如下图所示:

    pastedGraphic.png

    一开始感觉没有什么头绪,参考网上各种demo,发现大部分的demo都是以自绘制标签为主实现标签的长度计算和自动换行,但是这样需要实现的计算量就非常大,对于一部分参考和后期维护起来就非常麻烦,稍微修改错一个参数,导致计算不准确,这就不太好实现。

    但是想了一下我们常用的系统控件中,是否有相关的控件可以实现呢?第一个想法就让我想到了UICollectionView,既然UICollectionView能实现瀑布流,为什么标签这种无规则的界面不能实现呢?,于是就开始初步搭建:

    首先,先了解在UICollectionView中如何能或者每一个cell的大小,想到我们常用的UITableView的操作,其实两者的用法基本也是一样的,所以在UICollectionViewDelegate中就有



    - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {

        NSString * containString = @"你想知道怎么实现宽度计算吗?";

        CGFloat width = [NSString getWidthWithText:containString height:30 font:13];

        return CGSizeMake(width + 25, 30.0f);

    }

    这个方法能手动设置cell的大小。

    既然能手动设置cell的大小了,那这样实现起来就容易了,按照正常的流程设置UICollectionView的数据源方法和相关的代理方法即可。

    接下来的就是一个封装好的类,即计算String的宽度:

    /**

     根据高度度求宽度

     @param text 计算的内容

     @param height 计算的高度

     @param font 字体大小

     @return 返回宽度

     */

    + (CGFloat)getWidthWithText:(NSString *)text height:(CGFloat)height font:(CGFloat)font

    {

        //加上判断,防止传nil等不符合的值,导致程序奔溃

        if (text == nil || [text isEqualToString:@""]){

            text = @"";

        }

        if (font <= 0){

            font = 13;

        }

        if (height < 0){

            height = 0;

        }

        CGRect rect = [text boundingRectWithSize:CGSizeMake(MAXFLOAT, height)

                                         options:NSStringDrawingUsesLineFragmentOrigin

                                      attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:font]}

                                         context:nil];

        return rect.size.width;

    }

    这样大概的设置,大概即可实现在UICollectionView中的每一个cell的大小,而最重要的一步就来了,如何设置UICollectionView的布局呢?,这个就是整个标签组最为重要的一部分。

    1.如何实现cell的布局位置要靠左对齐,并实现到屏幕最右边时能自动换行;

    2.如何实现每一个不同长度的cell的具体距离和到不会被UICollectionViewFlowLayout默认数据自动拉伸到屏幕平分呢;

    这个时候,我们就需要重写UICollectionViewFlowLayout,首先要获取相关容器的宽度,或者每一个cell的宽度,然后通过计算每一个行cell的宽度和间隙相加的和是否大于容器的宽度,如果大于,即可更换其行数,以下就是 .m文件的直接代码展示(参考来源 @Giovanni Lodi):

    @interface UICollectionViewLayoutAttributes (LeftAligned)

    - (void)leftAlignFrameWithSectionInset:(UIEdgeInsets)sectionInset;

    @end

    @implementation UICollectionViewLayoutAttributes (LeftAligned)

    - (void)leftAlignFrameWithSectionInset:(UIEdgeInsets)sectionInset

    {

        CGRect frame = self.frame;

        frame.origin.x = sectionInset.left;

        self.frame = frame;

    }

    @end

    #pragma mark -

    @implementation UICollectionViewLeftAlignedLayout

    #pragma mark - UICollectionViewLayout

    - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

        NSArray *originalAttributes = [super layoutAttributesForElementsInRect:rect];

        NSMutableArray *updatedAttributes = [NSMutableArray arrayWithArray:originalAttributes];

        for (UICollectionViewLayoutAttributes *attributes in originalAttributes) {

            if (!attributes.representedElementKind) {

                NSUInteger index = [updatedAttributes indexOfObject:attributes];

                updatedAttributes[index] = [self layoutAttributesForItemAtIndexPath:attributes.indexPath];

            }

        }

        return updatedAttributes;

    }

    - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {

        UICollectionViewLayoutAttributes* currentItemAttributes = [[super layoutAttributesForItemAtIndexPath:indexPath] copy];

        UIEdgeInsets sectionInset = [self evaluatedSectionInsetForItemAtIndex:indexPath.section];

        BOOL isFirstItemInSection = indexPath.item == 0;

        CGFloat layoutWidth = CGRectGetWidth(self.collectionView.frame) - sectionInset.left - sectionInset.right;

        if (isFirstItemInSection) {

            [currentItemAttributes leftAlignFrameWithSectionInset:sectionInset];

            return currentItemAttributes;

        }

        NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];

        CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;

        CGFloat previousFrameRightPoint = previousFrame.origin.x + previousFrame.size.width;

        CGRect currentFrame = currentItemAttributes.frame;

        CGRect strecthedCurrentFrame = CGRectMake(sectionInset.left,

                                                  currentFrame.origin.y,

                                                  layoutWidth,

                                                  currentFrame.size.height);

        // if the current frame, once left aligned to the left and stretched to the full collection view

        // width intersects the previous frame then they are on the same line

        BOOL isFirstItemInRow = !CGRectIntersectsRect(previousFrame, strecthedCurrentFrame);

        if (isFirstItemInRow) {

            // make sure the first item on a line is left aligned

            [currentItemAttributes leftAlignFrameWithSectionInset:sectionInset];

            return currentItemAttributes;

        }

        CGRect frame = currentItemAttributes.frame;

        frame.origin.x = previousFrameRightPoint + [self evaluatedMinimumInteritemSpacingForSectionAtIndex:indexPath.section];

        currentItemAttributes.frame = frame;

        return currentItemAttributes;

    }

    - (CGFloat)evaluatedMinimumInteritemSpacingForSectionAtIndex:(NSInteger)sectionIndex

    {

        if ([self.collectionView.delegate respondsToSelector:@selector(collectionView:layout:minimumInteritemSpacingForSectionAtIndex:)]) {

            id<UICollectionViewDelegateLeftAlignedLayout> delegate = (id<UICollectionViewDelegateLeftAlignedLayout>)self.collectionView.delegate;

            return [delegate collectionView:self.collectionView layout:self minimumInteritemSpacingForSectionAtIndex:sectionIndex];

        } else {

            return self.minimumInteritemSpacing;

        }

    }

    - (UIEdgeInsets)evaluatedSectionInsetForItemAtIndex:(NSInteger)index

    {

        if ([self.collectionView.delegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)]) {

            id<UICollectionViewDelegateLeftAlignedLayout> delegate = (id<UICollectionViewDelegateLeftAlignedLayout>)self.collectionView.delegate;

            return [delegate collectionView:self.collectionView layout:self insetForSectionAtIndex:index];

        } else {

            return self.sectionInset;

        }

    }

    @end

    那么既然最困难的一步就是布局的问题已经解决了,那整体的页面显示已经完成了,那接下来就是数据的处理了,那要如何实现下图显示?

    10861439-72a5c17d5d8c92f1.gif

    标签选择.gif

    那首先要思考两个问题,如果通过对数据源的设置实现UICollectionView中每一组选中cell或者不选中cell,多选和单选的区别,此时我想到的一个方法就是给数据源一个属性,标识该cell是否选中:

    /**

     是否选中

     */

    @property (nonatomic, assign) BOOL isSelect;

    通过数据源的属性来控制cell内部选中的控件的颜色和状态

        [_tagBtn setTitle:model.name forState: UIControlStateNormal];

        UIColor *containStringColor = model.color == 0 ? tagBlueColor : model.color == 1 ? tagRedColor : model.color == 2 ? tagGreenColor : model.color == 3 ? tagYellowColor : model.color == 4 ? tagVioletColor : tagIndigoColor;

        if (model.isSelect == YES)

        {

            [_tagBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];

            _tagBtn.backgroundColor = containStringColor;

        }

        else

        {

            [_tagBtn setTitleColor:labelBlackColor forState:UIControlStateNormal];

            _tagBtn.backgroundColor = HEXCOLOR(0xeeeeee);

        }

    那controller中的数据该怎么判断是否选中和未选择,然后实现给数据源的属性选中呢?具体思路就是:需要进行遍历数据源中的所有模型进行判断和赋值选中状态,实现方法如下:



    - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath

    {

        [collectionView deselectItemAtIndexPath:indexPath animated:NO];

        PBCTagGroundModel *groundModel = self.listArrM[indexPath.section];

        DLog(@"indexPath.Seciont = %zd,row = %zd",indexPath.section,indexPath.row);

        //替换成可选模式

        for (int i = 0; i < groundModel.tags.count; ++i)

        {

            PBCTagModel *model = groundModel.tags[i];

            if (indexPath.row == i)

            {

                if (model.isSelect == YES)

                {

                    model.isSelect = NO;

                }

                else

                {

                    model.isSelect = YES;

                }

                [groundModel.tags replaceObjectAtIndex:i withObject:model];

            }

        }

        [self.listArrM replaceObjectAtIndex:indexPath.section withObject:groundModel];

        [self.collectionView reloadData];

    }

    既然每一组的cell都实现了选中和未选中状态了,那如何实现判断全选和不选中的状态呢?这里就需要给UICollectionView的头部设置一个点击事件,并且也需要对数据源中的所有数据进行遍历,对数据的选中状态进行选中和未选中的属性赋值。

    那么问题来了,如何这是UICollectionView的头部呢?这个和UITableView的操作其他大有雷同:

    首先,要先注册UICollectionView的头部:

        //注册头视图

        [collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"UICollectionViewHeader"];

    然后实现collectionView的头部尾部设置代理

    //设置头视图的大小

    -(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section{

        return CGSizeMake([UIScreen mainScreen].bounds.size.width, 44);

    }

    //创建头视图

    - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView

               viewForSupplementaryElementOfKind:(NSString *)kind

                                     atIndexPath:(NSIndexPath *)indexPath {

        NSString *indentifierString = @"UICollectionViewHeader";

        UICollectionReusableView *headView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader

                                                                                withReuseIdentifier:indentifierString

                                                                                       forIndexPath:indexPath];

       headView.backgroundColor = [UIColor whiteColor];

        //此处操作是为了防止头部视图重复使用(从缓存机制中取出),导致重叠视图,如要移除此前设置的视图再进行绘制

        for (UIView *subView in headView.subviews)

        {

            [subView removeFromSuperview];

        }

       //设置相关视图

       //.....此处设置

        return headView;

    }

    设置完头部视图了,那具体的数据操作就是遍历数据修改数据源状态了,代码如下:

    /**

     点击选择组便签

     @param btn 组标签

     */

    - (void)clickSelectAllBtn:(UIButton *)btn

    {

        btn.selected = !btn.selected;

        PBCTagGroundModel *groundModel = self.listArrM[btn.tag];

        //替换成可选模式

        for (int i = 0; i < groundModel.tags.count; ++i)

        {

            PBCTagModel *model = groundModel.tags[i];

            if (btn.selected == YES)

            {

                model.isSelect = YES;

            }

            else

            {

                model.isSelect = NO;

            }

            [groundModel.tags replaceObjectAtIndex:i withObject:model];

        }

        [self.listArrM replaceObjectAtIndex:btn.tag withObject:groundModel];

        [self.collectionView reloadData];

    }

    那么对数据源也处理完了,整体的标签组也是实现了。

    其实整体设置标签组,最大的难度是在于UICollectionViewFlowLayout的重写和计算,如果这一步解决了,整体的思路很清晰,可以直接解决问题。

    以上就是通过UICollectionView来实现标签组的方法,可能实现的路径和方法很多,也有更加多便捷方法和思路,上面方法如有不足之处望大家指出,或者有更优的方法,也欢迎大家来探讨。

    大千世界,求同存异;相遇是缘,相识是份,相知便是“猿粪”(缘分)

  • 相关阅读:
    [原]跟我学silverlight系列教程[1]—wpf/silverlight体系架构和运行机制
    [转载]My97DatePicker在Frame中无法打开站点
    sql select的时候按特定的顺序排序
    javascript获取滚动条位置
    sql2005 数据库没有完全关闭,无法重新生成日志
    .Net framework
    输出datagrid的内容到excel
    IIS中网站出错
    [原]跟我学silverlight系列教程
    SharePoint2010 的ADFS2.0设置
  • 原文地址:https://www.cnblogs.com/sundaysgarden/p/10559429.html
Copyright © 2020-2023  润新知