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