Day 4:MarqueeLabel,UILabel滚动效果、跑马灯效果

MarqueeLabel,是一个UILabel的滚动效果、跑马灯效果控件,下面先来看下运行效果:

MarqueeLabel

MarqueeLabel继承于UILabel,主要原理是通过控制两个UILabel的key frame动画进行完成的,并结合使用attributedText等其它属性。

https://github.com/cbpowell/MarqueeLabel

工程目录结构:

MarqueeLabel

 

使用方法:

 

    // Continuous Type

    self.demoLabel1.tag = 101;

    self.demoLabel1.marqueeType = MLContinuous;//滚动样式

    self.demoLabel1.scrollDuration = 10.0;//滚动持续时间

    self.demoLabel1.animationCurve = UIViewAnimationOptionCurveEaseInOut;//动画方式

    self.demoLabel1.fadeLength = 10.0f;//左右边缘模糊效果

    self.demoLabel1.leadingBuffer = 30.0f;

    self.demoLabel1.trailingBuffer = 20.0f;

 

MarqueeType枚举:

 

/** An enum that defines the types of `MarqueeLabel` scrolling */
typedef NS_ENUM(NSUInteger, MarqueeType) {
    /** Scrolls left first, then back right to the original position. */
    MLLeftRight = 0,
    /** Scrolls right first, then back left to the original position. */
    MLRightLeft,
    /** Continuously scrolls left (with a pause at the original position if animationDelay is set). See the `trailingBuffer` property to define a spacing between the repeating strings.*/
    MLContinuous,
    /** Continuously scrolls right (with a pause at the original position if animationDelay is set). See the `trailingBuffer` property to define a spacing between the repeating strings.*/
    MLContinuousReverse
};

 

部分核心代码:

1、在initWithFrame、initWithCoder中调用的,在setupLabel方法中完成了第一个UILabel的创建,和部分属性的初始化。同时通知也是这里完成注册的。

 

- (void)setupLabel {



    // Basic UILabel options override

    self.clipsToBounds = YES;

    self.numberOfLines = 1;



    // Create first sublabel

    self.subLabel = [[UILabel alloc] initWithFrame:self.bounds];

    self.subLabel.tag = 700;

    self.subLabel.layer.anchorPoint = CGPointMake(0.0f, 0.0f);



    [self addSubview:self.subLabel];



    // Setup default values

    _animationCurve = UIViewAnimationOptionCurveLinear;

    _labelize = NO;

    _holdScrolling = NO;

    _tapToScroll = NO;

    _isPaused = NO;

    _fadeLength = 0.0f;

    _animationDelay = 1.0;

    _animationDuration = 0.0f;

    _leadingBuffer = 0.0f;

    _trailingBuffer = 0.0f;



    // Add notification observers

    // Custom class notifications

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewControllerShouldRestart:) name:kMarqueeLabelControllerRestartNotification object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(labelsShouldLabelize:) name:kMarqueeLabelShouldLabelizeNotification object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(labelsShouldAnimate:) name:kMarqueeLabelShouldAnimateNotification object:nil];



    // UINavigationController view controller change notifications

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(observedViewControllerChange:) name:@"UINavigationControllerDidShowViewControllerNotification" object:nil];



    // UIApplication state notifications

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(restartLabel) name:UIApplicationDidBecomeActiveNotification object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(shutdownLabel) name:UIApplicationDidEnterBackgroundNotification object:nil];

}

 

2、更新label的frame,并开始滚动。

- (void)updateSublabelAndLocationsAndBeginScroll:(BOOL)beginScroll {

    if (!self.subLabel.text || !self.superview) {

        return;

    }

    // Calculate expected size

    CGSize expectedLabelSize = [self subLabelSize];

    // Invalidate intrinsic size

    [self invalidateIntrinsicContentSize];

    // Move to home

    [self returnLabelToOriginImmediately];

    // Configure gradient for the current condition

    [self applyGradientMaskForFadeLength:self.fadeLength animated:YES];

    // Check if label should scroll

    // Can be because: 1) text fits, or 2) labelization

    // The holdScrolling property does NOT affect this

    if (!self.labelShouldScroll) {

        // Set text alignment and break mode to act like normal label

        [self.subLabel setTextAlignment:[super textAlignment]];

        [self.subLabel setLineBreakMode:[super lineBreakMode]];

        CGRect labelFrame, unusedFrame;

        switch (self.marqueeType) {

            case MLContinuousReverse:

            case MLRightLeft:

                CGRectDivide(self.bounds, &unusedFrame, &labelFrame, self.leadingBuffer, CGRectMaxXEdge);

                labelFrame = CGRectIntegral(labelFrame);

                break;
            default:

                labelFrame = CGRectIntegral(CGRectMake(self.leadingBuffer, 0.0f, self.bounds.size.width - self.leadingBuffer, self.bounds.size.height));

                break;

        }
        self.homeLabelFrame = labelFrame;

        self.awayLabelFrame = labelFrame;

        // Remove any additional text layers (for MLContinuous)

        NSArray *labels = [self allSubLabels];

        for (UILabel *sl in labels) {

            if (sl != self.subLabel) {

                [sl removeFromSuperview];

            }

        }

        // Set sublabel frame calculated labelFrame

        self.subLabel.frame = labelFrame;



        return;

    }



    // Label DOES need to scroll



    [self.subLabel setLineBreakMode:NSLineBreakByClipping];



    // Spacing between primary and second sublabel must be at least equal to leadingBuffer, and at least equal to the fadeLength

    CGFloat minTrailing = MAX(MAX(self.leadingBuffer, self.trailingBuffer), self.fadeLength);



    switch (self.marqueeType) {

        case MLContinuous:

        case MLContinuousReverse:

        {

            CGFloat awayLabelOffset;

            if (self.marqueeType == MLContinuous) {

                self.homeLabelFrame = CGRectIntegral(CGRectMake(self.leadingBuffer, 0.0f, expectedLabelSize.width, self.bounds.size.height));

                awayLabelOffset = -(self.homeLabelFrame.size.width + minTrailing);

                self.awayLabelFrame = CGRectIntegral(CGRectOffset(self.homeLabelFrame, awayLabelOffset, 0.0f));

            } else {

                self.homeLabelFrame = CGRectIntegral(CGRectMake(self.bounds.size.width - (expectedLabelSize.width + self.leadingBuffer), 0.0f, expectedLabelSize.width, self.bounds.size.height));

                awayLabelOffset = (self.homeLabelFrame.size.width + minTrailing);

                self.awayLabelFrame = CGRectIntegral(CGRectOffset(self.homeLabelFrame, awayLabelOffset, 0.0f));

            }



            NSArray *labels = [self allSubLabels];

            if (labels.count < 2) {

                UILabel *secondSubLabel = [[UILabel alloc] initWithFrame:CGRectOffset(self.homeLabelFrame, -awayLabelOffset, 0.0f)];

                secondSubLabel.numberOfLines = 1;

                secondSubLabel.tag = 701;

                secondSubLabel.layer.anchorPoint = CGPointMake(0.0f, 0.0f);



                [self addSubview:secondSubLabel];

                labels = [labels arrayByAddingObject:secondSubLabel];

            }



            [self refreshSubLabels:labels];



            // Recompute the animation duration

            self.animationDuration = (self.rate != 0) ? ((NSTimeInterval) fabs(awayLabelOffset) / self.rate) : (self.scrollDuration);



            // Set sublabel frames

            for (UILabel *sl in [self allSubLabels]) {

                sl.frame = CGRectOffset(self.homeLabelFrame, -awayLabelOffset * (sl.tag - 700), 0.0f);

            }



            break;

        }



        case MLRightLeft:

        {

            self.homeLabelFrame = CGRectIntegral(CGRectMake(self.bounds.size.width - (expectedLabelSize.width + self.leadingBuffer), 0.0f, expectedLabelSize.width, self.bounds.size.height));

            self.awayLabelFrame = CGRectIntegral(CGRectMake(self.trailingBuffer, 0.0f, expectedLabelSize.width, self.bounds.size.height));



            // Calculate animation duration

            self.animationDuration = (self.rate != 0) ? ((NSTimeInterval)fabs(self.awayLabelFrame.origin.x - self.homeLabelFrame.origin.x) / self.rate) : (self.scrollDuration);



            // Set frame and text

            self.subLabel.frame = self.homeLabelFrame;



            // Enforce text alignment for this type

            self.subLabel.textAlignment = NSTextAlignmentRight;



            break;

        }



        case MLLeftRight:

        {

            self.homeLabelFrame = CGRectIntegral(CGRectMake(self.leadingBuffer, 0.0f, expectedLabelSize.width, expectedLabelSize.height));

            self.awayLabelFrame = CGRectIntegral(CGRectOffset(self.homeLabelFrame, self.bounds.size.width - (expectedLabelSize.width + self.leadingBuffer + self.trailingBuffer), 0.0));



            // Calculate animation duration

            self.animationDuration = (self.rate != 0) ? ((NSTimeInterval)fabs(self.awayLabelFrame.origin.x - self.homeLabelFrame.origin.x) / self.rate) : (self.scrollDuration);



            // Set frame

            self.subLabel.frame = self.homeLabelFrame;



            // Enforce text alignment for this type

            self.subLabel.textAlignment = NSTextAlignmentLeft;



            break;

        }



        default:

        {

            // Something strange has happened

            self.homeLabelFrame = CGRectZero;

            self.awayLabelFrame = CGRectZero;



            // Do not attempt to begin scroll

            return;

            break;

        }



    } //end of marqueeType switch



    if (!self.tapToScroll && !self.holdScrolling && beginScroll) {

        [self beginScroll];

    }

}

 

3、进行key frame动画,动画完成后继续进行动画。。。

 

- (void)scrollContinuousWithInterval:(NSTimeInterval)interval after:(NSTimeInterval)delayAmount {

    // Check for conditions which would prevent scrolling

    if (![self labelReadyForScroll]) {

        return;

    }



    // Return labels to home (cancel any animations)

    [self returnLabelToOriginImmediately];



    // Call pre-animation method

    [self labelWillBeginScroll];



    // Animate

    [CATransaction begin];



    // Set Duration

    [CATransaction setAnimationDuration:(delayAmount + interval)];



    // Create animation for gradient, if needed

    if (self.fadeLength != 0.0f) {

        CAKeyframeAnimation *gradAnim = [self keyFrameAnimationForGradientFadeLength:self.fadeLength

                                                                            interval:interval

                                                                               delay:delayAmount];

        [self.layer.mask addAnimation:gradAnim forKey:@"gradient"];

    }



    MLAnimationCompletionBlock completionBlock = ^(BOOL finished) {

        if (!finished) {

            // Do not continue into the next loop

            return;

        }

        // Call returned home method

        [self labelReturnedToHome:YES];

        // Check to ensure that:

        // 1) We don't double fire if an animation already exists

        // 2) The instance is still attached to a window - this completion block is called for

        //    many reasons, including if the animation is removed due to the view being removed

        //    from the UIWindow (typically when the view controller is no longer the "top" view)

        if (self.window && ![self.subLabel.layer animationForKey:@"position"]) {

            // Begin again, if conditions met

            if (self.labelShouldScroll && !self.tapToScroll && !self.holdScrolling) {

                [self scrollContinuousWithInterval:interval after:delayAmount];

            }

        }

    };



    // Create animations for sublabel positions

    NSArray *labels = [self allSubLabels];

    CGFloat offset = 0.0f;

    for (UILabel *sl in labels) {

        // Create values, bumped by the offset

        NSArray *values = @[[NSValue valueWithCGPoint:MLOffsetCGPoint(self.homeLabelFrame.origin, offset)],      // Initial location, home

                            [NSValue valueWithCGPoint:MLOffsetCGPoint(self.homeLabelFrame.origin, offset)],      // Initial delay, at home

                            [NSValue valueWithCGPoint:MLOffsetCGPoint(self.awayLabelFrame.origin, offset)]];     // Animation to home



        CAKeyframeAnimation *awayAnim = [self keyFrameAnimationForProperty:@"position"

                                                                    values:values

                                                                  interval:interval

                                                                     delay:delayAmount];

        // Attach completion block to subLabel

        if (sl == self.subLabel) {

            [awayAnim setValue:completionBlock forKey:kMarqueeLabelAnimationCompletionBlock];

        }



        // Add animation

        [sl.layer addAnimation:awayAnim forKey:@"position"];



        // Increment offset

        offset += (self.homeLabelFrame.origin.x - self.awayLabelFrame.origin.x);

    }



    [CATransaction commit];

}

 

当然,MarqueeLabel中不仅仅只有这几个重要的方法,例如还有applyGradientMaskForFadeLength、keyFrameAnimationForGradientFadeLength等。有兴趣的童鞋可以自己去完整的看一遍。

 

 

可以学习到的内容:

1、NSMutableAttributedString的使用。

2、CAKeyframeAnimation、CABasicAnimation、CAAnimationGroup的使用。

 

本文来自Awnlab.com麦芒实验室,转载请注明出处,谢谢合作。