效果
倒计时View视频
背景
业务场景需要显示带有毫秒级别的倒计时,于是自己封装一个通用的倒计时组件
源码分析
- 核心倒计时逻辑,主要是每隔100毫秒计算一次从开始倒计时到现在的剩余时间,并通过process接口返回出去
- Handler每次设置100毫秒的延迟
- 将返回出来的时间解析出来
private fun formatTimeToView(remainTime: Long) {
val lengthSec = remainTime / 1000
val hours = lengthSec / 3600
val rem = lengthSec % 3600
val minutes = rem / 60
val seconds = rem % 60
val milliseconds = remainTime % 1000
tvMill.text = String.format("%03d", milliseconds)
tvHour.text = String.format("%02d", hours)
tvMin.text = String.format("%02d", minutes)
tvSecond.text = String.format("%02d", seconds)
}
完整源码
class MillCountdownView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
private val root = LayoutInflater.from(context).inflate(R.layout.view_count_down, this, true)
private val countDownTask: CountDownRunnable
private val tvHour: TextView
private val tvMin: TextView
private val tvSecond: TextView
private val tvMill: TextView
init {
background = context.getDrawable(R.drawable.count_down_item_bg)
orientation = HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
tvHour = root.findViewById(R.id.tvHour)
tvMin = root.findViewById(R.id.tvMin)
tvSecond = root.findViewById(R.id.tvSecond)
tvMill = root.findViewById(R.id.tvMill)
countDownTask = CountDownRunnable(1).apply {
listener = object : TaskListener {
override fun finish() {
tvHour.text = "00"
tvMin.text = "00"
tvSecond.text = "00"
tvMill.text = "000"
Toast.makeText(context, "倒计时结束", Toast.LENGTH_SHORT).show()
}
override fun process(remainTime: Long) {
if (remainTime < 1) {
tvHour.text = "00"
tvMin.text = "00"
tvSecond.text = "00"
tvMill.text = "000"
return
}
formatTimeToView(remainTime)
}
}
}
}
private fun formatTimeToView(remainTime: Long) {
val lengthSec = remainTime / 1000
val hours = lengthSec / 3600
val rem = lengthSec % 3600
val minutes = rem / 60
val seconds = rem % 60
val milliseconds = remainTime % 1000
tvMill.text = String.format("%03d", milliseconds)
tvHour.text = String.format("%02d", hours)
tvMin.text = String.format("%02d", minutes)
tvSecond.text = String.format("%02d", seconds)
}
/**
* 预先展示倒计时文本
* @param remainTime 倒计时时间,单位毫秒
*/
fun preShowRemainSecs(remainTime: Long) {
countDownTask.totalCountDownTime = remainTime
formatTimeToView(remainTime)
}
/**
* 开始倒计时
* @param remainTime 倒计时时间,单位毫秒
*/
fun startCountdown(remainTime: Long) {
countDownTask.destroy()
countDownTask.totalCountDownTime = remainTime
countDownTask.start()
}
fun destroyCountdown() {
countDownTask.destroy()
}
}
class CountDownRunnable(@IntRange(from = 1)var totalCountDownTime: Long) : Runnable {
private val mHandler = Handler(Looper.getMainLooper())
var listener: TaskListener? = null
private var startCountDownTime = 0L //开始时当前系统时间
private var isTaskExecuting = false
override fun run() {
if (!isTaskExecuting) {
return
}
val dur = SystemClock.elapsedRealtime() - startCountDownTime
val remainTime = totalCountDownTime - dur
val mill = remainTime % 1000
if (remainTime <= 0) {
listener?.finish()
isTaskExecuting = false
return
} else {
listener?.process(remainTime)
}
mHandler.postDelayed(this, 100)
}
fun start() {
startCountDownTime = SystemClock.elapsedRealtime()
mHandler.post(this)
isTaskExecuting = true
}
fun resume() {
val remainTime = totalCountDownTime - ((SystemClock.elapsedRealtime() - startCountDownTime) / 1000).toInt()
if (remainTime <= 0) {
listener?.finish()
return
}
mHandler.removeCallbacks(this)
isTaskExecuting = true
mHandler.post(this)
}
fun pause() {
isTaskExecuting = false
mHandler.removeCallbacks(this)
}
fun destroy() {
isTaskExecuting = false
mHandler.removeCallbacks(this)
}
}
interface TaskListener {
fun finish()
fun process(remainTime: Long)
}
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:parentTag="LinearLayout">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvHour"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="30dp"
android:gravity="center"
android:textColor="#000"
android:textSize="25sp"
android:textStyle="bold"
android:layout_marginStart="10dp"
android:layout_marginVertical="5dp"
tools:text="1" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/divider1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:gravity="center"
android:text=":"
android:textColor="#000"
android:textSize="25sp"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvMin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:gravity="center"
android:textColor="#000"
android:minWidth="30dp"
android:textSize="25sp"
android:textStyle="bold"
tools:text="8" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/divider2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:gravity="center"
android:text=":"
android:textColor="#000"
android:textSize="25sp"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvSecond"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="#000"
android:textSize="25sp"
android:textStyle="bold"
android:minWidth="30dp"
tools:text="3" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/divider3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:gravity="center"
android:text=":"
android:textColor="#000"
android:textSize="25sp"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvMill"
android:layout_width="wrap_content"
android:minWidth="49dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:gravity="center"
android:textColor="#a00"
android:textSize="25sp"
android:textStyle="bold"
tools:text="000"
android:layout_marginEnd="10dp"/>
</merge>