Painted painted levels: fostered fostered fostered

QiCardView by MrLiuQ Review: QiShare team


Foreword: because of the demand in the project, need to do a card type control. So QiCardView was born.

First, let’s take a look at the QiCardView:

From the name, QiCardView is, as the name suggests, a customizable card-like UI control. In terms of design, QiCardView mimics the design of UITableView and supports cell reuse, saving resources.

Without further ado, let’s take a look at the overall architecture

I. QiCardView overall architecture design

The architecture layer mimics the design of UITableView and adopts the cell reuse strategy. On this basis, into some gesture operation, more interactive.

Shelf composition:

The two main classes are QiCardView and QiCardViewCell. (Copy UITableView+UITableViewCell design)

  • QiCardViewThere are two agents under:QiCardViewDataSource,QiCardViewDelegate. (Similar to UITableView’s proxy method)
  • QiCardViewCellThere is an agent next:QiCardViewCellDelegate. (This proxy can be ignored, the main purpose is to assist some processing logic in QiCardView.)

How to customize QiCardView?

Cell customization is as simple as creating a new class (for example, QiCardViewItemCell) that inherits from QiCardViewCell.

In Controller, the basic usage is almost similar to UITableView.

  • Initialize theCardViewMethods:

Before the Demo, here are a few configuration properties that you can customize:

attribute type introduce
visibleCount NSInteger Number of card cells visible (default: 3). This is the number of cells actually created because of the reuse policy.
lineSpacing CGFloat Line spacing (default 10.0, you can calculate the scale scale to do the spacing)
interitemSpacing CGFloat Column spacing (default 10.0, you can calculate scale scale to do spacing)
maxAngle CGFloat Maximum sideslip Angle (default 15°). The smaller the value, the easier it is to draw, the harder it is to draw.
maxRemoveDistance CGFloat Maximum removal distance (1/4 of the default screen), when the slide distance is not enough to return.
isAlpha CGFloat Whether the cell needs gradient transparency. (Default YES)
- (void)initViews {_cardView = [[QiCardView alloc] initWithFrame:CGRectMake(25.0, 150.0, Self. View. Frame. The size. Width - 50.0, 420.0)]; _cardView.backgroundColor = [UIColor lightGrayColor]; / /! < To specify the carddView region, specify the background color _cardView.dataSource = self; _cardView.delegate = self; _cardView.visibleCount = 4; _cardView. LineSpacing = 15.0; _cardView. InteritemSpacing = 10.0; _cardView. MaxAngle = 10.0; _cardView.isAlpha = YES; _cardView. MaxRemoveDistance = 100.0; _cardView. Layer. The cornerRadius = 10.0; [_cardView registerClass:[QiCardItemCell class] forCellReuseIdentifier:qiCardCellId]; [self.view addSubview:_cardView]; }Copy the code
  • Data sources:QiCardViewDataSource:

#pragma mark - QiCardViewDataSource - (QiCardItemCell *)cardView:(QiCardView *)cardView cellForRowAtIndex:(NSInteger)index { QiCardItemCell *cell = [cardView dequeueReusableCellWithIdentifier:qiCardCellId]; cell.cellData = _cellItems[index]; / /... return cell; } - (NSInteger)numberOfCountInCardView:(UITableView *)cardView { return _cellItems.count; }Copy the code
  • Agent:QiCardViewDelegate:

Again, the controller needs to follow the protocol first:

.

#pragma mark - QiCardViewDelegate

- (void)cardView:(QiCardView *)cardView didRemoveLastCell:(QiCardViewCell *)cell forRowAtIndex:(NSInteger)index {
    [cardView reloadDataAnimated:YES];
}

- (void)cardView:(QiCardView *)cardView didRemoveCell:(QiCardViewCell *)cell forRowAtIndex:(NSInteger)index {
    NSLog(@"didRemoveCell forRowAtIndex = %ld", index);
}

- (void)cardView:(QiCardView *)cardView didDisplayCell:(QiCardViewCell *)cell forRowAtIndex:(NSInteger)index {
    
    NSLog(@"didDisplayCell forRowAtIndex = %ld", index);
}

- (void)cardView:(QiCardView *)cardView didMoveCell:(QiCardViewCell *)cell forMovePoint:(CGPoint)point {
    NSLog(@"move point = %@", NSStringFromCGPoint(point));
}
Copy the code

3. Technical points of QiCardView

3.1 Implementation of QiCardViewCell reuse strategy

  1. Registered Cell:

There are two methods: registerNib and registerClass. Very simple.

/** Register cell Nib */ - (void)registerNib:(nullable UINib *)nib forCellReuseIdentifier:(NSString *)identifier { self.nib = nib; self.identifier = identifier; } /** Register cell Class */ - (void)registerClass:(nullable Class)cellClass forCellReuseIdentifier:(NSString *)identifier { self.cellClass = cellClass; self.identifier = identifier; }Copy the code
  1. Get cache Cell policy:

Check whether any Cell with the same IDENTIFIER (IDENTIFIER) exists in the cache pool. If so, return the Cell directly. If no Cell exists in the cache pool, create a new Cell

/ * * access to the cache cell * / - (__kindof QiCardViewCell *) dequeueReusableCellWithIdentifier: (nsstrings *) identifier {the for (QiCardViewCell *cell in self.reusableCells) { if ([cell.reuseIdentifier isEqualToString:identifier]) { [self.reusableCells removeObject:cell]; return cell; } } if (self.nib) { QiCardViewCell *cell = [[self.nib instantiateWithOwner:nil options:nil] lastObject]; cell.reuseIdentifier = identifier; return cell; } else if (self.cellClass) {// Register class QiCardViewCell *cell = [[self.cellClass alloc] initWithReuseIdentifier:identifier]; cell.reuseIdentifier = identifier; return cell; } return nil; }Copy the code
  1. When the cellDidRemoveFromSuperViewMethod to add the cell to the cache pool.
- (void)cardViewCellDidRemoveFromSuperView:(QiCardViewCell *)cell { //... [self.reusableCells addObject:cell]; / /... }Copy the code

3.2 Realization of cell overlapping transparency gradient

  1. We first declare a static variable:moveCountTo keep track of card flips. (To logically associate the index of the cell with the index of the card)
static int moveCount = 0; / /! < Record the number of page turnsCopy the code
  1. Logic: Each CardCell moveCount+1 when “remove from Super View”.
#pragma mark - QiCardViewCellDelagate

- (void)cardViewCellDidRemoveFromSuperView:(QiCardViewCell *)cell {
    
    moveCount++;
    
    //....
}
Copy the code
  1. Logic: In the reload method, you need to set moveCount0. (On reload, moveCount needs to be recalculated)
- (void)reloadDataAnimated:(BOOL)animated { moveCount = 0; / /! < gradient needs //... }Copy the code
  1. Key logic: Set the gradient value for each Cell each time the layout is updated (i.ealpha)
/ * * update layout (animation) * / - (void) animated updateLayoutVisibleCellsWithAnimated: (BOOL) {/ /... if (_isAlpha) { BOOL isTopCell = (i == _currentIndex - moveCount); if (isTopCell) {//! < For the uppermost Cell, transparency is 1 Cell. Alpha = 1.0; } else {cell. The alpha = (I) + 1.9 * 1.0 / self visibleCells. Count; }} / /... }Copy the code

3.3 Gesture operation implementation

This part is mainly gesture + animation. More details, small and miscellaneous. Detailed logic, please see the source code.

#define Qi_DEGREES_TO_RADIANS(Angle) (Angle / 180.0 * M_PI) - (void)panGestureRecognizer:(UIPanGestureRecognizer*)pan { switch (pan.state) { case UIGestureRecognizerStateBegan: self.currentPoint = CGPointZero; break; case UIGestureRecognizerStateChanged: { CGPoint movePoint = [pan translationInView:pan.view]; self.currentPoint = CGPointMake(self.currentPoint.x + movePoint.x , self.currentPoint.y + movePoint.y); CGFloat moveScale = self.currentPoint.x / self.maxRemoveDistance; If (ABS(moveScale) > 1.0) {moveScale = (moveScale > 0)? : 1.0-1.0; } CGFloat angle = Qi_DEGREES_TO_RADIANS(self.maxAngle) * moveScale; CGAffineTransform transRotation = CGAffineTransformMakeRotation(angle); self.transform = CGAffineTransformTranslate(transRotation, self.currentPoint.x, self.currentPoint.y); if (self.cell_delegate && [self.cell_delegate respondsToSelector:@selector(cardViewCellDidMoveFromSuperView:forMovePoint:)]) { [self.cell_delegate cardViewCellDidMoveFromSuperView:self forMovePoint:self.currentPoint]; } [pan setTranslation:CGPointZero inView:pan.view]; } break; case UIGestureRecognizerStateEnded: [self didPanStateEnded]; break; case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateFailed: [self restoreCellLocation]; break; default: break; }} - (void)didPanStateEnded {// Right slip if (self.currentPoint.x > self.maxRemoveDistance) {__block UIView *snapshotView = [self snapshotViewAfterScreenUpdates:NO]; snapshotView.transform = self.transform; [self.superview.superview addSubview:snapshotView]; [self didCellRemoveFromSuperview]; CGFloat endCenterX = [UIScreen mainScreen].bounds.size.width/2 + self.frame.size.width * 1.5; [UIView animateWithDuration:Qi_DefaultDuration animations:^{ CGPoint center = self.center; center.x = endCenterX;  snapshotView.center = center; } completion:^(BOOL finished) { [snapshotView removeFromSuperview]; }]; } else if (self.currentPoint. X < -self.maxRemoveDistance) {__block UIView *snapshotView = [self snapshotViewAfterScreenUpdates:NO]; snapshotView.transform = self.transform; [self.superview.superview addSubview:snapshotView]; [self didCellRemoveFromSuperview]; CGFloat endCenterX = -([UIScreen mainScreen].bounds.size.width/2 + self.frame.size.width); [UIView animateWithDuration:Qi_DefaultDuration animations:^{ CGPoint center = self.center; center.x = endCenterX;  snapshotView.center = center; } completion:^(BOOL finished) { [snapshotView removeFromSuperview]; }]; } else {[self restoreCellLocation]; }} // restoreCellLocation - (void)restoreCellLocation {[UIView animateWithDuration:Qi_SpringDuration delay:0 usingSpringWithDamping:Qi_SpringWithDamping initialSpringVelocity:Qi_SpringVelocity options:UIViewAnimationOptionCurveEaseOut animations:^{ self.transform = CGAffineTransformIdentity; } completion:nil]; } / / card removed processing - (void) didCellRemoveFromSuperview {self. The transform = CGAffineTransformIdentity; [self removeFromSuperview]; if ([self.cell_delegate respondsToSelector:@selector(cardViewCellDidRemoveFromSuperView:)]) { [self.cell_delegate cardViewCellDidRemoveFromSuperView:self]; } } - (void)removeFromSuperviewSwipe:(QiCardCellSwipeDirection)direction { switch (direction) { case QiCardCellSwipeDirectionLeft: { [self removeFromSuperviewLeft]; } break; case QiCardCellSwipeDirectionRight: { [self removeFromSuperviewRight]; } break; default: break; }} // Remove the animation to the left - (void)removeFromSuperviewLeft {__block UIView *snapshotView = [self snapshotViewAfterScreenUpdates:NO]; [self.superview.superview addSubview:snapshotView]; [self didCellRemoveFromSuperview]; CGAffineTransform transRotation = CGAffineTransformMakeRotation(-Qi_DEGREES_TO_RADIANS(self.maxAngle)); CGAffineTransform transform = CGAffineTransformTranslate (transRotation, 0, the self. Frame. The size, height / 4.0); CGFloat endCenterX = -([UIScreen mainScreen].bounds.size.width/2 + self.frame.size.width); [UIView animateWithDuration:Qi_DefaultDuration animations:^{ CGPoint center = self.center; center.x = endCenterX;  snapshotView.center = center; snapshotView.transform = transform;  } completion:^(BOOL finished) { [snapshotView removeFromSuperview]; }]; } / / to the right to remove the animation - (void) removeFromSuperviewRight {__block UIView * snapshotView = [self snapshotViewAfterScreenUpdates: NO];  snapshotView.frame = self.frame; [self.superview.superview addSubview:snapshotView]; [self didCellRemoveFromSuperview]; CGAffineTransform transRotation = CGAffineTransformMakeRotation(Qi_DEGREES_TO_RADIANS(self.maxAngle)); CGAffineTransform transform = CGAffineTransformTranslate (transRotation, 0, the self. Frame. The size, height / 4.0); CGFloat endCenterX = [UIScreen mainScreen].bounds.size.width/2 + self.frame.size.width * 1.5; [UIView animateWithDuration:Qi_DefaultDuration animations:^{ CGPoint center = self.center; center.x = endCenterX;  snapshotView.center = center; snapshotView.transform = transform;  } completion:^(BOOL finished) { [snapshotView removeFromSuperview]; }]; }Copy the code

Four, possible optimization points in the future

  • Design level: Would it be better to integrate gestures into QiCardView and make the QiCardViewCell pure? (Thinking)
  • Application level: Currently, only one Cell with one ID can be reused. In the future, cells with multiple ids can be reused. Since only one ID is stored, consider storing the array and the corresponding Cell cache pool array. Guess the internal implementation of UITableView.)

QiCardView source code.


Recommended articles:

IOS Wireshark Packet capture iOS Charles Packet capture TCP IP address UDP weekly