中文 | English
CountTimeProgressView is a Kotlin-based Android circular countdown progress view. Drop-in, zero runtime dependency, with first-class support for both classic View (XML) and Jetpack Compose. It ships a full state machine, lifecycle-aware pause/resume, warning threshold, and resume-from-progress out of the box.
Minimum supported version: Android 5.0 (API 21).
The demo app includes real-world scenarios: Ad Skip / Verification Code / Progress Resume / Exam Timer, each with Java + XML, Kotlin + XML, and Jetpack Compose implementations.
- One-line integration — classic View / Jetpack Compose, no AppCompat required
- Full state machine —
IDLE→RUNNING→PAUSED/CANCELED/FINISHED, all in a single listener - Multiple text styles —
jump,second,clock(mm:ss),none, plus customtextFormatter - Warning threshold — auto-change color and fire a callback in the last N seconds (e.g. red in the final 3s)
- Per-second tick — fires only on second change; perfect for OTP buttons and ad-skip buttons
- Resume anywhere — start from
fromProgressorfromRemainingfor list recycling / server-synced countdowns - Lifecycle-aware —
bindLifecycle(owner)auto pauses in background and resumes on foreground - Rich visual customization — gradient progress bar,
StrokeCap, marker ball, custom interpolator - Click delay — block clicks for the first N seconds with
disabledText(ideal for splash ads) - Accessibility-friendly — automatic
contentDescription,wrap_contentdefaults to 84dp
In your root settings.gradle (or top-level build.gradle):
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}In your app module build.gradle:
dependencies {
implementation 'com.github.sfyc23:CountTimeProgressView:2.1.0'
}Releases are published to JitPack automatically on every git tag. See the badge at the top for the latest version.
<com.sfyc.ctpv.CountTimeProgressView
android:id="@+id/countTimeProgressView"
android:layout_width="84dp"
android:layout_height="84dp"
app:backgroundColorCenter="#FF7F00"
app:borderWidth="3dp"
app:borderBottomColor="#D60000"
app:borderDrawColor="#CDC8EA"
app:markBallColor="#002FFF"
app:markBallFlag="true"
app:markBallWidth="3dp"
app:titleCenterColor="#000000"
app:titleCenterText="Click to skip"
app:titleCenterSize="14sp"
app:countTime="5000"
app:startAngle="0"
app:textStyle="jump"
app:clockwise="true"
app:warningTime="3000"
app:warningColor="#FF3B30"
app:clickableAfter="2000"
app:disabledText="Please wait" />with(countTimeProgressView) {
countTime = 6000L
textStyle = CountTimeProgressView.TEXT_STYLE_SECOND
titleCenterText = "Skip (%s)s"
// v2.1 Turn red in the last 3 seconds
warningTime = 3000L
warningColor = Color.parseColor("#FF3B30")
// v2.1 Not clickable for the first 2 seconds
clickableAfterMillis = 2000L
disabledText = "Please wait"
setOnCountdownEndListener {
Toast.makeText(context, "Time's up", Toast.LENGTH_SHORT).show()
}
setOnClickCallback { overageTime ->
if (isRunning) cancelCountTimeAnimation() else startCountTimeAnimation()
}
// Unified state listener
setOnStateChangedListener { state ->
Log.d("CTPV", "state=$state") // IDLE / RUNNING / PAUSED / CANCELED / FINISHED
}
// Fires only on second change — great for OTP / ad-skip buttons
setOnTickListener { remainingMillis, remainingSeconds ->
Log.d("CTPV", "Remaining: ${remainingSeconds}s")
}
setOnWarningListener { remainingMillis ->
Log.d("CTPV", "About to finish!")
}
// Fires on every animation frame
addOnProgressChangedListener { progress, remainingMillis ->
Log.d("CTPV", "progress=$progress, remaining=$remainingMillis")
}
// Auto pause / resume with lifecycle
bindLifecycle(this@SimpleActivity)
startCountTimeAnimation()
// Or start from a specific progress / remaining time:
// startCountTimeAnimation(fromProgress = 0.5f)
// startCountTimeAnimationFromRemaining(3000L)
}countTimeProgressView.setCountTime(5000L);
countTimeProgressView.setTextStyle(CountTimeProgressView.TEXT_STYLE_CLOCK);
countTimeProgressView.setWarningTime(3000L);
countTimeProgressView.setWarningColor(Color.parseColor("#FF3B30"));
countTimeProgressView.setClickableAfterMillis(2000L);
countTimeProgressView.setDisabledText("Please wait");
countTimeProgressView.setOnCountdownEndListener(() ->
Toast.makeText(this, "Time's up", Toast.LENGTH_SHORT).show());
countTimeProgressView.setOnStateChangedListener(state ->
Log.d("CTPV", "state=" + state));
countTimeProgressView.setOnTickListener((remainingMillis, remainingSeconds) ->
Log.d("CTPV", "Remaining: " + remainingSeconds + "s"));
countTimeProgressView.bindLifecycle(this);
countTimeProgressView.startCountTimeAnimation();The library ships a lightweight Compose adapter CountTimeProgressViewCompose (no Compose runtime dependency required). Use it directly inside AndroidView:
AndroidView(
modifier = Modifier.size(84.dp),
factory = { ctx ->
CountTimeProgressViewCompose.create(ctx) {
countTime = 5000L
textStyle = CountTimeProgressView.TEXT_STYLE_SECOND
warningTime = 3000L
warningColor = Color.RED
setOnStateChangedListener { state -> /* ... */ }
setOnTickListener { _, sec -> /* ... */ }
startCountTimeAnimation()
}
},
update = { view ->
CountTimeProgressViewCompose.update(view) {
// React to Compose State changes here
}
}
)| Scenario | Recommended Setup |
|---|---|
| Splash ad skip button | textStyle=jump + clickableAfterMillis + disabledText to prevent mis-taps |
| OTP / Verification code | textStyle=second + setOnTickListener (only on second change) |
| Server-synced countdown | startCountTimeAnimationFromRemaining(remainMs) |
| List item countdown | startCountTimeAnimation(fromProgress = 0.7f) + bindLifecycle |
| Exam / long timer | textStyle=clock (mm:ss) + warningTime to turn red near the end |
| Jetpack Compose screens | AndroidView + CountTimeProgressViewCompose.create |
startCountTimeAnimation()
IDLE ─────────────────────────────▶ RUNNING
▲ │ ▲ │
│ resetCountTimeAnimation() │ │ │ pause / resume
│ │ │ ▼
└─────────── CANCELED / FINISHED ─── PAUSED
IDLE— initial / after resetRUNNING— tickingPAUSED— manual or lifecycle-driven pauseCANCELED— stopped viacancelCountTimeAnimation()FINISHED— naturally ended;OnCountdownEndListenerfires here
Use setOnStateChangedListener { state -> ... } to observe every transition in one place.
| Attribute | Format | Description | Default |
|---|---|---|---|
backgroundColorCenter |
color | Center background color | #00BCD4 |
borderWidth |
dimension | Ring stroke width | 3dp |
borderDrawColor |
color | Progress color | #4dd0e1 |
borderBottomColor |
color | Track color | #D32F2F |
markBallWidth |
dimension | Marker ball width | 6dp |
markBallColor |
color | Marker ball color | #536DFE |
markBallFlag |
boolean | Show marker ball | true |
startAngle |
float | Start angle (0 = top) | 0f |
clockwise |
boolean | Clockwise or counter-clockwise | true |
countTime |
integer | Total time in milliseconds | 5000 |
textStyle |
enum | jump / second / clock / none |
jump |
titleCenterText |
string | Center text (for jump) |
"jump" |
titleCenterColor |
color | Center text color | #FFFFFF |
titleCenterSize |
dimension | Center text size | 16sp |
autoStart |
boolean | Auto-start on attach | false |
finishedText |
string | Text shown when finished | - |
showCenterText |
boolean | Whether to show center text | true |
strokeCap |
enum | butt / round / square |
butt |
gradientStartColor |
color | Gradient start color | - |
gradientEndColor |
color | Gradient end color | - |
warningTime |
integer | Warning threshold in ms (fires when remaining ≤ value) | 0 |
warningColor |
color | Progress color in warning state | #FF3B30 |
clickableAfter |
integer | Ms after start before clicks are accepted | 0 |
disabledText |
string | Text shown during non-clickable period | - |
Unit note:
borderWidthandmarkBallWidthsetters take a dp value and store pixels internally.titleCenterTextSizetakes an sp value. For raw pixel values, usesetBorderWidthPx(),setMarkBallWidthPx(),setTitleCenterTextSizePx().
| Method | Description |
|---|---|
startCountTimeAnimation() |
Start from the beginning |
startCountTimeAnimation(fromProgress: Float) |
Start from a 0f..1f progress |
startCountTimeAnimationFromRemaining(millis: Long) |
Start with a given remaining time |
pauseCountTimeAnimation() / resumeCountTimeAnimation() |
Pause / resume |
cancelCountTimeAnimation() |
Cancel (enters CANCELED) |
resetCountTimeAnimation() |
Reset back to IDLE |
bindLifecycle(owner) |
Bind lifecycle, auto pause/resume |
setOnCountdownEndListener { } |
Fired when countdown finishes naturally |
setOnStateChangedListener { state -> } |
Unified state listener |
setOnTickListener { ms, sec -> } |
Fires only on second change |
setOnWarningListener { ms -> } |
Fires when entering warning threshold |
setOnClickCallback { overage -> } |
View clicked, with remaining time |
addOnProgressChangedListener { p, ms -> } |
Per-frame progress callback |
setGradientColors(start, end) |
Set gradient progress color |
textFormatter = { millis -> "..." } |
Custom center text formatter |
- Added
CountdownStatestate machine (IDLE / RUNNING / PAUSED / CANCELED / FINISHED) withsetOnStateChangedListener - Added per-second tick callback
setOnTickListener(fires only on second change) - Added warning threshold
warningTime/warningColor/setOnWarningListener(XML supported) - Added resume-from-progress:
startCountTimeAnimation(fromProgress)andstartCountTimeAnimationFromRemaining(millis) - Added click-delay
clickableAfterMillis/disabledText(XML supported) - Fixed
wasRunningrestore: animation auto-resumes from saved progress after rotation - Added lifecycle-aware auto pause/resume via
bindLifecycle(owner) - Removed unused
displayTextdead code
- Added Pause / Resume / Reset animation APIs
- Added
progressgetter/setter and per-frame progress callback - Added gradient progress bar (SweepGradient, XML supported)
- Added StrokeCap, Interpolator, autoStart, finishedText, showCenterText
- Added Jetpack Compose adapter
CountTimeProgressViewCompose - Added SavedState and accessibility support
- Added
wrap_contentsupport with 84dp default - Split callbacks into
OnCountdownEndListener+setOnClickCallback()(lambda-friendly) - Migrated to AndroidX;
minSdkraised to 21 - Library module no longer depends on AppCompat
- See full notes in CHANGELOG.md
- 1.1.3 (2017-11-11) — Rewritten in Kotlin
- 1.1.1 (2017-03-27) — Fix: stop running on exit
- 1.1.0 (2017-02-07) — Clockwise animation support
- 1.0.0 (2016-12-20) — First release
Issues and PRs are welcome! Please read CONTRIBUTING.md before submitting.
Licensed under the Apache License 2.0.

