zoukankan      html  css  js  c++  java
  • ios开发瀑布流框架的封装

    一:瀑布流框架封装的实现思路:此瀑布流框架的封装仿照tableView的底层实现,1:每个cell的frame的设置都是找出每列的最大y值,比较每列的最大y值,将下一个cell放在最大y值最小的那一列,并更新最大y值,继续比较设置frame。2:还涉及了类似于tableView缓存池的处理  瀑布流效果如图:

    二:封装代码:

    1:瀑布流控件的view封装:

     1 //  使用瀑布流形式展示内容的控件
     2 
     3 #import <UIKit/UIKit.h>
     4 
     5 typedef enum {
     6     HMWaterflowViewMarginTypeTop,
     7     HMWaterflowViewMarginTypeBottom,
     8     HMWaterflowViewMarginTypeLeft,
     9     HMWaterflowViewMarginTypeRight,
    10     HMWaterflowViewMarginTypeColumn, // 每一列
    11     HMWaterflowViewMarginTypeRow, // 每一行
    12 } HMWaterflowViewMarginType;
    13 
    14 @class HMWaterflowView, HMWaterflowViewCell;
    15 
    16 /**
    17  *  数据源方法
    18  */
    19 @protocol HMWaterflowViewDataSource <NSObject>
    20 @required
    21 /**
    22  *  一共有多少个数据
    23  */
    24 - (NSUInteger)numberOfCellsInWaterflowView:(HMWaterflowView *)waterflowView;
    25 /**
    26  *  返回index位置对应的cell
    27  */
    28 - (HMWaterflowViewCell *)waterflowView:(HMWaterflowView *)waterflowView cellAtIndex:(NSUInteger)index;
    29 
    30 @optional
    31 /**
    32  *  一共有多少列
    33  */
    34 - (NSUInteger)numberOfColumnsInWaterflowView:(HMWaterflowView *)waterflowView;
    35 @end
    36 
    37 /**
    38  *  代理方法
    39  */
    40 @protocol HMWaterflowViewDelegate <UIScrollViewDelegate>
    41 @optional
    42 /**
    43  *  第index位置cell对应的高度
    44  */
    45 - (CGFloat)waterflowView:(HMWaterflowView *)waterflowView heightAtIndex:(NSUInteger)index;
    46 /**
    47  *  选中第index位置的cell
    48  */
    49 - (void)waterflowView:(HMWaterflowView *)waterflowView didSelectAtIndex:(NSUInteger)index;
    50 /**
    51  *  返回间距
    52  */
    53 - (CGFloat)waterflowView:(HMWaterflowView *)waterflowView marginForType:(HMWaterflowViewMarginType)type;
    54 @end
    55 
    56 /**
    57  *  瀑布流控件
    58  */
    59 @interface HMWaterflowView : UIScrollView
    60 /**
    61  *  数据源
    62  */
    63 @property (nonatomic, weak) id<HMWaterflowViewDataSource> dataSource;
    64 /**
    65  *  代理
    66  */
    67 @property (nonatomic, weak) id<HMWaterflowViewDelegate> delegate;
    68 
    69 /**
    70  *  刷新数据(只要调用这个方法,会重新向数据源和代理发送请求,请求数据)
    71  */
    72 - (void)reloadData;
    73 
    74 /**
    75  *  cell的宽度
    76  */
    77 - (CGFloat)cellWidth;
    78 
    79 /**
    80  *  根据标识去缓存池查找可循环利用的cell
    81  */
    82 - (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;
    83 @end

    思路分析:1:仿照tableView来设置dataSource 和 delegate 两个代理:waterFlow的dataSource代理方法中仿照tableView的dataSource代理方法,@required 必须实现的是,1:一共有多少个cell,- (NSUInteger)numberOfCellsInWaterflowView:(HMWaterflowView *)waterflowView ,2:根据cell的index返回每一个cell,- (HMWaterflowViewCell *)waterflowView:(HMWaterflowView *)waterflowView cellAtIndex:(NSUInteger)index;其中HMWaterflowViewCell就为每一个单元的cell,自定义HMWaterflowViewCell继承于UIView 3:@optional 方法为,一共返回多少列,如果没有实现此方法则返回默认的列数 2:在waterFlow的delegate方法中,都为@optional,并仿照tableView提供,1:根据index返回每个cell的行高  - (CGFloat)waterflowView:(HMWaterflowView *)waterflowView heightAtIndex:(NSUInteger)index;2:点击index处的cell的方法:- (void)waterflowView:(HMWaterflowView *)waterflowView didSelectAtIndex:(NSUInteger)index; 3:返回整个瀑布流的间距值,若不返回,则会有默认值,上下左右,行间距,列间距,根据上下左右行列不同的类型返回不同的高度,将这几个不同的类型定义成枚举值,将枚举传过去,外界可利用switch根据不同类型,返回不同的间距(类似于按钮回调的枚举tag)

    2:以属性设置两个代理:设置delegate时,会有警告,因为瀑布流控件继承的是UIScroll,则继承了UIScroll在.h声明的属性和方法,此delegate会覆盖掉scrollView的滚动视图的代理,所以会有警告,解决办法是,让瀑布流控件的代理协议再遵守UISCrollViewDelegate,则在外部只要遵守了瀑布流的代理,也就遵守了UIScroll的滚动代理,所以以属性设置瀑布流代理后,外部遵守协议,设置代理,则其就可以调用UIscroll的滚动代理方法了

    3:仿照tableView并提供刷新表格,和缓存池根据重用标识取cell的方法,- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;其中id 和 instanceType的区别:1:id 修饰非关联返回类型 ,可做参数,也可以做返回值 而instanceType为关联返回类型,返回该方法所在类的类型,并且只能做返回值,一般以alloc。autorelease,retain开头的方法用id 2:用instanceType相对id来说,能帮助编译器快速确定返回值类型。

    2:瀑布流的内部核心封装代码:

      1 #import "HMWaterflowView.h"
      2 #import "HMWaterflowViewCell.h"
      3 
      4 #define HMWaterflowViewDefaultCellH 70
      5 #define HMWaterflowViewDefaultMargin 8
      6 #define HMWaterflowViewDefaultNumberOfColumns 3
      7 
      8 @interface HMWaterflowView()
      9 /**
     10  *  所有cell的frame数据
     11  */
     12 @property (nonatomic, strong) NSMutableArray *cellFrames;
     13 /**
     14  *  正在展示的cell
     15  */
     16 @property (nonatomic, strong) NSMutableDictionary *displayingCells;
     17 /**
     18  *  缓存池(用Set,存放离开屏幕的cell)
     19  */
     20 @property (nonatomic, strong) NSMutableSet *reusableCells;
     21 @end
     22 
     23 @implementation HMWaterflowView
     24 
     25 #pragma mark - 初始化
     26 - (NSMutableArray *)cellFrames
     27 {
     28     if (_cellFrames == nil) {
     29         self.cellFrames = [NSMutableArray array];
     30     }
     31     return _cellFrames;
     32 }
     33 
     34 - (NSMutableDictionary *)displayingCells
     35 {
     36     if (_displayingCells == nil) {
     37         self.displayingCells = [NSMutableDictionary dictionary];
     38     }
     39     return _displayingCells;
     40 }
     41 
     42 - (NSMutableSet *)reusableCells
     43 {
     44     if (_reusableCells == nil) {
     45         self.reusableCells = [NSMutableSet set];
     46     }
     47     return _reusableCells;
     48 }
     49 
     50 - (id)initWithFrame:(CGRect)frame
     51 {
     52     self = [super initWithFrame:frame];
     53     if (self) {
     54         
     55     }
     56     return self;
     57 }
     58 
     59 - (void)willMoveToSuperview:(UIView *)newSuperview
     60 {
     61     [self reloadData];
     62 }
     63 
     64 #pragma mark - 公共接口
     65 /**
     66  *  cell的宽度
     67  */
     68 - (CGFloat)cellWidth
     69 {
     70     // 总列数
     71     int numberOfColumns = [self numberOfColumns];
     72     CGFloat leftM = [self marginForType:HMWaterflowViewMarginTypeLeft];
     73     CGFloat rightM = [self marginForType:HMWaterflowViewMarginTypeRight];
     74     CGFloat columnM = [self marginForType:HMWaterflowViewMarginTypeColumn];
     75     return (self.bounds.size.width - leftM - rightM - (numberOfColumns - 1) * columnM) / numberOfColumns;
     76 }
     77 
     78 /**
     79  *  刷新数据
     80  */
     81 - (void)reloadData
     82 {
     83     // 清空之前的所有数据
     84     // 移除正在正在显示cell
     85     [self.displayingCells.allValues makeObjectsPerformSelector:@selector(removeFromSuperview)];
     86     [self.displayingCells removeAllObjects];
     87     [self.cellFrames removeAllObjects];
     88     [self.reusableCells removeAllObjects];
     89     
     90     // cell的总数
     91     int numberOfCells = [self.dataSource numberOfCellsInWaterflowView:self];
     92     
     93     // 总列数
     94     int numberOfColumns = [self numberOfColumns];
     95     
     96     // 间距
     97     CGFloat topM = [self marginForType:HMWaterflowViewMarginTypeTop];
     98     CGFloat bottomM = [self marginForType:HMWaterflowViewMarginTypeBottom];
     99     CGFloat leftM = [self marginForType:HMWaterflowViewMarginTypeLeft];
    100     CGFloat columnM = [self marginForType:HMWaterflowViewMarginTypeColumn];
    101     CGFloat rowM = [self marginForType:HMWaterflowViewMarginTypeRow];
    102     
    103     // cell的宽度
    104     CGFloat cellW = [self cellWidth];
    105     
    106     // 用一个C语言数组存放所有列的最大Y值
    107     CGFloat maxYOfColumns[numberOfColumns];
    108     for (int i = 0; i<numberOfColumns; i++) {
    109         maxYOfColumns[i] = 0.0;
    110     }
    111     
    112     // 计算所有cell的frame
    113     for (int i = 0; i<numberOfCells; i++) {
    114         // cell处在第几列(最短的一列)
    115         NSUInteger cellColumn = 0;
    116         // cell所处那列的最大Y值(最短那一列的最大Y值)
    117         CGFloat maxYOfCellColumn = maxYOfColumns[cellColumn];
    118         // 求出最短的一列
    119         for (int j = 1; j<numberOfColumns; j++) {
    120             if (maxYOfColumns[j] < maxYOfCellColumn) {
    121                 cellColumn = j;
    122                 maxYOfCellColumn = maxYOfColumns[j];
    123             }
    124         }
    125         
    126         // 询问代理i位置的高度
    127         CGFloat cellH = [self heightAtIndex:i];
    128         
    129         // cell的位置
    130         CGFloat cellX = leftM + cellColumn * (cellW + columnM);
    131         CGFloat cellY = 0;
    132         if (maxYOfCellColumn == 0.0) { // 首行
    133             cellY = topM;
    134         } else {
    135             cellY = maxYOfCellColumn + rowM;
    136         }
    137         
    138         // 添加frame到数组中
    139         CGRect cellFrame = CGRectMake(cellX, cellY, cellW, cellH);
    140         [self.cellFrames addObject:[NSValue valueWithCGRect:cellFrame]];
    141         
    142         // 更新最短那一列的最大Y值
    143         maxYOfColumns[cellColumn] = CGRectGetMaxY(cellFrame);
    144     }
    145     
    146     // 设置contentSize
    147     CGFloat contentH = maxYOfColumns[0];
    148     for (int j = 1; j<numberOfColumns; j++) {
    149         if (maxYOfColumns[j] > contentH) {
    150             contentH = maxYOfColumns[j];
    151         }
    152     }
    153     contentH += bottomM;
    154     self.contentSize = CGSizeMake(0, contentH);
    155 }
    156 
    157 /**
    158  *  当UIScrollView滚动的时候也会调用这个方法
    159  */
    160 - (void)layoutSubviews
    161 {
    162     [super layoutSubviews];
    163     
    164     // 向数据源索要对应位置的cell
    165     NSUInteger numberOfCells = self.cellFrames.count;
    166     for (int i = 0; i<numberOfCells; i++) {
    167         // 取出i位置的frame
    168         CGRect cellFrame = [self.cellFrames[i] CGRectValue];
    169         
    170         // 优先从字典中取出i位置的cell
    171         HMWaterflowViewCell *cell = self.displayingCells[@(i)];
    172         
    173         // 判断i位置对应的frame在不在屏幕上(能否看见)
    174         if ([self isInScreen:cellFrame]) {// 在屏幕上
    175             if (cell == nil) {
    176                 cell = [self.dataSource waterflowView:self cellAtIndex:i];
    177                 cell.frame = cellFrame;
    178                 [self addSubview:cell];
    179                 
    180                 // 存放到字典中
    181                 self.displayingCells[@(i)] = cell;
    182             }
    183         } else {  // 不在屏幕上
    184             if (cell) {
    185                 // 从scrollView和字典中移除
    186                 [cell removeFromSuperview];
    187                 [self.displayingCells removeObjectForKey:@(i)];
    188                 
    189                 // 存放进缓存池
    190                 [self.reusableCells addObject:cell];
    191             }
    192         }
    193     }
    194 }
    195 
    196 - (id)dequeueReusableCellWithIdentifier:(NSString *)identifier
    197 {
    198     __block HMWaterflowViewCell *reusableCell = nil;
    199     
    200     [self.reusableCells enumerateObjectsUsingBlock:^(HMWaterflowViewCell *cell, BOOL *stop) {
    201         if ([cell.identifier isEqualToString:identifier]) {
    202             reusableCell = cell;
    203             *stop = YES;
    204         }
    205     }];
    206     
    207     if (reusableCell) { // 从缓存池中移除
    208         [self.reusableCells removeObject:reusableCell];
    209     }
    210     return reusableCell;
    211 }
    212 
    213 #pragma mark - 私有方法
    214 /**
    215  *  判断一个frame有无显示在屏幕上
    216  */
    217 - (BOOL)isInScreen:(CGRect)frame
    218 {
    219     return (CGRectGetMaxY(frame) > self.contentOffset.y) &&
    220     (CGRectGetMinY(frame) < self.contentOffset.y + self.bounds.size.height);
    221 }
    222 
    223 /**
    224  *  间距
    225  */
    226 - (CGFloat)marginForType:(HMWaterflowViewMarginType)type
    227 {
    228     if ([self.delegate respondsToSelector:@selector(waterflowView:marginForType:)]) {
    229         return [self.delegate waterflowView:self marginForType:type];
    230     } else {
    231         return HMWaterflowViewDefaultMargin;
    232     }
    233 }
    234 /**
    235  *  总列数
    236  */
    237 - (NSUInteger)numberOfColumns
    238 {
    239     if ([self.dataSource respondsToSelector:@selector(numberOfColumnsInWaterflowView:)]) {
    240         return [self.dataSource numberOfColumnsInWaterflowView:self];
    241     } else {
    242         return HMWaterflowViewDefaultNumberOfColumns;
    243     }
    244 }
    245 /**
    246  *  index位置对应的高度
    247  */
    248 - (CGFloat)heightAtIndex:(NSUInteger)index
    249 {
    250     if ([self.delegate respondsToSelector:@selector(waterflowView:heightAtIndex:)]) {
    251         return [self.delegate waterflowView:self heightAtIndex:index];
    252     } else {
    253         return HMWaterflowViewDefaultCellH;
    254     }
    255 }
    256 
    257 #pragma mark - 事件处理
    258 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
    259 {
    260     if (![self.delegate respondsToSelector:@selector(waterflowView:didSelectAtIndex:)]) return;
    261     
    262     // 获得触摸点
    263     UITouch *touch = [touches anyObject];
    264     //    CGPoint point = [touch locationInView:touch.view];
    265     CGPoint point = [touch locationInView:self];
    266     
    267     __block NSNumber *selectIndex = nil;
    268     [self.displayingCells enumerateKeysAndObjectsUsingBlock:^(id key, HMWaterflowViewCell *cell, BOOL *stop) {
    269         if (CGRectContainsPoint(cell.frame, point)) {
    270             selectIndex = key;
    271             *stop = YES;
    272         }
    273     }];
    274     
    275     if (selectIndex) {
    276         [self.delegate waterflowView:self didSelectAtIndex:selectIndex.unsignedIntegerValue];
    277     }
    278 }
    279 
    280 @end

    思路分析:1:瀑布流的核心代码的封装也是仿照tableView的底层实现:在tableView第一次加载的时候会首先调用一次刷新数据的方法,在下列方法中实现:

    - (void)willMoveToSuperview:(UIView *)newSuperview{[self reloadData];},此方法willMoveToSuperview,是子view即将添加到superView时会调用一次。调用[self reloadData],模拟tableView第一次刷新表格 2:基本实现思路是,在reloadData方法中设置每一个cell的frame,存放到大数组中,再在layoutSubView里设置每一个view的frame。

    2:在reloadData方法中,1:每执行一次reloadData方法,都要将原来的数据清空,来展示新的cell。其中self.displayingCells为正在显示的cell的缓存字典,key值为每一个index,value为每一个index对应的正在显示的cell(dic.allKeys,dic.allValues,分别获得字典的所有的key值和value值得到一个数组,enum遍历字典的时候,分别可得到字典的key值和value值)。2:在清空数据时,缓存字典,让字典中每一个正在显示的cell从父视图上移除,并清空缓存字典,缓存池,还有装有frame的数组,其中数组里有一个方法是遍历每个元素让每个元素都去执行某个方法[self.displayingCells.allValues makeObjectsPerformSelector:@selector(removeFromSuperview)]; 2:在reloadData中最主要的就是要求出每个cell的xy宽高,其中高度需要询问代理,若实现了代理方法,则根据服务器返回图片的宽高,按比例计算出相应的高度并返回。要想设置每个cell的frame,调用数据源方法,返回总的cell的个数,for循环遍历每个cell,因为设置frame,要从最大y值最小的那列开始设置,所以在for循环遍历每一个cell的时候,首先应找出最小y值的那一列,和最小y值。定义数组存放每一列的最大的y值,因为其为基本数据类型,所以用C语言数组存放,OC数组只能存放对象。先调用代理方法返回列数,若没有实现代理方法,则默认返回的而是3列,定义C语言的数组,定义数组元素个数为numberOfColumns列数的数组,且数组中元素的基本类型都为CGFloat类型。并对数组中的元素初始化,遍历数组,取出每个元素,赋值为0.0,float类型

      CGFloat maxYOfColumns[numberOfColumns];

        for (int i = 0; i<numberOfColumns; i++) {

            maxYOfColumns[i] = 0.0;

        }

    3:找出最小y值所在的列号,并记录此列下最大的y值。先假设最小的y值的列号是第0列,则最小y值列号下的最大的y值就为从C语言数组中根据列号取出的该列的最大y值,再for循环遍历C语言数组,从列号为1开始遍历,做判断,从列号为1开始从数组中取出的最大y值还比定义的最大y值还小,则此时记录下最小的y值的列号,和最小y值列号下的最大y值。直到找到最小y值的列号,此时,设置frame就从最小y值的列号处开始设置。

    4:找到最小y值所在的列号后,开始设置cell的frame,宽度 = 屏幕宽度 - 左右间距 - (总列数 -1)*列间距,都可以求出,屏幕的高度,调用代理根据index返回该index处的cell的高度,(瀑布流图片的宽高都为后台服务器返回),若实现了代理高度的方法,则返回高度时,应该根据服务器返回图片高度的比例计算出。若没实现代理方法则返回默认的高度。 询问代理i位置的高度 CGFloat cellH = [self heightAtIndex:i];X值的计算:计算x值和cell所在的列号有关, CGFloat cellX = leftM + cellColumn * (cellW + columnM);Y值计算:和是否是首行有关,若是在首行,则y值为顶部间距,若不在首行,则y值为该列的最大y值加上一个行间距的值,是否是首行的判断,则判断C语言数组中每一列对应的最大y值,若为0,则在首行,若不为0,则不在首行,此时可以设置cell的frame

    5:将设置好的cell的frame(结构体类型,非对象)封装成OC对象,存放到数组中。 CGRect cellFrame = CGRectMake(cellX, cellY, cellW, cellH); [self.cellFrames addObject:[NSValue valueWithCGRect:cellFrame]];

    6:还要更新C语言数组中设置frame的所在列的最大y值,以便下次循环计算:maxYOfColumns[cellColumn] = CGRectGetMaxY(cellFrame);

    7:在设置完所有的frame后,要根据C语言数组中最大的y值,计算scrollView的contentSize,否则scrollView将不会滚动。如何找出最大的y值?先定义第0列为最大y值,将最大的y值从数组中取出,在for循环遍历C语言数组,j从1开始,将数组中的每一列最大y值取出比较,若比这个最大的还大,则记录下来,最后计算contentSize为最大y值加一个底部间距

     CGFloat contentH = maxYOfColumns[0];

        for (int j = 1; j<numberOfColumns; j++) {

            if (maxYOfColumns[j] > contentH) {

                contentH = maxYOfColumns[j];

            }

        }

        contentH += bottomM;

        self.contentSize = CGSizeMake(0, contentH);

    3:layoutSubView中设置每一个cell的frame:1:遍历存放cell所有frame的数组,取出每一个frame,CGRect cellFrame = [self.cellFrames[i] CGRectValue];首先先判断是否在屏幕上,如果不在屏幕上,则将其cell从父视图上移除,并且从缓存字典中移除,并加入到缓存池,NSMutableSet 所定义的集合中,同数组的用法是相同,存放对象,但数组是有序的,NSMutableSet是无序的。如果在屏幕上显示,则证明已经计算好frame,则优先从缓存字典中根据索引取出cell。判断取出的cell是否存在,不存在,则调用数据源方法,从缓存池中取或是自行创建,返回一个cell,设置frame,添加到scrollView上。并将新创建的cell存入缓存字典中。2:如何判断是否在屏幕上,传递frame,根据frame判断是否在屏幕上

    - (BOOL)isInScreen:(CGRect)frame

    {

        return (CGRectGetMaxY(frame) > self.contentOffset.y) &&

        (CGRectGetMinY(frame) < self.contentOffset.y + self.bounds.size.height);

    }

    4:根据重用标识从缓存池中取数据:遍历集合,得到每一个cell,做条件过滤,判断如果cell的重用标识符与传入的标识符相等,则赋值cell,并停止遍历,在block内部若是想赋值外部变量,则用__block修饰外部的变量,最后再判断cell如果存在从缓存池中删除,若是不删除重用的cell,则其不到重用的作用,内存会被撑爆.

    5:cell的点击事件:1:若是想实现点击事件:1:继承UIControl的addTarget  ,touch或是valueChanged 2;添加手势监听器,要开启用户交互权限(如lable,iamgeView)3:实现touchBeagn,touchEnd,touchCancle方法,其中的touchBeagn刚开始点击的时候调用,touchEnd结束点击的时候调用。2:cell的点击采取实现touch方法,实现的是touchEnd,点击结束时的方法。并实现UITouch的方法。3:首先做条件过滤,若没有实现该代理方法,直接返回,若实现了代理方法,先获得触摸点, UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self];得到触摸点,若是第二个参数为cell则触摸点的位置是相对cell来计算的,若是为self,则是针对整个瀑布流控件的frame来计算的。其中touch.view获得的是触摸的view,也就是cell 。目的是求出index,调用代理将index传过去,所以遍历缓存字典,得到key,value值,key为index,value为cell,再判断所得触摸点在不在cell上,调用CGRectContainsPoint(cell.frame, point),如果在,则赋值index,停止遍历,再外部判断index值是否存在,存在则调用代理执行代理的点击方法。@5或是@(3)两个均为对象的表示形式,都为NSNumber类型。

    7:自定义cell的封装:

    1 #import <UIKit/UIKit.h>
    2 
    3 @interface HMWaterflowViewCell : UIView
    4 @property (nonatomic, copy) NSString *identifier;
    5 @end
     1 #import "HMWaterflowViewCell.h"
     2 
     3 @implementation HMWaterflowViewCell
     4 
     5 - (id)initWithFrame:(CGRect)frame
     6 {
     7     self = [super initWithFrame:frame];
     8     if (self) {
     9         // Initialization code
    10     }
    11     return self;
    12 }
    13 
    14 /*
    15 // Only override drawRect: if you perform custom drawing.
    16 // An empty implementation adversely affects performance during animation.
    17 - (void)drawRect:(CGRect)rect
    18 {
    19     // Drawing code
    20 }
    21 */
    22 
    23 @end
  • 相关阅读:
    A1052. Linked List Sorting (25)
    A1032. Sharing (25)
    A1022. Digital Library (30)
    A1071. Speech Patterns (25)
    A1054. The Dominant Color (20)
    A1060. Are They Equal (25)
    A1063. Set Similarity (25)
    电子码表
    矩阵键盘
    对象追踪、临时对象追踪、绝对坐标与相对坐标
  • 原文地址:https://www.cnblogs.com/cqb-learner/p/5733416.html
Copyright © 2011-2022 走看看