Skip to content

Commit 39a9c5d

Browse files
committed
更新文档。
1 parent 4d562ec commit 39a9c5d

3 files changed

Lines changed: 572 additions & 527 deletions

File tree

README-EN.md

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
[![twitter](https://img.shields.io/badge/twitter-sfyc23-blue.svg)](https://twitter.com/sfyc23)
2+
[![Weibo](https://img.shields.io/badge/Weibo-sfyc23-blue.svg)](https://weibo.com/sfyc23)
3+
[![API](https://img.shields.io/badge/API-%2B21-green.svg)](https://android-arsenal.com/api?level=21)
4+
[![JitPack](https://jitpack.io/v/sfyc23/CountTimeProgressView.svg)](https://jitpack.io/#sfyc23/CountTimeProgressView)
5+
[![License](https://img.shields.io/badge/License-Apache%202.0-red.svg)](LICENSE)
6+
7+
# CountTimeProgressView
8+
9+
> [中文](README.md)**English**
10+
11+
**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.
12+
13+
Minimum supported version: **Android 5.0 (API 21)**.
14+
15+
---
16+
17+
## Preview
18+
19+
<p align="center">
20+
<img src="screenshot/menu.png" width="300" alt="Demo menu" />
21+
&nbsp;&nbsp;
22+
<img src="screenshot/features.png" width="300" alt="Animation control & live state" />
23+
</p>
24+
25+
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.
26+
27+
---
28+
29+
## Features
30+
31+
- **One-line integration** — classic View / Jetpack Compose, no AppCompat required
32+
- **Full state machine**`IDLE``RUNNING``PAUSED` / `CANCELED` / `FINISHED`, all in a single listener
33+
- **Multiple text styles**`jump`, `second`, `clock` (mm:ss), `none`, plus custom `textFormatter`
34+
- **Warning threshold** — auto-change color and fire a callback in the last N seconds (e.g. red in the final 3s)
35+
- **Per-second tick** — fires only on second change; perfect for OTP buttons and ad-skip buttons
36+
- **Resume anywhere** — start from `fromProgress` or `fromRemaining` for list recycling / server-synced countdowns
37+
- **Lifecycle-aware**`bindLifecycle(owner)` auto pauses in background and resumes on foreground
38+
- **Rich visual customization** — gradient progress bar, `StrokeCap`, marker ball, custom interpolator
39+
- **Click delay** — block clicks for the first N seconds with `disabledText` (ideal for splash ads)
40+
- **Accessibility-friendly** — automatic `contentDescription`, `wrap_content` defaults to 84dp
41+
42+
---
43+
44+
## Install
45+
46+
### Step 1. Add the JitPack repository
47+
48+
In your root `settings.gradle` (or top-level `build.gradle`):
49+
50+
```groovy
51+
dependencyResolutionManagement {
52+
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
53+
repositories {
54+
google()
55+
mavenCentral()
56+
maven { url 'https://jitpack.io' }
57+
}
58+
}
59+
```
60+
61+
### Step 2. Add the dependency
62+
63+
In your app module `build.gradle`:
64+
65+
```groovy
66+
dependencies {
67+
implementation 'com.github.sfyc23:CountTimeProgressView:2.1.0'
68+
}
69+
```
70+
71+
> Releases are published to JitPack automatically on every git tag. See the badge at the top for the latest version.
72+
73+
---
74+
75+
## Quick Start
76+
77+
### Option 1. XML layout
78+
79+
```xml
80+
<com.sfyc.ctpv.CountTimeProgressView
81+
android:id="@+id/countTimeProgressView"
82+
android:layout_width="84dp"
83+
android:layout_height="84dp"
84+
app:backgroundColorCenter="#FF7F00"
85+
app:borderWidth="3dp"
86+
app:borderBottomColor="#D60000"
87+
app:borderDrawColor="#CDC8EA"
88+
app:markBallColor="#002FFF"
89+
app:markBallFlag="true"
90+
app:markBallWidth="3dp"
91+
app:titleCenterColor="#000000"
92+
app:titleCenterText="Click to skip"
93+
app:titleCenterSize="14sp"
94+
app:countTime="5000"
95+
app:startAngle="0"
96+
app:textStyle="jump"
97+
app:clockwise="true"
98+
app:warningTime="3000"
99+
app:warningColor="#FF3B30"
100+
app:clickableAfter="2000"
101+
app:disabledText="Please wait" />
102+
```
103+
104+
### Option 2. Kotlin
105+
106+
```kotlin
107+
with(countTimeProgressView) {
108+
countTime = 6000L
109+
textStyle = CountTimeProgressView.TEXT_STYLE_SECOND
110+
titleCenterText = "Skip (%s)s"
111+
112+
// v2.1 Turn red in the last 3 seconds
113+
warningTime = 3000L
114+
warningColor = Color.parseColor("#FF3B30")
115+
116+
// v2.1 Not clickable for the first 2 seconds
117+
clickableAfterMillis = 2000L
118+
disabledText = "Please wait"
119+
120+
setOnCountdownEndListener {
121+
Toast.makeText(context, "Time's up", Toast.LENGTH_SHORT).show()
122+
}
123+
124+
setOnClickCallback { overageTime ->
125+
if (isRunning) cancelCountTimeAnimation() else startCountTimeAnimation()
126+
}
127+
128+
// Unified state listener
129+
setOnStateChangedListener { state ->
130+
Log.d("CTPV", "state=$state") // IDLE / RUNNING / PAUSED / CANCELED / FINISHED
131+
}
132+
133+
// Fires only on second change — great for OTP / ad-skip buttons
134+
setOnTickListener { remainingMillis, remainingSeconds ->
135+
Log.d("CTPV", "Remaining: ${remainingSeconds}s")
136+
}
137+
138+
setOnWarningListener { remainingMillis ->
139+
Log.d("CTPV", "About to finish!")
140+
}
141+
142+
// Fires on every animation frame
143+
addOnProgressChangedListener { progress, remainingMillis ->
144+
Log.d("CTPV", "progress=$progress, remaining=$remainingMillis")
145+
}
146+
147+
// Auto pause / resume with lifecycle
148+
bindLifecycle(this@SimpleActivity)
149+
150+
startCountTimeAnimation()
151+
// Or start from a specific progress / remaining time:
152+
// startCountTimeAnimation(fromProgress = 0.5f)
153+
// startCountTimeAnimationFromRemaining(3000L)
154+
}
155+
```
156+
157+
### Option 3. Java
158+
159+
```java
160+
countTimeProgressView.setCountTime(5000L);
161+
countTimeProgressView.setTextStyle(CountTimeProgressView.TEXT_STYLE_CLOCK);
162+
countTimeProgressView.setWarningTime(3000L);
163+
countTimeProgressView.setWarningColor(Color.parseColor("#FF3B30"));
164+
countTimeProgressView.setClickableAfterMillis(2000L);
165+
countTimeProgressView.setDisabledText("Please wait");
166+
167+
countTimeProgressView.setOnCountdownEndListener(() ->
168+
Toast.makeText(this, "Time's up", Toast.LENGTH_SHORT).show());
169+
170+
countTimeProgressView.setOnStateChangedListener(state ->
171+
Log.d("CTPV", "state=" + state));
172+
173+
countTimeProgressView.setOnTickListener((remainingMillis, remainingSeconds) ->
174+
Log.d("CTPV", "Remaining: " + remainingSeconds + "s"));
175+
176+
countTimeProgressView.bindLifecycle(this);
177+
countTimeProgressView.startCountTimeAnimation();
178+
```
179+
180+
### Option 4. Jetpack Compose
181+
182+
The library ships a lightweight Compose adapter `CountTimeProgressViewCompose` (no Compose runtime dependency required). Use it directly inside `AndroidView`:
183+
184+
```kotlin
185+
AndroidView(
186+
modifier = Modifier.size(84.dp),
187+
factory = { ctx ->
188+
CountTimeProgressViewCompose.create(ctx) {
189+
countTime = 5000L
190+
textStyle = CountTimeProgressView.TEXT_STYLE_SECOND
191+
warningTime = 3000L
192+
warningColor = Color.RED
193+
setOnStateChangedListener { state -> /* ... */ }
194+
setOnTickListener { _, sec -> /* ... */ }
195+
startCountTimeAnimation()
196+
}
197+
},
198+
update = { view ->
199+
CountTimeProgressViewCompose.update(view) {
200+
// React to Compose State changes here
201+
}
202+
}
203+
)
204+
```
205+
206+
---
207+
208+
## Typical Use Cases
209+
210+
| Scenario | Recommended Setup |
211+
| :--- | :--- |
212+
| **Splash ad skip button** | `textStyle=jump` + `clickableAfterMillis` + `disabledText` to prevent mis-taps |
213+
| **OTP / Verification code** | `textStyle=second` + `setOnTickListener` (only on second change) |
214+
| **Server-synced countdown** | `startCountTimeAnimationFromRemaining(remainMs)` |
215+
| **List item countdown** | `startCountTimeAnimation(fromProgress = 0.7f)` + `bindLifecycle` |
216+
| **Exam / long timer** | `textStyle=clock` (mm:ss) + `warningTime` to turn red near the end |
217+
| **Jetpack Compose screens** | `AndroidView` + `CountTimeProgressViewCompose.create` |
218+
219+
---
220+
221+
## State Machine
222+
223+
```
224+
startCountTimeAnimation()
225+
IDLE ─────────────────────────────▶ RUNNING
226+
▲ │ ▲ │
227+
│ resetCountTimeAnimation() │ │ │ pause / resume
228+
│ │ │ ▼
229+
└─────────── CANCELED / FINISHED ─── PAUSED
230+
```
231+
232+
- `IDLE` — initial / after reset
233+
- `RUNNING` — ticking
234+
- `PAUSED` — manual or lifecycle-driven pause
235+
- `CANCELED` — stopped via `cancelCountTimeAnimation()`
236+
- `FINISHED` — naturally ended; `OnCountdownEndListener` fires here
237+
238+
Use `setOnStateChangedListener { state -> ... }` to observe every transition in one place.
239+
240+
---
241+
242+
## Full XML Attributes
243+
244+
| Attribute | Format | Description | Default |
245+
| :--- | :--- | :--- | :--- |
246+
| `backgroundColorCenter` | color | Center background color | `#00BCD4` |
247+
| `borderWidth` | dimension | Ring stroke width | `3dp` |
248+
| `borderDrawColor` | color | Progress color | `#4dd0e1` |
249+
| `borderBottomColor` | color | Track color | `#D32F2F` |
250+
| `markBallWidth` | dimension | Marker ball width | `6dp` |
251+
| `markBallColor` | color | Marker ball color | `#536DFE` |
252+
| `markBallFlag` | boolean | Show marker ball | `true` |
253+
| `startAngle` | float | Start angle (0 = top) | `0f` |
254+
| `clockwise` | boolean | Clockwise or counter-clockwise | `true` |
255+
| `countTime` | integer | Total time in milliseconds | `5000` |
256+
| `textStyle` | enum | `jump` / `second` / `clock` / `none` | `jump` |
257+
| `titleCenterText` | string | Center text (for `jump`) | `"jump"` |
258+
| `titleCenterColor` | color | Center text color | `#FFFFFF` |
259+
| `titleCenterSize` | dimension | Center text size | `16sp` |
260+
| `autoStart` | boolean | Auto-start on attach | `false` |
261+
| `finishedText` | string | Text shown when finished | `-` |
262+
| `showCenterText` | boolean | Whether to show center text | `true` |
263+
| `strokeCap` | enum | `butt` / `round` / `square` | `butt` |
264+
| `gradientStartColor` | color | Gradient start color | `-` |
265+
| `gradientEndColor` | color | Gradient end color | `-` |
266+
| `warningTime` | integer | Warning threshold in ms (fires when remaining ≤ value) | `0` |
267+
| `warningColor` | color | Progress color in warning state | `#FF3B30` |
268+
| `clickableAfter` | integer | Ms after start before clicks are accepted | `0` |
269+
| `disabledText` | string | Text shown during non-clickable period | `-` |
270+
271+
> **Unit note**: `borderWidth` and `markBallWidth` setters take a **dp** value and store pixels internally. `titleCenterTextSize` takes an **sp** value. For raw pixel values, use `setBorderWidthPx()`, `setMarkBallWidthPx()`, `setTitleCenterTextSizePx()`.
272+
273+
---
274+
275+
## API Cheatsheet
276+
277+
| Method | Description |
278+
| :--- | :--- |
279+
| `startCountTimeAnimation()` | Start from the beginning |
280+
| `startCountTimeAnimation(fromProgress: Float)` | Start from a `0f..1f` progress |
281+
| `startCountTimeAnimationFromRemaining(millis: Long)` | Start with a given remaining time |
282+
| `pauseCountTimeAnimation()` / `resumeCountTimeAnimation()` | Pause / resume |
283+
| `cancelCountTimeAnimation()` | Cancel (enters `CANCELED`) |
284+
| `resetCountTimeAnimation()` | Reset back to `IDLE` |
285+
| `bindLifecycle(owner)` | Bind lifecycle, auto pause/resume |
286+
| `setOnCountdownEndListener { }` | Fired when countdown finishes naturally |
287+
| `setOnStateChangedListener { state -> }` | Unified state listener |
288+
| `setOnTickListener { ms, sec -> }` | Fires only on second change |
289+
| `setOnWarningListener { ms -> }` | Fires when entering warning threshold |
290+
| `setOnClickCallback { overage -> }` | View clicked, with remaining time |
291+
| `addOnProgressChangedListener { p, ms -> }` | Per-frame progress callback |
292+
| `setGradientColors(start, end)` | Set gradient progress color |
293+
| `textFormatter = { millis -> "..." }` | Custom center text formatter |
294+
295+
---
296+
297+
## Changelog
298+
299+
### 2.1.0 (2026-04-16)
300+
301+
- Added `CountdownState` state machine (IDLE / RUNNING / PAUSED / CANCELED / FINISHED) with `setOnStateChangedListener`
302+
- Added per-second tick callback `setOnTickListener` (fires only on second change)
303+
- Added warning threshold `warningTime` / `warningColor` / `setOnWarningListener` (XML supported)
304+
- Added resume-from-progress: `startCountTimeAnimation(fromProgress)` and `startCountTimeAnimationFromRemaining(millis)`
305+
- Added click-delay `clickableAfterMillis` / `disabledText` (XML supported)
306+
- Fixed `wasRunning` restore: animation auto-resumes from saved progress after rotation
307+
- Added lifecycle-aware auto pause/resume via `bindLifecycle(owner)`
308+
- Removed unused `displayText` dead code
309+
310+
### 2.0.0 (2026-04-15)
311+
312+
- Added **Pause / Resume / Reset** animation APIs
313+
- Added `progress` getter/setter and per-frame progress callback
314+
- Added **gradient progress bar** (SweepGradient, XML supported)
315+
- Added **StrokeCap**, **Interpolator**, **autoStart**, **finishedText**, **showCenterText**
316+
- Added **Jetpack Compose adapter** `CountTimeProgressViewCompose`
317+
- Added **SavedState** and **accessibility** support
318+
- Added `wrap_content` support with 84dp default
319+
- Split callbacks into `OnCountdownEndListener` + `setOnClickCallback()` (lambda-friendly)
320+
- Migrated to AndroidX; `minSdk` raised to 21
321+
- Library module no longer depends on AppCompat
322+
- See full notes in [CHANGELOG.md](CHANGELOG.md)
323+
324+
### Older Versions
325+
326+
- **1.1.3 (2017-11-11)** — Rewritten in Kotlin
327+
- **1.1.1 (2017-03-27)** — Fix: stop running on exit
328+
- **1.1.0 (2017-02-07)** — Clockwise animation support
329+
- **1.0.0 (2016-12-20)** — First release
330+
331+
---
332+
333+
## Contributing
334+
335+
Issues and PRs are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting.
336+
337+
## License
338+
339+
Licensed under the [Apache License 2.0](LICENSE).

0 commit comments

Comments
 (0)