Có thể bạn quan tâm: Cách Vẽ Chữ “p” Đẹp Và Chuyên Nghiệp: Hướng Dẫn Từng Bước Cho Người Mới Bắt Đầu
TÓM TẮT
- 1 Mở Đầu
- 2 1. Khái Niệm và Nguyên Lý Hoạt Động Của Circle ProgressBar
- 3 2. Vẽ Circle ProgressBar Bằng XML (ProgressBar Có Sẵn)
- 4 3. Tự Tạo Custom View Với Canvas
- 5 4. Sử Dụng Thư Viện Hỗ Trợ
- 6 5. Animation và Interpolator cho Circle ProgressBar
- 7 6. Tùy Chỉnh Màu Sắc, Gradient, Shadow
- 8 7. Kết Nối Với LiveData / ViewModel
- 9 8. Kiểm Thử và Tối Ưu Hiệu Suất
- 10 9. Kết Luận và Một Số Lưu Ý Quan Trọng
Mở Đầu
Circle ProgressBar (hoặc Circular Progress Indicator) là một trong những thành phần UI (User Interface) phổ biến trong các ứng dụng di động hiện đại, đặc biệt là trên nền tảng Android. Khác với ProgressBar dạng linear truyền thống, Circle ProgressBar cho phép hiển thị tiến độ dưới dạng vòng tròn, mang lại cảm giác trực quan và thẩm mỹ hơn cho người dùng. Trong bài viết này, chúng ta sẽ đi sâu vào cách vẽ Circle ProgressBar từ các khái niệm cơ bản, các phương pháp thực hiện (sử dụng XML, Canvas, hoặc thư viện bên thứ ba), tới các tùy chỉnh nâng cao như animation, gradient, shadow và tích hợp với ViewModel.
Bài viết được chia thành các phần chính:
- Khái niệm và nguyên lý hoạt động
- Cách vẽ Circle ProgressBar bằng XML (ProgressBar tích hợp sẵn)
- Tự tạo Custom View với Canvas
- Sử dụng thư viện hỗ trợ (Material Components, CircleProgressBar, MPAndroidChart, v.v.)
- Animation và Interpolator
- Tùy chỉnh màu sắc, gradient, shadow
- Kết nối với LiveData / ViewModel
- Kiểm thử và tối ưu hiệu suất
- Kết luận và một số lưu ý quan trọng
Mỗi phần sẽ cung cấp ví dụ code chi tiết, giải thích từng bước và những lưu ý thực tiễn để bạn có thể áp dụng ngay vào dự án của mình.
1. Khái Niệm và Nguyên Lý Hoạt Động Của Circle ProgressBar
1.1. Circle ProgressBar là gì?
Circle ProgressBar là một widget hiển thị trạng thái tiến độ dưới dạng một vòng tròn hoặc cung tròn. Thông thường, nó bao gồm:
- Background track: vòng tròn nền, thường có màu xám hoặc mờ, đại diện cho toàn bộ tiến độ (100%).
- Progress arc: cung tròn màu sắc hiển thị phần đã hoàn thành (từ 0% tới giá trị hiện tại).
- Optional thumb/indicator: một điểm hoặc hình ảnh di chuyển dọc theo cung để chỉ vị trí hiện tại.
- Text hoặc icon: hiển thị phần trăm, thời gian, hoặc biểu tượng ở giữa vòng.
1.2. Nguyên lý vẽ
Khi vẽ một Circle ProgressBar, chúng ta thực chất đang vẽ hai hình elip (hoặc vòng tròn) trên Canvas:
- Draw background circle – dùng
drawArchoặcdrawCirclevớiPaint.Style.STROKE. - Draw progress arc – dùng
drawArcvới góc bắt đầu (startAngle) và góc quét (sweepAngle).sweepAngleđược tính bằng công thức:
sweepAngle = (progress / maxProgress) 360
- Optional: Vẽ text trung tâm bằng
drawTexthoặc sử dụngTextViewoverlay.
1.3. Các thuộc tính quan trọng
| Thuộc tính | Mô tả | Giá trị mặc định |
|---|---|---|
max |
Giá trị tối đa của progress | 100 |
progress |
Giá trị hiện tại | 0 |
strokeWidth |
Độ dày của vòng | 8dp |
backgroundColor |
Màu nền vòng | #CCCCCC |
progressColor |
Màu tiến độ | #3F51B5 |
startAngle |
Góc bắt đầu (độ) | -90 (điểm trên) |
clockwise |
Quay chiều kim đồng hồ | true |
animationDuration |
Thời gian animation | 500ms |
Hiểu rõ các thuộc tính này sẽ giúp bạn kiểm soát mọi khía cạnh của Circle ProgressBar.
2. Vẽ Circle ProgressBar Bằng XML (ProgressBar Có Sẵn)
Android cung cấp một widget ProgressBar có thể được biến tấu thành dạng vòng tròn bằng cách thay đổi style. Đây là cách nhanh nhất nếu bạn không cần tùy chỉnh phức tạp.
2.1. Sử dụng android:indeterminateDrawable
<ProgressBar android:id="@+id/circularProgressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="120dp" android:layout_height="120dp" android:indeterminate="false" android:max="100" android:progress="30" android:progressDrawable="@drawable/custom_circular_progress" android:layout_gravity="center"/>
2.2. Tạo custom_circular_progress.xml trong thư mục drawable
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Background track --> <item android:id="@android:id/background"> <shape android:shape="ring" android:innerRadiusRatio="3" android:thicknessRatio="8"> <size android:width="120dp" android:height="120dp"/> <solid android:color="#E0E0E0"/> </shape> </item> <!-- Progress arc --> <item android:id="@android:id/progress"> <rotate android:fromDegrees="-90" android:pivotX="50%" android:pivotY="50%"> <shape android:shape="ring" android:innerRadiusRatio="3" android:thicknessRatio="8"> <size android:width="120dp" android:height="120dp"/> <gradient android:startColor="#FF5722" android:endColor="#FFC107" android:angle="0"/> </shape> </rotate> </item>
</layer-list>
Giải thích:
layer-listchứa 2 layer: background và progress.shapekiểuringtạo vòng tròn.rotatedùng để đặt góc bắt đầu là -90° (đỉnh trên).gradientgiúp tạo hiệu ứng màu chuyển dần.
2.3. Cập nhật giá trị trong code

Có thể bạn quan tâm: Cách Vẽ Chữ “án” Đẹp, Chuẩn Và Độc Đáo: Hướng Dẫn Từ Cơ Bản Đến Nâng Cao
val progressBar = findViewById<ProgressBar>(R.id.circularProgressBar)
progressBar.progress = 70 // Cập nhật 70%
Ưu điểm: Đơn giản, không cần viết code Java/Kotlin.
Nhược điểm: Hạn chế trong việc tùy chỉnh animation, không thể thêm text trung tâm, không hỗ trợ gradient phức tạp hoặc shadow.
3. Tự Tạo Custom View Với Canvas
Khi yêu cầu UI phức tạp hơn (ví dụ: gradient đa màu, shadow, thumb, text), chúng ta cần tự viết một Custom View.
3.1. Khởi tạo lớp Custom View
class CircleProgressBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) { // Thuộc tính private var maxProgress = 100 private var progress = 0 private var strokeWidth = 20f private var startAngle = -90f private var clockwise = true // Paints private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE color = Color.LTGRAY strokeCap = Paint.Cap.ROUND } private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE color = Color.BLUE strokeCap = Paint.Cap.ROUND } private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.DKGRAY textSize = 48f textAlign = Paint.Align.CENTER } // RectF chứa vòng tròn private val rectF = RectF() init { // Đọc attribute từ XML nếu có attrs?.let { val a = context.obtainStyledAttributes(it, R.styleable.CircleProgressBar, 0, 0) maxProgress = a.getInt(R.styleable.CircleProgressBar_maxProgress, 100) progress = a.getInt(R.styleable.CircleProgressBar_progress, 0) strokeWidth = a.getDimension(R.styleable.CircleProgressBar_strokeWidth, 20f) startAngle = a.getFloat(R.styleable.CircleProgressBar_startAngle, -90f) clockwise = a.getBoolean(R.styleable.CircleProgressBar_clockwise, true) backgroundPaint.strokeWidth = strokeWidth progressPaint.strokeWidth = strokeWidth // Color backgroundPaint.color = a.getColor(R.styleable.CircleProgressBar_backgroundColor, Color.LTGRAY) progressPaint.color = a.getColor(R.styleable.CircleProgressBar_progressColor, Color.BLUE) a.recycle() } } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) val padding = strokeWidth / 2 + 4 // Thêm một chút padding cho shadow rectF.set(padding, padding, w - padding, h - padding) } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) // 1. Vẽ vòng nền canvas.drawArc(rectF, 0f, 360f, false, backgroundPaint) // 2. Tính góc quét val sweepAngle = 360f progress / maxProgress if (clockwise) 1 else -1 // 3. Vẽ progress arc canvas.drawArc(rectF, startAngle, sweepAngle, false, progressPaint) // 4. Vẽ text trung tâm val percent = (progress 100 / maxProgress).toInt() val text = "$percent%" val x = width / 2f val y = height / 2f - (textPaint.descent() + textPaint.ascent()) / 2 canvas.drawText(text, x, y, textPaint) } // Setter/getter để cập nhật từ bên ngoài fun setProgress(value: Int, animated: Boolean = true) { val newVal = value.coerceIn(0, maxProgress) if (animated) { val animator = ValueAnimator.ofInt(progress, newVal) animator.duration = 500 animator.interpolator = AccelerateDecelerateInterpolator() animator.addUpdateListener { progress = it.animatedValue as Int invalidate() } animator.start() } else { progress = newVal invalidate() } } // Các setter khác (color, strokeWidth, …) có thể thêm tương tự
}
3.2. Định nghĩa custom attributes trong res/values/attrs.xml
<resources> <declare-styleable name="CircleProgressBar"> <attr name="maxProgress" format="integer" /> <attr name="progress" format="integer" /> <attr name="strokeWidth" format="dimension" /> <attr name="startAngle" format="float" /> <attr name="clockwise" format="boolean" /> <attr name="backgroundColor" format="color" /> <attr name="progressColor" format="color" /> </declare-styleable>
</resources>
3.3. Sử dụng trong layout XML
<com.example.customview.CircleProgressBar android:id="@+id/customCircle" android:layout_width="200dp" android:layout_height="200dp" app:maxProgress="100" app:progress="45" app:strokeWidth="16dp" app:backgroundColor="#EEEEEE" app:progressColor="#FF5722" app:startAngle="-90" app:clockwise="true"/>
3.4. Cập nhật giá trị từ Activity/Fragment
val circle = findViewById<CircleProgressBar>(R.id.customCircle)
circle.setProgress(80) // animate tới 80%
Ưu điểm:
- Toàn quyền tùy chỉnh: màu, độ dày, gradient, shadow, text, animation.
- Hiệu suất tốt vì vẽ trực tiếp trên Canvas.
Nhược điểm:
- Cần kiến thức về Canvas, Paint và Android custom view lifecycle.
- Phải tự quản lý state (save/restore) nếu muốn hỗ trợ rotation.
4. Sử Dụng Thư Viện Hỗ Trợ
Nếu bạn không muốn “tự tay” viết code vẽ, có rất nhiều thư viện mở rộng giúp tạo Circle ProgressBar nhanh chóng và đẹp mắt.
4.1. Material Components – CircularProgressIndicator
Google Material Components cung cấp CircularProgressIndicator (được tích hợp trong com.google.android.material:material). Đây là lựa chọn chuẩn cho UI Material Design.
Thêm dependency:
implementation 'com.google.android.material:material:1.9.0'
Sử dụng trong XML:
<com.google.android.material.progressindicator.CircularProgressIndicator android:id="@+id/materialCircular" android:layout_width="80dp" android:layout_height="80dp" app:indicatorSize="80dp" app:trackThickness="8dp" app:trackColor="#E0E0E0" app:indicatorColor="#3F51B5" app:showAnimationBehavior="linear" app:progress="40"/>
Tùy chỉnh gradient: Material không hỗ trợ gradient trực tiếp, nhưng bạn có thể dùng CircularProgressDrawable tùy chỉnh.
4.2. CircleProgressBar – Thư viện của lopspower
GitHub: https://github.com/lopspower/CircleProgressBar
Thêm dependency:
implementation 'com.github.lopspower:circleprogressbar:1.4.3'
Sử dụng:
<com.lopspower.CircularProgressBar android:id="@+id/lopsCircle" android:layout_width="150dp" android:layout_height="150dp" app:cpb_progress="75" app:cpb_maxProgress="100" app:cpb_progressBarWidth="12dp" app:cpb_backgroundProgressBarWidth="12dp" app:cpb_progressBarColor="#FF5722" app:cpb_backgroundProgressBarColor="#BDBDBD" app:cpb_roundBorder="true"/>
Thư viện này hỗ trợ gradient, shadow, và text mặc định.
4.3. SmoothProgressBar – Hiệu ứng mượt mà
Thư viện com.github.castorflex.smoothprogressbar:library thường dùng cho linear, nhưng có phiên bản SmoothCircularProgressBar.
Example:
implementation 'com.github.castorflex.smoothprogressbar:library:1.1.0'
4.4. Khi nào nên dùng thư viện?

Có thể bạn quan tâm: Cách Vẽ Chữ Tfboys: Hướng Dẫn Chi Tiết Từng Bước Để Tự Tạo Kiệt Tác
- Tiết kiệm thời gian: Dự án gấp, không cần tùy biến sâu.
- Yêu cầu UI chuẩn: Material Components.
- Cần tính năng đặc thù: Gradient đa màu, thumb, icon. Thư viện thường đã triển khai sẵn.
Lưu ý: Kiểm tra kích thước AAR, độ tương thích SDK và có cập nhật thường xuyên không.
5. Animation và Interpolator cho Circle ProgressBar
Một ProgressBar “cứng” không thu hút người dùng. Thêm animation sẽ làm UI mượt mà và chuyên nghiệp.
5.1. Sử dụng ObjectAnimator
fun animateProgress(toProgress: Int, duration: Long = 800) { val animator = ObjectAnimator.ofInt(this, "progress", progress, toProgress) animator.duration = duration animator.interpolator = DecelerateInterpolator() animator.addUpdateListener { setProgress(it.animatedValue as Int, animated = false) // avoid double animation } animator.start()
}
Ghi chú: Phải có setter setProgress(int) để ObjectAnimator có thể truy cập.
5.2. Interpolators phổ biến
| Interpolator | Hiệu ứng |
|---|---|
LinearInterpolator |
Tốc độ cố định |
AccelerateInterpolator |
Tăng dần tốc độ |
DecelerateInterpolator |
Giảm dần tốc độ |
AccelerateDecelerateInterpolator |
Tăng rồi giảm (mặc định) |
BounceInterpolator |
Nhảy lên khi kết thúc |
OvershootInterpolator |
Đi qua giá trị mục tiêu rồi quay lại |
Bạn có thể thử AnticipateOvershootInterpolator để tạo cảm giác “đánh bật”.
5.3. Animation vòng tròn vô hạn (Indeterminate)
Nếu bạn muốn hiển thị trạng thái đang tải mà không biết phần trăm, có thể tạo animation vòng xoay vô hạn:
val rotateAnim = ObjectAnimator.ofFloat(this, "rotation", 0f, 360f)
rotateAnim.duration = 1500
rotateAnim.repeatCount = ValueAnimator.INFINITE
rotateAnim.interpolator = LinearInterpolator()
rotateAnim.start()
Hoặc sử dụng ProgressBar style ?android:attr/progressBarStyleLarge với indeterminate="true".
6. Tùy Chỉnh Màu Sắc, Gradient, Shadow
6.1. Gradient cho progress arc
Sử dụng Shader trên Paint.
private fun initGradient() { val sweepGradient = SweepGradient( width / 2f, height / 2f, intArrayOf(Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.MAGENTA, Color.RED), null ) // Đặt góc bắt đầu -90 độ val matrix = Matrix() matrix.postRotate(startAngle, width / 2f, height / 2f) sweepGradient.setLocalMatrix(matrix) progressPaint.shader = sweepGradient
}
Gọi initGradient() trong onSizeChanged để có kích thước đúng.
6.2. Shadow (đổ bóng)
init { setLayerType(LAYER_TYPE_SOFTWARE, null) // bật phần mềm rendering cho shadow backgroundPaint.setShadowLayer(8f, 0f, 2f, Color.GRAY) progressPaint.setShadowLayer(8f, 0f, 2f, Color.DKGRAY)
}
6.3. Thêm Thumb (đầu mũi tên)
private val thumbPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL color = Color.WHITE setShadowLayer(4f, 0f, 2f, Color.BLACK)
}
private val thumbRadius = 12f private fun drawThumb(canvas: Canvas) { val angle = Math.toRadians((startAngle + sweepAngle).toDouble()) val cx = (width / 2 + (rectF.width() / 2) Math.cos(angle)).toFloat() val cy = (height / 2 + (rectF.height() / 2) Math.sin(angle)).toFloat() canvas.drawCircle(cx, cy, thumbRadius, thumbPaint)
}
Gọi drawThumb(canvas) sau khi vẽ progressArc.
7. Kết Nối Với LiveData / ViewModel
Trong kiến trúc MVVM, UI nên phản ứng với dữ liệu thay vì tự thay đổi. Dưới đây là cách tích hợp CircleProgressBar với LiveData.
7.1. ViewModel

Có thể bạn quan tâm: Cách Vẽ Chữ “sóc” Đẹp Và Chuẩn – Hướng Dẫn Từng Bước Cho Người Mới Bắt Đầu
class DownloadViewModel : ViewModel() { private val _progress = MutableLiveData<Int>(0) val progress: LiveData<Int> = _progress fun startDownload() { viewModelScope.launch { for (i in 1..100) { delay(50) _progress.postValue(i) } } }
}
7.2. Activity / Fragment
class MainFragment : Fragment(R.layout.fragment_main) { private val viewModel: DownloadViewModel by viewModels() private lateinit var circleProgressBar: CircleProgressBar override fun onViewCreated(view: View, savedInstanceState: Bundle?) { circleProgressBar = view.findViewById(R.id.customCircle) viewModel.progress.observe(viewLifecycleOwner) { value -> circleProgressBar.setProgress(value) } view.findViewById<Button>(R.id.btnStart).setOnClickListener { viewModel.startDownload() } }
}
Nhờ LiveData, khi tiến độ thay đổi, UI sẽ tự động cập nhật mà không cần gọi setProgress thủ công.
7.3. Data Binding (tùy chọn)
Nếu bạn dùng Data Binding, có thể tạo BindingAdapter:
@BindingAdapter("app:progress")
fun setCircleProgress(view: CircleProgressBar, progress: Int) { view.setProgress(progress, animated = true)
}
Trong XML:
<com.example.customview.CircleProgressBar android:id="@+id/customCircle" android:layout_width="200dp" android:layout_height="200dp" app:progress="@{viewModel.progress}" />
8. Kiểm Thử và Tối Ưu Hiệu Suất
8.1. Kiểm thử UI (Espresso)
@RunWith(AndroidJUnit4::class)
class CircleProgressBarTest { @Rule @JvmField val activityRule = ActivityScenarioRule(MainActivity::class.java) @Test fun progress_updates_correctly() { onView(withId(R.id.btnStart)).perform(click()) // Đợi một thời gian ngắn Thread.sleep(1200) onView(withId(R.id.customCircle)).check(matches(isDisplayed())) // Kiểm tra giá trị progress đã thay đổi (cần expose getter hoặc sử dụng IdlingResource) }
}
8.2. Profiler
- GPU Overdraw: Đảm bảo không vẽ lại nền không cần thiết. Sử dụng
canvas.save()vàcanvas.restore()hợp lý. - Memory: Tránh tạo
Shadermới trongonDraw. Tạo một lần trongonSizeChangedvà tái sử dụng. - Hardware Acceleration: Khi dùng shadow, cần
setLayerType(LAYER_TYPE_SOFTWARE)– có thể làm giảm hiệu năng. Nếu cần hiệu năng cao, cân nhắc dùngblurMaskFilterhoặc tránh shadow.
8.3. Đánh giá Frame Rate
Sử dụng Profile GPU Rendering trong Developer Options. Một vòng tròn đơn giản dưới 16ms (60fps) là tiêu chuẩn.
9. Kết Luận và Một Số Lưu Ý Quan Trọng
9.1. Tổng kết
- XML + ProgressDrawable: Thích hợp cho UI đơn giản, nhanh chóng.
- Custom View + Canvas: Cung cấp tối đa khả năng tùy biến (gradient, shadow, thumb, text, animation). Yêu cầu hiểu rõ
onDraw,Paint,Canvas. - Thư viện bên thứ ba: Giải pháp cân bằng giữa tốc độ triển khai và tính năng. Chọn thư viện dựa trên độ ổn định, kích thước và tính năng cần thiết.
- Animation: Sử dụng
ObjectAnimator,ValueAnimatorhoặcAnimatorSet. Chọn interpolator phù hợp với trải nghiệm người dùng. - Kiến trúc MVVM: Kết hợp
LiveData/StateFlowđể quản lý trạng thái progress một cách sạch sẽ. - Hiệu suất: Tối ưu tạo shader, tránh tạo đối tượng trong
onDraw, sử dụnghardware accelerationkhi có thể.
9.2. Các lỗi thường gặp và cách khắc phục
| Vấn đề | Nguyên nhân | Giải pháp |
|---|---|---|
| Vòng tròn bị lệch, không trung tâm | Không tính padding đúng khi thiết lập RectF |
Đảm bảo rectF.set(padding, padding, w - padding, h - padding) |
| Gradient không khớp góc bắt đầu | SweepGradient mặc định bắt đầu từ 3h (right) |
Dùng Matrix để xoay gradient theo startAngle |
| Shadow không hiển thị trên API 28+ | setLayerType(LAYER_TYPE_SOFTWARE) bị tắt |
Kiểm tra if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { setLayerType(LAYER_TYPE_SOFTWARE, null) } |
| Animation giật khi cập nhật nhanh | Gọi invalidate() quá thường xuyên |
Dùng Choreographer hoặc postOnAnimation để đồng bộ với frame |
| Text không nằm chính giữa | Không tính toán ascender/descender | val y = height/2f - (textPaint.descent() + textPaint.ascent())/2 |
9.3. Hướng phát triển tiếp theo
- Responsive Size: Tính toán
strokeWidthvàtextSizedựa trên kích thước view để hỗ trợ đa màn hình. - Touch Interaction: Cho phép người dùng kéo thumb để thay đổi giá trị (cần xử lý
onTouchEvent). - Multi‑segment Progress: Hiển thị nhiều phần tiến độ (ví dụ: CPU, RAM, Disk) trong cùng một vòng tròn.
- SVG/Vector Integration: Dùng
VectorDrawablecho thumb hoặc biểu tượng trung tâm để giảm kích thước tài nguyên. - Compose: Nếu dự án chuyển sang Jetpack Compose, có thể tái hiện cùng logic bằng
Canvascomposable.
Tài Nguyên Tham Khảo
- Android Developers – Canvas: https://developer.android.com/reference/android/graphics/Canvas
- Material Components – CircularProgressIndicator: https://material.io/components/progress-indicators/android
- GitHub – lopspower/CircleProgressBar: https://github.com/lopspower/CircleProgressBar
- Kotlin Coroutines – viewModelScope: https://kotlinlang.org/docs/coroutines-android.html
Hy vọng qua bài viết chi tiết này, bạn đã nắm vững cách vẽ Circle ProgressBar trong Android, từ các giải pháp nhanh gọn cho tới việc tự xây dựng một Custom View mạnh mẽ, đồng thời biết cách tích hợp animation, gradient, shadow và kiến trúc MVVM. Chúc bạn thành công trong việc tạo ra những UI đẹp mắt và mượt mà cho ứng dụng của mình!
