Kotlin series - progress bar with arc

Keywords: Android Lambda Attribute Java

This article has authorized WeChat public number Guo Lin (guolin_blog) to reprint.

This is a progress bar with an arc. In fact, this control has been implemented for a long time, but I want to rewrite and optimize it with Kotlin and explain it.

Project GitHub: CircularArcProgressView

Design sketch

attribute

Name Format Description
capv_background_color color background color
capv_progress_color color Progress bar color
capv_progress_text_color color Progress text color
capv_percent float Percentage
capv_is_show_progress_text boolean Show progress text or not

Use

Import into your project

dependencies {
    implementation 'com.tanjiajun.widget:CircularArcProgressView:1.0.2'
}

Layout file

<com.tanjiajun.widget.CircularArcProgressView
    android:id="@+id/capv_first"
    android:layout_width="0dp"
    android:layout_height="30dp"
    android:layout_marginStart="16dp"
    android:layout_marginTop="16dp"
    android:layout_marginEnd="16dp"
    app:capv_background_color="@color/circular_arc_progress_view_first_background_color"
    app:capv_is_show_progress_text="true"
    app:capv_percent="0.8"
    app:capv_progress_color="@color/circular_arc_progress_view_first_progress_color"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

Kotlin

findViewById<CircularArcProgressView>(R.id.capv_first).startAnimator(duration = 2000)

Java

((CircularArcProgressView) findViewById(R.id.capv_first)).startAnimator(2000);

Source code analysis

Define the custom attribute, write the corresponding code to obtain the custom attribute, and expose some methods that need to be set by the user. The code is as follows:

/**
 * Set percent to show the progress.
 */
var percent: Float = 0f
    set(value) {
        var percent = value

        if (percent < 0f) {
            percent = 0f
        } else if (percent > 1f) {
            percent = 1f
        }

        if (percent != field) {
            field = percent
            invalidate()
        }
    }

init {
    attrs?.let { set ->
        context.obtainStyledAttributes(set, R.styleable.CircularArcProgressView).apply {
            bgColor =
                getColor(R.styleable.CircularArcProgressView_capv_background_color, Color.BLACK)
            progressColor =
                getColor(R.styleable.CircularArcProgressView_capv_progress_color, Color.RED)
            progressTextColor =
                getColor(
                    R.styleable.CircularArcProgressView_capv_progress_text_color,
                    Color.WHITE
                )
            getFloat(R.styleable.CircularArcProgressView_capv_percent, 0f).let {
                percent = it
            }
            isShowProgressText =
                getBoolean(
                    R.styleable.CircularArcProgressView_capv_is_show_progress_text,
                    false
                )
            recycle()
        }
    }
}

According to the width and height set by the user, draw a rounded rectangle with a radius of half the height. Pay attention to the padding property. This part is the background, and the code is as follows:

val halfHeight = height / 2f
val saveCount = canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null)

// Draw background.
backgroundRectF.left = paddingStart.toFloat()
backgroundRectF.top = paddingTop.toFloat()
backgroundRectF.right = width - paddingEnd.toFloat()
backgroundRectF.bottom = height - paddingBottom.toFloat()
canvas.drawRoundRect(backgroundRectF, halfHeight, halfHeight, backgroundPaint)

On the left side of the background rounded rectangle, draw another rounded rectangle with a radius of half the height. The width and height are the same as the background rounded rectangle, but the left and right coordinates will increase with the increase of percent. After drawing, the performance is to move to the right, and then use the PorterDuffXfermode to process the overlapping part, which is the progress. The code is as follows:

private val progressTextPaint by lazy {
    TextPaint().apply {
        isAntiAlias = true
        isDither = true
        style = Paint.Style.FILL
        color = progressTextColor
    }
}
// Draw progress.
progressRectF.left = -backgroundRectF.width() + percent * width
progressRectF.top = backgroundRectF.top
progressRectF.right = progressRectF.left + backgroundRectF.width()
progressRectF.bottom = backgroundRectF.bottom
canvas.drawRoundRect(progressRectF, halfHeight, halfHeight, progressPaint)
canvas.restoreToCount(saveCount)

Draw a percentage text according to the user's needs. The left and right coordinates also increase with the increase of percentage. After drawing, the performance also moves to the right, but it is located on the left side of the progress bar arc. Pay attention to accurately measure the width and height of the text. The code is as follows:

if (isShowProgressText && percent >= 0.1f) {
    progressTextPaint.run {
        textSize = halfHeight
        fontMetrics.let {
            val progressText = (percent * 100).toInt().toString() + "%"
            canvas.drawText(
                progressText,
                percent * width - progressTextPaint.measureText(progressText) - height / 5f,
                halfHeight - it.descent + (it.descent - it.ascent) / 2f,
                progressTextPaint
            )
        }
    }
}

Exposes an animation method.

/**
 * Start animator.
 *
 * @param timeInterpolator the interpolator to be used by this animation. The default value is
 * android.view.animation.AccelerateInterpolator.
 *
 * @param duration the length of the animation.
 */
@JvmOverloads
fun startAnimator(
    timeInterpolator: TimeInterpolator? = AccelerateInterpolator(),
    duration: Long
) =
    with(ObjectAnimator.ofFloat(this, "percent", 0f, percent)) {
        interpolator = timeInterpolator
        this.duration = duration
        start()
    }

PorterDuff.Mode

source

Why PorterDuff? In fact, it's two people. One is Thomas Porter, the other is Tom Duff. They published composing digital images in July 1984, describing 12 composition operators, which control the color of the image to be rendered and the content of the rendering target. Then this class also provides several other mixing modes besides those 12, but these are not determined by these two people It's just for convenience, so there are 18 kinds in total.

Source code

We can look at the PorterDuff class. There is an enumeration Mode in it. The code is as follows:

public enum Mode {

    CLEAR       (0),
    SRC         (1),
    DST         (2),
    SRC_OVER    (3),
    DST_OVER    (4),
    SRC_IN      (5),
    DST_IN      (6),
    SRC_OUT     (7),
    DST_OUT     (8),
    SRC_ATOP    (9),
    DST_ATOP    (10),
    XOR         (11),
    DARKEN      (16),
    LIGHTEN     (17),
    MULTIPLY    (13),
    SCREEN      (14),
    ADD         (12),
    OVERLAY     (15);

    Mode(int nativeInt) {
        this.nativeInt = nativeInt;
    }

    /**
     * @hide
     */
    @UnsupportedAppUsage
    public final int nativeInt;

}

PorterDuff has 18 modes in total. The names, pictures and descriptions of these modes are shown below. You can click on the pictures to view them. The pictures are as follows:

Delay attribute Lazy

The code of this control also uses the delay property Lazy. The code is as follows:

private val progressTextPaint by lazy {
    TextPaint().apply {
        isAntiAlias = true
        isDither = true
        style = Paint.Style.FILL
        color = progressTextColor
    }
}

We can see that the lazy function accepts a Lambda expression. As mentioned in the previous article, if the last parameter of the function is a Lambda expression, it can refer to the outside of the parentheses, and the parentheses can also be omitted. The call delay property has such characteristics that the first time you get the value of the property (call the get method), you will execute the Lambda expression passed to the function and And record the results. Subsequent calls to get() only return the recorded results.

We can look at the source code and provide three functions.

lazy(initializer: () -> T)

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

This function accepts a Lambda expression, returns lazy, and calls the SynchronizedLazyImpl function. We can know that it is safe for multiple threads to call this lazy function. The code is as follows:

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

We can see that * * Double Checked Locking * * is used to ensure thread safety.

###lazy(mode: LazyThreadSafetyMode, initializer: () -> T)

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

This function takes two parameters, one is LazyThreadSafetyMode, the other is Lambda expression, and returns Lazy. LazyThreadSafetyMode is an enumeration class. The code is as follows:

public enum class LazyThreadSafetyMode {

    SYNCHRONIZED,
    PUBLICATION,
    NONE,

}

Using SYNCHRONIZED can ensure that only one thread initializes an instance. The implementation details are also mentioned above. Using PUBLICATION allows multiple threads to initialize simultaneously, but only the first return value is used as the instance value. Using NONE will not guarantee any thread safety and related costs, so if you confirm that initialization always occurs in the same thread You can use this mode to reduce some performance overhead.

lazy(lock: Any?, initializer: () -> T)

public actual fun <T> lazy(lock: Any?, initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer, lock)

This function takes two parameters. One is that you use the specified object (lock) to synchronize. The other is the Lambda expression, which returns Lazy and calls the synchronized lazyimpl function. As mentioned above, we will not repeat it here.

@JvmOverloads

If you write a Kotlin function with default parameter values, only one method with all parameters can be called in Java. If you want to expose multiple overloads to Java callers, you can use the * * @ JvmOverloads * * annotation.

Take the code of this control as an example. The code is as follows:

class CircularArcProgressView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    // Omit implementation code
}

Decompile to Java code as follows:

public final class CircularArcProgressView extends View {

   @JvmOverloads
   public CircularArcProgressView(@NotNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
      // Omit implementation code
   }

  @JvmOverloads
   public CircularArcProgressView(@NotNull Context context, @Nullable AttributeSet attrs) {
      // Omit implementation code
   }

  @JvmOverloads
   public CircularArcProgressView(@NotNull Context context) {
      // Omit implementation code
   }

}

My GitHub: TanJiaJunBeyond

Android general framework: Android general framework (kotlin MVVM)

My Nuggets: Tan Jia Jun

My short book: Tan Jia Jun

My CSDN: Tan Jia Jun

Published 7 original articles, praised 0, visited 88
Private letter follow

Posted by bapi on Tue, 28 Jan 2020 01:19:39 -0800