preface

When making the requirement of full screen, the progress bar will change from “almost impossible exposure” in half-screen background to “high frequency exposure” in full-screen scene, so it is necessary to create a silky and highly available progress bar. Just like when I Debug until 4 o ‘clock in the morning, it is to solve the animation problem of progress bar after suspension.

Today, take stock of the architecture, design logic, and potholes of the progress bar.

The code covered in this article has been open-source on Github: Building a highly available progress bar

The interface is introduced

Bncommonprogressbar. h // change progress, animateWithDuration is the incoming animation time - (void)setValue:(CGFloat)value; - (void)setValue:(CGFloat)value animateWithDuration:(NSTimeInterval)duration time:(NSTimeInterval)time; - (void)setValue:(CGFloat)value animateWithDuration:(NSTimeInterval)duration completion:(void (^__nullable)(BOOL finished))completion; // Reset all states to reset the progress to 0 - (void)reset; // pauseAnimation - (void)pauseAnimation; // Restore animation - (void)resumeAnimation; - (void)removeProgressAnimation;Copy the code

Why does UISlider not meet the goal of “high availability”?

Before stating that UISlider does not meet the goal of “high availability”, let’s consider what criteria a progress bar meets to qualify as “high availability”.

I came up with four goals:

    1. The UI is highly customizable
    1. Smooth back and forth drawing
    1. Customizable response range
    1. Response gesture, and no lag problem

Among them, UISlider can satisfy 3 and 4, because UISlider is a component provided by the system, and the item “UI can be highly customized” definitely does not meet the requirement.

In addition, UISlider’s processing of animation is not strong enough. In the scene of video playback, the video player will regularly call back the video playback progress with high frequency, and the animation updating progress should be smooth enough. But in fact, the effect of using UISlider is as follows:

Therefore, UISlider does not meet the second point: “smooth callback drawing”, and in the case of video number scene, the video progress callback updates the progress bar progress is a high-exposure scene, so we must make this animation smooth enough.

In this context, abandoning UISlider and customizing the progress bar is the only option.

Customize a “high availability” progress bar

BNCommonProgressBar is a class name for the progress bar.

BNCommonProgressBar is designed to meet the requirements:

    1. Goal: UI highly customizable –> Solution: Custom UI
    1. Goal: Smooth callback drawing –> solution: animation processing
    1. Target: customizable response range –> Solution: gesture range processing
    1. Target: response gesture, and no stuck problem –> scheme: drag gesture processing, stuck problem processing

So the BNCommonProgressBar design is divided into four modules.

(1) Custom UI

BNCommonProgressBar initialsbar

- (instancetype)initWithFrame:(CGRect)frame
                    barHeight:(CGFloat)progressBarHeight
                    dotHeight:(CGFloat)dotHeight
                 defaultColor:(UIColor *)defaultColor
              inProgressColor:(UIColor *)inProgressColor
                    dragColor:(UIColor *)dragColor
                 cornerRadius:(CGFloat)cornerRadius
         progressBarIconImage:(UIImage *)progressBarIconImage
         enablePanProgressIcon:(BOOL)enablePanProgressIcon;
Copy the code

Allows the service layer to configure the height of the progress bar, the height of the progress dot, the default color, the color when the progress is dragged, and whether to allow dragging, which has a higher degree of customization than UISlider.

If these interfaces are not enough, you can use BNCommonProgressBar initializer to add controls and components to the initialization method. This will not affect the progress bar animation/gesture function.

(2) Callback progress processing

1. Problem analysis

Why can’t the progress bar stop immediately after a pause is triggered, instead of sliding a distance to stop?

By looking at the code for the progress bar implementation, I found a clue:

The “position of the progress bar” is changed by the “player’s progress timing callback”

The player triggers a callback method about every 0.25 second to tell the progress bar where to move to for the next 0.25 second.

Progress bar To achieve smooth progress changes, using [UIView animationWithDuration:] animation.

So here’s the question:

Callback if in 2 seconds, the player told the position of the progress bar next 0.25 seconds, and then trigger the [UIView animationWithDuration: 0.25] of animation,

If the user clicks pause at 2.01 seconds, if the animation is not paused at this moment, the progress bar will move the remaining (0.25-0.01) seconds of animation, and the performance of pause and slide will also appear.

There are two most intuitive solutions to this problem:

Plan 1: Increase the frequency of player callbacks

When the frequency is high enough, the duration interval of [UIView animationWithDuration:] becomes smaller, and the performance of pausing while gliding is weakened

There are two problems with this scheme:

  1. Increasing the frequency of player callbacks only attenuates the performance of coasting, but it doesn’t really solve the coasting problem. When the same progress bar is referenced on the iPad, the progress bar becomes longer and the problem is still exposed

  2. Simply adding player callbacks to solve coasting problems is too costly and unnecessary.

Scheme 2: Pause the animation in progress

Here we will introduce and add some knowledge about Layer animation around suspending CoreAnimation.

(1) Implementation process

Plan 2 has an intuitive step:

  • A. Record the position of the progress bar when the video is paused

  • B. Then stop the animation of the progress bar

  • C. When the playback resumes, restore the animation from the last recorded position.

A. Record the position of the progress bar when the video is paused

The first question is: Which property of the progress bar View should be logged?

Can I record view.x directly?

In fact, it won’t, if we record [UIView animationWithDuration:] view.x as A before it happens, view.x as C after animation, and B during animation.

____I________I_______I___ Starting point A pause B end point CCopy the code

You’ll notice that as soon as the animation starts, regardless of whether the animation ends or not, you’ll always go to C via View. x, not B at the moment the animation is paused.

Even if you monitor view.layer.frame with KVO, you’ll see that there are no callbacks to the animation during changes.

Why is that?

CALayer layer tree

As we all know, UIView is a wrapper around CALayer. The CALayer class is similar in concept to UIView, and is also a rectangular block managed by a hierarchical tree. It can also contain content (like images, text, or background colors) and manage the position of sublayers. They have methods and properties to animate and transform. The biggest difference with UIView is that CALayer doesn’t handle user interaction.

CALayer and UIView have a hierarchical Tree structure, called Layer Tree, or Model Tree.

What’s the use of these three layer trees? What does Core Animation do? Because these three layers only show their characteristics and usefulness in the core animation. Here’s what the official document says:

  • The objects in the model layer tree are the objects with which the application interacts. The object in this tree is the model object that stores the target value of any animation. Each time you change the properties of the layer, one of these objects is used.

  • Indicates that the object in the layer tree contains the flying median of any animation that is running. The layer tree object contains the target value of the animation, and the object in the presentation tree reflects the current value displayed on the screen. You should never modify objects in this tree. Instead, you can use these objects to read the current animation values, perhaps in order to create a new animation from those values.

  • Objects in the render layer tree perform the actual Animation and are private animations of Core Animation.

In other words, there are two properties in the layer tree that we can actually use during development: modelLayer (modelLayer) and presentationLayer (presentationLayer).

(Render layers do not provide direct attributes for us to use in CALayer, they are private to Core Animation)

What is modelLayer?

ModelLayer is actually carrying various data of the final state of the layer. During the development process, we assign values to various parameters of the layer, in fact, we assign values to layer.modellayer.

Layer == view.layer.modelLayer.

Since modelLayer is the final value that we set during animation, there will be no change in view.layer.frame during animation execution.

What is the presentationLayer?

The presentationLayer is our main character. The presentationLayer refers to the layer that is displayed on the screen in real time. In Core Animation, you can use this property to get the data of the animation layer at each time during the animation. In this way, if you need to do something during the animation process, you can dynamically get the relevant data on the layer.

So when executing a Core animation, the presentationLayer changes, but the modelLayer does not.

The presentationLayer can be used for a variety of purposes, such as scrolling in a video. If you’re animating with Layer, and you’re scrolling, and you need to click on it to do what you need to do, you’ll need the presentationLayer. Based on the method of hintTest:

[self. Layer. PresentationLayer hitTest: point] / / determines if you click on which barrageCopy the code
B. Stop the animation of the progress bar

There are many ways to stop the core animation animation, layer. RemoveAllAnimations is one of them.

But layer. RemoveAllAnimations cannot achieve our desired results, for example:

____I________I_______I___ Starting point A pause B end point CCopy the code

At pause B, call removeAllAnimations. The animation will stop, but progress will jump to the final state C instead of stopping at B, so what we need is an action that can work with pauseAnimation instead of removeAnimation.

Although CALayer does not provide pauseAnimation’s interface, we can use CALayer’s time model to implement pause effects.

CAMediaTiming protocol

The CAMediaTiming protocol does not have much content, the header file is listed here.

@protocol CAMediaTiming

@property CFTimeInterval beginTime;
@property CFTimeInterval duration;
@property float speed;
@property CFTimeInterval timeOffset;
@property float repeatCount;
@property CFTimeInterval repeatDuration;
@property BOOL autoreverses;
@property(copy) CAMediaTimingFillMode fillMode;

@end
Copy the code

CALayer implements the CAMediaTiming protocol. CALayer implements a hierarchical time system through the CAMediaTiming protocol. (Besides CALayer,CAAnimation also adopts this protocol to implement the animation time system.)

beginTime

Both layers and animations have a concept of Timeline, and their beginTime is the start time relative to the parent object. Although it is not specified in apple’s documentation, code testing shows that by default all CALayer layers have the same timeline, beginTime is 0, and the absolute time converted to the current Layer is the absolute time. Therefore, for layers, although they are created in different order, their timelines are the same (as long as they do not actively change the beginTime of a layer), so we can imagine that all layers start their timelines by default after the system restarts.

But the time line of animation is different. When an animation is created and added to a Layer, it will be copied first and used to add the current Layer. When the CA transaction is submitted, if the beginTime of the animation in the Layer is 0, the beginTime will be set to the current time of the current Layer, making the animation move The painting began at once. If you want an animation to be added directly to the layer to be executed later, you can manually set the beginTime of the animation, but note that the beginTime should be CACurrentMediaTime()+ the number of seconds delayed, because beginTime refers to a time in the parent object’s timeline, when the parent object of the animation is the layer being added, and the current time of the layer is actually [Layer ConvertTime: CACurrentMediaTime () fromLayer: nil], actually equals CACurrentMediaTime (), then again in this layer of time online to wait a certain number of seconds that gets the result of the above.

timeOffset

The timeOffset may be one of the more difficult attributes to understand, and the official document does not explain it clearly. Local time is also divided into two types: Either active local time or basic local time. TimeOffset is the offset of the active local time. If a duration is 5 seconds, set timeOffset to 2(or 7, mod 5 to 2), then the animation will run from 2 seconds to 5 seconds, and then 0 to 2 seconds to complete the animation.

speed

The speed attribute is used to set the passing speed of the current object’s time stream relative to the parent object’s time stream. For example, if beginTime is 0 and speed is 2, then 1 second of the animation is equal to 2 seconds of the parent object’s time stream. For example, if a layer with speed 2 and all of its parents have speed 1, and it has a subLayer with speed 2, then an 8-second animation that runs in this subLayer takes only 2 seconds (8 / (2 * 2)). So speed has the effect of stacking.

Pause and resume: pause and resume:

- (void)pauseLayer:(CALayer *)layer {CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil]; Layer. speed = 0.0; Layer. timeOffset = pausedTime; layer.timeOffset = pausedTime; } - (void)resumeLayer:(CALayer *)layer { CFTimeInterval pausedTime = layer.timeOffset; // 1. Let CALayer's time continue to go layer.speed = 1.0; // 2. Cancel the layer.timeOffset = 0.0; // 3. Cancel layer.beginTime = 0.0; // 4. Calculate the pause time (CACurrentMediaTime()-pausedTime) CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime; // 5. Set the start time relative to the parent coordinate system (timeSincePause backward) layer.beginTime = timeSincePause; }Copy the code

The CACurrentMediaTime() used in the above method, also known as Mach time, is a global time concept in CoreAnimation.

Mach times are global for all processes on the device-but not for different devices-but this is enough to facilitate reference points for animations.

The value returned by this function doesn’t really matter (it returns the number of seconds since the device was last booted, not what you care about), what it really does is provide a relative value to the time measurement of the animation. Note that Mach time can be paused while the device is asleep, meaning that all CAAnimations (based on Mach time) can also be paused.

(3) gesture range processing

The BNCommonProgressBar handles the hitTest:withEvent: method. The response range can be macro defined or passed in by the business layer. By default, the BNResponseWidHeight/2 response range is increased.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGRect respRect = CGRectMake(- BNResponseWidHeight/ 2, - BNResponseWidHeight / 2, self.width + BNResponseWidHeight, self.height + BNResponseWidHeight);
    if (CGRectContainsPoint(respRect, point)) {
        return self;
    }
    return [super hitTest:point withEvent:event];
}
Copy the code

(four) drag gesture processing, stuck problem processing

Drag-and-drop gesture handling code in onPanProgressIcon:.


This public account will continue to update the technical solution, pay attention to the industry technology trends, pay attention to the cost is not high, miss dry goods loss is not small. Left left left

This article is published by OpenWrite!