在 Swift 中使用渐变实现圆形加载指示器

问题描述

我有一个圆形加载指示器,如下面的屏幕截图所示:

enter image description here

注意:有一个“外环”,但我已将其设置为隐藏。

为此有两个类;下面的UICircularProgressRing

final public class UICircularProgressRing: UICircularRing {
    // MARK: Members

    /**
     The delegate for the UICircularRing

     ## Important ##
     When progress is done updating via UICircularRing.setValue(_:),the
     finishedUpdatingProgressFor(_ ring: UICircularRing) will be called.

     The ring will be passed to the delegate in order to keep track of
     multiple ring updates if needed.

     ## Author
     Luis padron
     */
    public weak var delegate: UICircularProgressRingDelegate?

    /**
     The value property for the progress ring.

     ## Important ##
     Default = 0

     Must be a non-negative value. If this value falls below `minValue` it will be
     clamped and set equal to `minValue`.

     This cannot be used to get the value while the ring is animating,to get
     current value while animating use `currentValue`.

     The current value of the progress ring after animating,use startProgress(value:)
     to alter the value with the option to animate and have a completion handler.

     ## Author
     Luis padron
     */
    @IBInspectable public var value: CGFloat = 0 {
        didSet {
            if value < minValue {
                #if DEBUG
                print("Warning in: \(#file):\(#line)")
                print("Attempted to set a value less than minValue,value has been set to minValue.\n")
                #endif
                ringLayer.value = minValue
            } else if value > maxValue {
                #if DEBUG
                print("Warning in: \(#file):\(#line)")
                print("Attempted to set a value greater than maxValue,value has been set to maxValue.\n")
                #endif
                ringLayer.value = maxValue
            } else {
                ringLayer.value = value
            }
        }
    }

    /**
     The current value of the progress ring

     This will return the current value of the progress ring,if the ring is animating it will be updated in real time.
     If the ring is not currently animating then the value returned
     will be the `value` property of the ring

     ## Author
     Luis padron
     */
    public var currentValue: CGFloat? {
        return isAnimating ? layer.presentation()?.value(forKey: .value) as? CGFloat : value
    }

    /**
     The minimum value for the progress ring. ex: (0) -> 100.

     ## Important ##
     Default = 0.0

     Must be a non-negative value,the absolute value is taken when setting this property.

     The `value` of the progress ring must NOT fall below `minValue` if it does the `value` property is clamped
     and will be set equal to `value`,you will receive a warning message in the console.

     Making this value greater than

     ## Author
     Luis padron
     */
    @IBInspectable public var minValue: CGFloat = 0.0 {
        didSet { ringLayer.minValue = minValue }
    }

    /**
     The maximum value for the progress ring. ex: 0 -> (100)

     ## Important ##
     Default = 100.0

     Must be a non-negative value,the absolute value is taken when setting this property.

     Unlike the `minValue` member `value` can extend beyond `maxValue`. What happens in this case
     is the inner ring will do an extra loop through the outer ring,this is not noticible however.


     ## Author
     Luis padron
     */
    @IBInspectable public var maxValue: CGFloat = 100.0 {
        didSet { ringLayer.maxValue = maxValue }
    }

    /**
     The type of animation function the ring view will use

     ## Important ##
     Default = .easeInEaSEOut

     ## Author
     Luis padron
     */
    public var animationTimingFunction: camediatimingFunctionName = .easeInEaSEOut {
        didSet { ringLayer.animationTimingFunction = animationTimingFunction }
    }

    /**
     The formatter responsible for formatting the
     value of the progress ring into a readable text string
     which is then displayed in the label of the ring.

     Default formatter is of type `UICircularProgressRingFormatter`.

     ## Author
     Luis padron
     */
    public var valueFormatter: UICircularRingValueFormatter = UICircularProgressRingFormatter() {
        didSet { ringLayer.valueFormatter = valueFormatter }
    }

    /**
     Typealias for the startProgress(:) method closure
     */
    public typealias ProgressCompletion = (() -> Void)

    /// The completion block to call after the animation is done
    private var completion: ProgressCompletion?

    // MARK: API

    /**
     Sets the current value for the progress ring,calling this method while ring is
     animating will cancel the prevIoUsly set animation and start a new one.

     - Parameter to: The value to be set for the progress ring
     - Parameter duration: The time interval duration for the animation
     - Parameter completion: The completion closure block that will be called when
     animtion is finished (also called when animationDuration = 0),default is nil

     ## Important ##
     Animation duration = 0 will cause no animation to occur,and value will instantly
     be set.

     ## Author
     Luis padron
     */
    public func startProgress(to value: CGFloat,duration: TimeInterval,completion: ProgressCompletion? = nil) {
        // Store the completion event locally
        self.completion = completion

        // call super class helper function to begin animating layer
        startAnimation(duration: duration) {
            self.delegate?.didFinishProgress(for: self)
            self.completion?()
        }

        self.value = value
    }

    /**
     Pauses the currently running animation and halts all progress.

     ## Important ##
     This method has no effect unless called when there is a running animation.
     You should call this method manually whenever the progress ring is not in an active view,for example in `viewWilldisappear` in a parent view controller.

     ## Author
     Luis padron & Nicolai Cornelis
     */
    public func pauseProgress() {
        // call super class helper to stop layer animation
        pauseAnimation()
        delegate?.didPauseProgress(for: self)
    }

    /**
     Continues the animation with its remaining time from where it left off before it was paused.
     This method has no effect unless called when there is a paused animation.
     You should call this method when you wish to resume a paused animation.

     ## Author
     Luis padron & Nicolai Cornelis
     */
    public func continueProgress() {
        // call super class helper to continue layer animation
        continueAnimation {
            self.delegate?.didFinishProgress(for: self)
            self.completion?()
        }

        delegate?.didContinueProgress(for: self)
    }

    /**
     Resets the progress back to the `minValue` of the progress ring.
     Does **not** perform any animations

     ## Author
     Luis padron
     */
    public func resetProgress() {
        // call super class helper to reset animation layer
        resetAnimation()
        value = minValue
        // Remove reference to the completion block
        completion = nil
    }

    // MARK: Overrides

    override func initialize() {
        super.initialize()
        ringLayer.ring = self
        ringLayer.value = value
        ringLayer.maxValue = maxValue
        ringLayer.minValue = minValue
        ringLayer.valueFormatter = valueFormatter
    }

    override func didUpdateValue(newValue: CGFloat) {
        super.didUpdateValue(newValue: newValue)
        delegate?.didUpdateProgressValue(for: self,to: newValue)
    }

    override func willdisplayLabel(label: UILabel) {
        super.willdisplayLabel(label: label)
        delegate?.willdisplayLabel(for: self,label)
    }
}

还有它继承的 UICircularRing

import UIKit

/**

 # UICircularRing

 This is the base class of `UICircularProgressRing` and `UICircularTimerRing`.
 You should not instantiate this class,instead use one of the concrete classes provided
 or subclass and make your own.

 This is the UIView subclass that creates and handles everything
 to do with the circular ring.

 This class has a custom CAShapeLayer (`UICircularRingLayer`) which
 handels the drawing and animating of the view

 ## Author
 Luis padron

 */
@IBDesignable open class UICircularRing: UIView {

    // MARK: Circle Properties

    /**
     Whether or not the progress ring should be a full circle.

     What this means is that the outer ring will always go from 0 - 360 degrees and
     the inner ring will be calculated accordingly depending on current value.

     ## Important ##
     Default = true

     When this property is true any value set for `endAngle` will be ignored.

     ## Author
     Luis padron

     */
    @IBInspectable open var fullCircle: Bool = true {
        didSet { ringLayer.setNeedsdisplay() }
    }

    // MARK: View Style

    /**
     The style of the progress ring.

     Type: `UICircularRingStyle`

     The five styles include `inside`,`ontop`,`dashed`,`dotted`,and `gradient`

     ## Important ##
     Default = UICircularRingStyle.inside

     ## Author
     Luis padron
     */
    open var style: UICircularRingStyle = .inside {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     The options for a gradient ring.

     If this is non-`nil` then a gradient style will be applied.

     ## Important ##
    Default = `nil`
    */
    open var gradientOptions: UICircularRingGradientOptions? = nil {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     A toggle for showing or hiding the value label.
     If false the current value will not be shown.

     ## Important ##
     Default = true

     ## Author
     Luis padron
     */
    @IBInspectable public var shouldShowValueText: Bool = true {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     A toggle for showing or hiding the value knob when current value == minimum value.
     If false the value knob will not be shown when current value == minimum value.

     ## Important ##
     Default = false

     ## Author
     Tom Knapen
     */
    @IBInspectable public var shouldDrawMinValueKnob: Bool = false {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     Style for the value knob,default is `nil`.

     ## Important ##
     If this is `nil`,no value knob is shown.

    */
    open var valueKnobStyle: UICircularRingValueKnobStyle? {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     The start angle for the entire progress ring view.

     Please note that Cocoa Touch uses a clockwise rotating unit circle.
     I.e: 90 degrees is at the bottom and 270 degrees is at the top

     ## Important ##
     Default = 0 (degrees)

     Values should be in degrees (they're converted to radians internally)

     ## Author
     Luis padron
     */
    @IBInspectable open var startAngle: CGFloat = 0 {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     The end angle for the entire progress ring

     Please note that Cocoa Touch uses a clockwise rotating unit circle.
     I.e: 90 degrees is at the bottom and 270 degrees is at the top

     ## Important ##
     Default = 360 (degrees)

     Values should be in degrees (they're converted to radians internally)

     ## Author
     Luis padron
     */
    @IBInspectable open var endAngle: CGFloat = 360 {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     Determines if the progress ring should animate in reverse

     ## Important ##
     Default = false

    */
    open var isReverse: Bool = false {
        didSet { ringLayer.isReverse = isReverse }
    }

    // MARK: Outer Ring properties

    /**
     The width of the outer ring for the progres bar

     ## Important ##
     Default = 10.0

     ## Author
     Luis padron
     */
    @IBInspectable open var outerRingWidth: CGFloat = 10.0 {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     The color for the outer ring

     ## Important ##
     Default = UIColor.gray

     ## Author
     Luis padron
     */
    @IBInspectable open var outerRingColor: UIColor = UIColor.gray {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     The style for the tip/cap of the outer ring

     Type: `CGLineCap`

     ## Important ##
     Default = CGLineCap.butt

     This is only noticible when ring is not a full circle.

     ## Author
     Luis padron
     */
    open var outerCapStyle: CGLineCap = .butt {
        didSet { ringLayer.setNeedsdisplay() }
    }

    // MARK: Inner Ring properties

    /**
     The width of the inner ring for the progres bar

     ## Important ##
     Default = 5.0

     ## Author
     Luis padron
     */
    @IBInspectable open var innerRingWidth: CGFloat = 5.0 {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     The color of the inner ring for the progres bar

     ## Important ##
     Default = UIColor.blue

     ## Author
     Luis padron
     */
    @IBInspectable open var innerRingColor: UIColor = UIColor.blue {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     The spacing between the outer ring and inner ring

     ## Important ##
     This only applies when using `ringStyle` = `.inside`

     Default = 1

     ## Author
     Luis padron
     */
    @IBInspectable open var innerRingSpacing: CGFloat = 1 {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     The style for the tip/cap of the inner ring

     Type: `CGLineCap`

     ## Important ##
     Default = CGLineCap.round

     ## Author
     Luis padron
     */
    open var innerCapStyle: CGLineCap = .round {
        didSet { ringLayer.setNeedsdisplay() }
    }

    // MARK: Label

    /**
     The text color for the value label field

     ## Important ##
     Default = UIColor.black


     ## Author
     Luis padron
     */
    @IBInspectable open var fontColor: UIColor = UIColor.black {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     The font to be used for the progress indicator.
     All font attributes are specified here except for font color,which is done
     using `fontColor`.


     ## Important ##
     Default = UIFont.systemFont(ofSize: 18)


     ## Author
     Luis padron
     */
    @IBInspectable open var font: UIFont = UIFont.systemFont(ofSize: 18) {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     This returns whether or not the ring is currently animating

     ## Important ##
     Get only property

     ## Author
     Luis padron
     */
    open var isAnimating: Bool {
        return ringLayer.animation(forKey: .value) != nil
    }

    /**
     The direction the circle is drawn in
     Example: true -> clockwise

     ## Important ##
     Default = true (draw the circle clockwise)

     ## Author
     Pete Walker
     */
    @IBInspectable open var isClockwise: Bool = true {
        didSet { ringLayer.setNeedsdisplay() }
    }

    /**
     Typealias for animateProperties(duration:animations:completion:) fucntion completion
     */
    public typealias PropertyAnimationCompletion = (() -> Void)

    // MARK: Private / internal

    /**
     Set the ring layer to the default layer,cated as custom layer
     */
    var ringLayer: UICircularRingLayer {
        // swiftlint:disable:next force_cast
        return layer as! UICircularRingLayer
    }

    /// This variable stores how long remains on the timer when it's paused
    private var pausedTimeRemaining: TimeInterval = 0

    /// Used to determine when the animation was paused
    private var animationPauseTime: CFTimeInterval?

    /// This stores the animation when the timer is paused. We use this variable to continue the animation where it left off.
    /// See https://stackoverflow.com/questions/7568567/restoring-animation-where-it-left-off-when-app-resumes-from-background
    var snapshottedAnimation: CAAnimation?

    /// The completion timer,also indicates whether or not the view is animating
    var animationCompletionTimer: Timer?

    typealias AnimationCompletion = () -> Void

    // MARK: Methods

    /**
     Overrides the default layer with the custom UICircularRingLayer class
     */
    override open class var layerClass: AnyClass {
        return UICircularRingLayer.self
    }

    /**
     Overriden public init to initialize the layer and view
     */
    override public init(frame: CGRect) {
        super.init(frame: frame)
        // Call the internal initializer
        initialize()
    }

    /**
     Overriden public init to initialize the layer and view
     */
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        // Call the internal initializer
        initialize()
    }

    /**
     This method initializes the custom CALayer to the default values
     */
    func initialize() {
        // This view will become the value delegate of the layer,which will call the updateValue method when needed
        ringLayer.ring = self

        // Helps with pixelation and blurriness on retina devices
        ringLayer.contentsScale = UIScreen.main.scale
        ringLayer.shouldRasterize = true
        ringLayer.rasterizationScale = UIScreen.main.scale * 2
        ringLayer.masksToBounds = false

        backgroundColor = UIColor.clear
        ringLayer.backgroundColor = UIColor.clear.cgColor

        NotificationCenter.default.addobserver(self,selector: #selector(restoreAnimation),name: UIApplication.willEnterForegroundNotification,object: nil)

        NotificationCenter.default.addobserver(self,selector: #selector(snapshotAnimation),name: UIApplication.willResignActiveNotification,object: nil)
    }

    /**
     Overriden because of custom layer drawing in UICircularRingLayer
     */
    open override func draw(_ rect: CGRect) {
        super.draw(rect)
    }

    // MARK: Internal API

    /**
     These methods are called from the layer class in order to notify
     this class about changes to the value and label display.

     In this base class they do nothing.
     */

    func didUpdateValue(newValue: CGFloat) { }

    func willdisplayLabel(label: UILabel) { }

    /**
     These functions are here to allow reuse between subclasses.
     They handle starting,pausing and resetting an animation of the ring.
    */

    func startAnimation(duration: TimeInterval,completion: @escaping AnimationCompletion) {
        if isAnimating {
            animationPauseTime = nil
        }

        ringLayer.timeOffset = 0
        ringLayer.beginTime = 0
        ringLayer.speed = 1
        ringLayer.animated = duration > 0
        ringLayer.animationDuration = duration

        // Check if a completion timer is still active and if so stop it
        animationCompletionTimer?.invalidate()
        animationCompletionTimer = Timer.scheduledTimer(timeInterval: duration,target: self,selector: #selector(self.animationDidComplete),userInfo: completion,repeats: false)
    }

    func pauseAnimation() {
        guard isAnimating else {
            #if DEBUG
            print("""
                    UICircularProgressRing: Progress was paused without having been started.
                    This has no effect but may indicate that you're unnecessarily calling this method.
                    """)
            #endif
            return
        }

        snapshotAnimation()

        let pauseTime = ringLayer.convertTime(CACurrentMediaTime(),from: nil)
        animationPauseTime = pauseTime

        ringLayer.speed = 0.0
        ringLayer.timeOffset = pauseTime

        if let fireTime = animationCompletionTimer?.fireDate {
            pausedTimeRemaining = fireTime.timeIntervalSince(Date())
        } else {
            pausedTimeRemaining = 0
        }

        animationCompletionTimer?.invalidate()
        animationCompletionTimer = nil
    }

    func continueAnimation(completion: @escaping AnimationCompletion) {
        guard let pauseTime = animationPauseTime else {
            #if DEBUG
            print("""
                    UICircularRing: Progress was continued without having been paused.
                    This has no effect but may indicate that you're unnecessarily calling this method.
                    """)
            #endif
            return
        }

        restoreAnimation()

        ringLayer.speed = 1.0
        ringLayer.timeOffset = 0.0
        ringLayer.beginTime = 0.0

        let timeSincePause = ringLayer.convertTime(CACurrentMediaTime(),from: nil) - pauseTime

        ringLayer.beginTime = timeSincePause

        animationCompletionTimer?.invalidate()
        animationCompletionTimer = Timer.scheduledTimer(timeInterval: pausedTimeRemaining,selector: #selector(animationDidComplete),repeats: false)

        animationPauseTime = nil
    }

    func resetAnimation() {
        ringLayer.animated = false
        ringLayer.removeAnimation(forKey: .value)
        snapshottedAnimation = nil

        // Stop the timer and thus make the completion method not get fired
        animationCompletionTimer?.invalidate()
        animationCompletionTimer = nil
        animationPauseTime = nil

    }

    // MARK: API

    /**
     This function allows animation of the animatable properties of the `UICircularRing`.
     These properties include `innerRingColor,innerRingWidth,outerRingColor,outerRingWidth,innerRingSpacing,fontColor`.

     Simply call this function and inside of the animation block change the animatable properties as you would in any `UView`
     animation block.

     The completion block is called when all animations finish.
     */
    open func animateProperties(duration: TimeInterval,animations: () -> Void) {
        animateProperties(duration: duration,animations: animations,completion: nil)
    }

    /**
     This function allows animation of the animatable properties of the `UICircularRing`.
     These properties include `innerRingColor,animations: () -> Void,completion: PropertyAnimationCompletion? = nil) {
        ringLayer.shouldAnimateProperties = true
        ringLayer.propertyAnimationDuration = duration
        CATransaction.begin()
        CATransaction.setCompletionBlock {
            // Reset and call completion
            self.ringLayer.shouldAnimateProperties = false
            self.ringLayer.propertyAnimationDuration = 0.0
            completion?()
        }
        // Commit and perform animations
        animations()
        CATransaction.commit()
    }
}

// MARK: Helpers

extension UICircularRing {
    /**
     This method is called when the application goes into the background or when the
     ProgressRing is paused using the pauseProgress method.
     This is necessary for the animation to properly pick up where it left off.
     Triggered by UIApplicationWillResignActive.

     ## Author
     Nicolai Cornelis
     */
    @objc func snapshotAnimation() {
        guard let animation = ringLayer.animation(forKey: .value) else { return }
        snapshottedAnimation = animation
    }

    /**
     This method is called when the application comes back into the foreground or
     when the ProgressRing is resumed using the continueProgress method.
     This is necessary for the animation to properly pick up where it left off.
     Triggered by UIApplicationWillEnterForeground.

     ## Author
     Nicolai Cornelis
     */
    @objc func restoreAnimation() {
        guard let animation = snapshottedAnimation else { return }
        ringLayer.add(animation,forKey: AnimationKeys.value.rawValue)
    }

    /// Called when the animation timer is complete
    @objc func animationDidComplete(withTimer timer: Timer) {
        (timer.userInfo as? AnimationCompletion)?()
    }
}

extension UICircularRing {
    /// Helper enum for animation key
    enum AnimationKeys: String {
        case value
    }
}

我现在要写的是实现一个基于渐变的指示器,整个圆圈是可见的,然后在加载时填充。有点卡在这里;不知道什么是正确的方法

enter image description here

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...