“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情”
Android--圆形倒计时
需求: 之前接受到一个需求根据开始时间与结束时间,与当前时间做比对展示一个倒计时的动画效果。 先上效果图,毕竟无图无真相:
总共三种状态:未开始、进行中、已结束
一、分析
- 首先它是一个圆圈,这个好画
- 如何让它动起来,根据路径去展示动画,涉及到
PathMeasure
该类路径动画的使用 - 根据时间计算比例展示动画的位置
二、根据分析
2.1、自定义view应该都懂,主要是ondraw()方法
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
when (mCircleStyleType) {
CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START -> {
// 参数分别为 (文本 基线x 基线y 画笔),根据中心点以及文本基准线调整文本的中心
mCenterBaseline?.let {
canvas.drawText(
mHintText, mViewWidthCenterX,
it, mTextPaint
)
}
canvas.drawCircle(
mViewWidthCenterX,
mViewHeightCenterY, mCircleRadius, mPaintBg
)
}
CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING -> {
// 参数分别为 (文本 基线x 基线y 画笔),根据中心点以及文本基准线调整文本的中心
mCenterBaseline?.let {
canvas.drawText(
mRunningTimeHintText, mViewWidthCenterX.toFloat(),
it - mRunningTimeHintTextDistance, mTextPaint
)
canvas.drawText(
mRunningHintText, mViewWidthCenterX.toFloat(),
it + mRunningTimeHintTextDistance, mTextRunningHintPaint
)
}
canvas.drawPath(mAnimaPath, mPaint)
drawAnimationCircle(canvas)
}
CircleAnimatorViewStyleType.STYLE_TYPE_END -> {
// 参数分别为 (文本 基线x 基线y 画笔),根据中心点以及文本基准线调整文本的中心
mCenterBaseline?.let {
canvas.drawText(
mHintText, mViewWidthCenterX,
it, mTextPaint
)
}
canvas.drawCircle(
mViewWidthCenterX,
mViewHeightCenterY, mCircleRadius, mPaint
)
}
}
}
上面三个方法就是对应前面说的三种状态:未开始、进行中、已结束。
canvas.drawCircle()方法大家画一个圆, canvas.drawText()绘制圆中心的文案,
未开始与已结束状态都是画了一个圆,然后在圆中心写文案,这个相信大家都会
主要讲进行中
这一状态的动画过程:
那该如何讲解呢?,那就从使用开始讲起:
2.2、使用
xml如下:
<LinearLayout
android:id="@+id/constraintlayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<com.example.myanimator.circleanimator.CircleAnimatorViewFinish
android:id="@+id/cir_anima_finnish"
android:layout_width="wrap_content"
android:background="#17CC7D"
app:progressWidth="8dp"
app:circleRadius="40dp"
app:isNeedAnimation="true"
android:layout_marginTop="50dp"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/start_anima"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
activity如下:
class CircleActivity : AppCompatActivity() {
private lateinit var mBind: ActivityCircleScrollerBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBind = DataBindingUtil.setContentView(this, R.layout.activity_circle_scroller)
mBind.startAnima.setOnClickListener(){mBind.cirAnimaFinnish.setCircleStyleStart(System.currentTimeMillis()+3000,System.currentTimeMillis()+6*1000L)
}
}
}
上面主要是setCircleStyleStart()方法,传入你想要的开始时间与结束时间即可
/**根据课程时段自动选择动画类型*/
fun setCircleStyleStart(
startTime: Long,
endTime: Long
) {
mCircleStartTime = startTime
mCircleEndTime = endTime
val currentTime = System.currentTimeMillis()
when {
currentTime < startTime -> {//未开始
setCircleStyleTypeNotStart(startTime, endTime)
}
currentTime in startTime until endTime -> {//进行中
setCircleStyleTypeRunning(startTime, endTime)
}
currentTime >= endTime -> {//已结束
setCircleStyleTypeEnd(startTime, endTime)
}
}
}
setCircleStyleStart()方法主要是三种状态,是与当前时间作比较,绘制不同的状态。 主要看setCircleStyleTypeRunning(startTime, endTime)方法。
setCircleStyleTypeRunning(startTime, endTime)方法如下:
/**进行中*/
private fun setCircleStyleTypeRunning(
startTime: Long, endTime: Long
) {
setCircleCircleStyleType(CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING, startTime, endTime)
// LogUtil.d(
// TAG,
// "setCircleStyleTypeRunning:周期: ${endTime - startTime},已运行周期${System.currentTimeMillis() - startTime}"
// )
setCircleValueAnimatorStart(
startTime,
endTime,
endTime - startTime,
System.currentTimeMillis() - startTime
)
}
/**
*设置圆圈展示的样式
*/
private fun setCircleCircleStyleType(circleStyleType: CircleAnimatorViewStyleType, startTime: Long, endTime: Long) {
mCircleStyleType = circleStyleType
Log.e(TAG, "setCircleCircleStyleType type $circleStyleType")
when (mCircleStyleType) {
CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START -> {
mHintText = "未开始"
setStartTimeJobCollect(startTime, endTime)
}
CircleAnimatorViewStyleType.STYLE_TYPE_END -> {
mHintText = "已结束"
mObserverAnimatorEndListener?.invoke()
}
}
//防止之前是模式二 有动画
circleValueAnimator?.cancel()
mRunningFlowJob?.cancel()
mPaintBg.strokeWidth = mProgressWidth
postInvalidate()
}
setCircleCircleStyleType
方法取消之前是进行中
的时候取消之前的计时动画,
所以进行中主要是看 setCircleValueAnimatorStart( startTime, endTime, endTime - startTime, System.currentTimeMillis() - startTime)方法
/**
*
* 该方法只有 mCircleStyleType == 2时才会生效
*@param duration 总周期
*@param runningDuration 已运行周期
*/
private fun setCircleValueAnimatorStart(
startTime: Long, endTime: Long,
duration: Long,
runningDuration: Long
) {
if (mCircleStyleType == CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING) {
if (runningDuration >= duration) {
setCircleStyleTypeEnd(startTime, endTime)
return
}
//防止同样大小,覆盖的不全
mPaintBg.strokeWidth = mProgressWidth - 2
setAnimator(duration, runningDuration)
}
}
上面的方法主要 已运行周期
大于总周期
,那就是走周期结束的ui状态
mPaintBg.strokeWidth = mProgressWidth - 2
这段代码为什么要减2
呢?
因为我们的动画是首先灰色的是背景色,绿色的动画是我们画的路径动画setAnimator
方法。
setAnimator方法
就是我们的进行中的核心方法。
private fun setAnimator(
duration: Long,
runningDuration: Long
) {
if (mPathMeasure == null) {
init()
}
//倒计时时间与文案,计算剩余时间
mRunningRemainTime =
(mCircleEndTime - mCircleStartTime) - (System.currentTimeMillis() - mCircleStartTime)
mRunningTimeHintText = TimeFormatUtils.secToTime(mRunningRemainTime.toInt() / 1000)
//收集倒计时的文本
setRunningTextJobCollect()
var runningProgress = 0f
//防止周期越界
if (runningDuration <= 0) {
runningDurationLength = mPathMeasure!!.length
} else {
//动画圆倒计时,计算已运行的长度
runningDurationLength = runningDuration / duration * mPathMeasure!!.length
runningProgress = (runningDuration.toFloat() / duration.toFloat()).toFloat()
}
circleValueAnimator = ValueAnimator.ofFloat(0f, 1f)
circleValueAnimator?.let { circleValueAnimator ->
circleValueAnimator.repeatCount = 0
//设置动画的总周期,为你设置的开始结束时间的周期
circleValueAnimator.duration = duration.toLong()
circleValueAnimator.interpolator = LinearInterpolator()
circleValueAnimator.addUpdateListener { animation ->
mCurAnimValue = (animation.animatedValue as Float) + runningProgress
if (mCurAnimValue <= 1f) {
postInvalidate()
} else {
circleValueAnimator.cancel()
}
}
circleValueAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
setCircleStyleTypeEnd(mCircleStartTime, mCircleEndTime)
}
})
//防止android 5.0 属性动画失效
ValueAnimatorUtil.resetDurationScaleIfDisable()
circleValueAnimator.start()
}
}
1.首先获取当前剩余的时间mRunningRemainTime。 2.TimeFormatUtils.secToTime(mRunningRemainTime.toInt() / 1000)方法将剩余的时间转化为时分秒。 3.setRunningTextJobCollect(),收集运行的倒计时文案 4.runningProgress已运行周期在总的周期中所占的比例,对应动画的总周期的0-1f 5.addUpdateListener监听动画的进度更新mPathMeasure的长度 6.调用 postInvalidate()方法,走onDraw()方法 7. ValueAnimatorUtil.resetDurationScaleIfDisable()方法防止动画5.0失效
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
when (mCircleStyleType) {
....
CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING -> {
// 参数分别为 (文本 基线x 基线y 画笔),根据中心点以及文本基准线调整文本的中心
mCenterBaseline?.let {
canvas.drawText(//倒计时文案
mRunningTimeHintText, mViewWidthCenterX.toFloat(),
it - mRunningTimeHintTextDistance, mTextPaint
)
canvas.drawText(//"距离结束"的文案
mRunningHintText, mViewWidthCenterX.toFloat(),
it + mRunningTimeHintTextDistance, mTextRunningHintPaint
)
}
//灰色圈
canvas.drawPath(mAnimaPath, mPaint)
//绿色圈
drawAnimationCircle(canvas)
}
...
}
}
}
上面只看运行中:
动画的核心为drawAnimationCircle
,mPathMeasure的用法是先覆盖蓝色圈然后,慢慢褪去,顺时针画的圆弧,利用mPathMeasure 倒退绘制,模拟逆时针
/**先覆盖蓝色圈然后,慢慢褪去,顺时针画的圆弧,利用mPathMeasure 倒退绘制,模拟逆时针*/
private fun drawAnimationCircle(canvas: Canvas) {
if (mPathMeasure == null) {
return
}
endLength = (mPathMeasure!!.length * (1 - mCurAnimValue))
mDstPath.reset()
// LogUtil.d(
// TAG,
// "drawAnimationCircle-endLength: " + endLength + "圆绘制 动画进度" + mCurAnimValue + "mPathMeasurelength:" + mPathMeasure!!.getLength()
// )
mPathMeasure!!.getSegment(0f, endLength, mDstPath, true)
canvas.drawPath(mDstPath, mPaintBg)
}
//灰色圈
canvas.drawPath(mAnimaPath, mPaint)
//绿色圈
drawAnimationCircle(canvas)
上面相当于是先画了个灰色圆圈,再画了个绿色圆圈,然后根据动画进度,逐渐减少绿色圆圈的覆盖程度。
总结
1.根据传入的开始,结束时间,与当前时间做对比,判断处于哪种运行状态 2.将开始时间与当前时间做对比获取已运行时间,与当前的倒计时的总周期作对比 3.开启一个动画利用动画的周期进行ui的绘制刷新 4.画两个圆,一个是背景圆灰色,绿色圆我们的动画圆使用mPathMeasure进行绘制的
代码如下:
/**
* @author tgw
* @date 2021/12/10
* @describe 倒计时圆动画--
*/
class CircleAnimatorViewFinish(context: Context, attrs: AttributeSet?) : View(context, attrs),
CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Job() + Dispatchers.Main
companion object {
private const val TAG = "CircleAnimatorView"
fun dip2px(context: Context, dpValue: Float): Float {
val scale = context.resources.displayMetrics.density
return dpValue * scale + 0.5f
}
/**
* 定义几个圆圈类型的常量
* 动画类型,1 未开始,2进行中,3已结束
*/
// private const val STYLE_TYPE_NOT_START = 1
// private const val STYLE_TYPE_RUNNING = 2
// private const val STYLE_TYPE_END = 3
}
private lateinit var mAnimaPath: Path
private lateinit var mDstPath: Path
private var circleValueAnimator: ValueAnimator? = null
private var mPaint: Paint
private var mPaintBg: Paint
private var mTextPaint: Paint
//运行中特有画笔,文字要小
private var mTextRunningHintPaint: Paint
//整个控件的大小范围 也是圆的范围,也是文字的范围
private var circleRectF: RectF? = null
private var mPathMeasure: PathMeasure? = null
//动画进度
private var mCurAnimValue = 0f
private var endLength = 0f
private var mProgressWidth = 0f
private var mCircleRadius = 0f
//画笔颜色
private var mBgColor = Color.parseColor("#FF1FB5AB")
private var mProgressBgColor = Color.parseColor("#FFEFEFEF")
private var mRunningHintTextColor = Color.parseColor("#FF999999")
//控件大小
private var mViewHeight = 0
private var mViewWidth = 0
//控件中心点
private var mViewWidthCenterX = 0F
private var mViewHeightCenterY = 0F
//2进行中 动画圆倒计时,计算已运行的长度
private var runningDurationLength: Float = 0f
//动画类型,1 未开始,2进行中,3已结束
private var mCircleStyleType:CircleAnimatorViewStyleType = CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START
//课堂开始时间
private var mCircleStartTime = 0L
//课堂结束时间
private var mCircleEndTime = 0L
//动画类型,1 未开始,3已结束的文案与居中基准线
private var mHintTextSize = 36f
private var mHintText = ""
private var mCenterBaseline: Float? = 0f
//动画类型,进行中
private var mRunningRemainTime = 0L //相当于倒计时
private var mRunningTimeHintText = ""
private var mRunningTimeHintTextDistance = 0f //文本相距距离
private var mRunningHintText = "距离结束"
//协程流
private var mRunningFlowJob: Job? = null //进行中的倒计时文案
private var mStartTimeFlowJob: Job? = null //未开始,到时间后转化为进行中
//已结束的回调监听
var mObserverAnimatorEndListener: (() -> Unit)? = null
init {
setLayerType(LAYER_TYPE_SOFTWARE, null)
if (attrs != null) {
val typedArray =
getContext().obtainStyledAttributes(attrs, R.styleable.CircleAnimatorView)
mBgColor = typedArray.getColor(R.styleable.CircleAnimatorView_bgColor, mBgColor)
mProgressBgColor = typedArray.getColor(
R.styleable.CircleAnimatorView_animationProgressColor,
mProgressBgColor
)
mProgressWidth = dip2px(
context,
typedArray.getDimension(
R.styleable.CircleAnimatorView_progressWidth,
mProgressWidth
)
)
mCircleRadius =
dip2px(
context, typedArray.getDimension(
R.styleable.CircleAnimatorView_circleRadius,
mCircleRadius
)
)
mHintTextSize = dip2px(
context, typedArray.getDimension(
R.styleable.CircleAnimatorView_circleTextHintSize, mHintTextSize
)
)
typedArray.recycle()
}
//相当于蓝色圆圈覆盖灰色圆圈,然后蓝色圈圈慢慢消散
mPaint = Paint()
mPaint.isAntiAlias = true // 去除锯齿
mPaint.strokeWidth = mProgressWidth
mPaint.style = Paint.Style.STROKE
mPaint.color = mProgressBgColor
//相当于蓝色圆圈
mPaintBg = Paint()
mPaintBg.isAntiAlias = true // 去除锯齿
mPaintBg.strokeWidth = mProgressWidth - 2
mPaintBg.style = Paint.Style.STROKE
mPaintBg.color = mBgColor
mPaintBg.strokeCap = Paint.Cap.ROUND
mTextPaint = Paint() // 创建每个模式都有的文字画笔
mTextPaint.color = Color.BLACK // 设置颜色
mTextPaint.isAntiAlias = true // 去除锯齿
mTextPaint.style = Paint.Style.FILL // 设置样式
mTextPaint.textSize = mHintTextSize // 设置字体大小
mTextPaint.textAlign = Paint.Align.CENTER
//距离课堂结束文案的文字画笔
mTextRunningHintPaint = Paint() // 创建每个模式都有的文字画笔
mTextRunningHintPaint.color = mRunningHintTextColor // 设置颜色
mTextRunningHintPaint.isAntiAlias = true // 去除锯齿
mTextRunningHintPaint.style = Paint.Style.FILL // 设置样式
mTextRunningHintPaint.textSize = ScreenUtils.dip2px(context, 24f) // 设置字体大小
mTextRunningHintPaint.textAlign = Paint.Align.CENTER
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
mViewWidth = measureWidthOrHeight(widthMeasureSpec)
mViewHeight = measureWidthOrHeight(heightMeasureSpec)
setMeasuredDimension(mViewWidth, mViewHeight)
init()
}
private fun measureWidthOrHeight(measureSpec: Int): Int {
var result = 0
//获取当前View的测量模式
val mode = MeasureSpec.getMode(measureSpec)
//精准模式获取当前Viwe测量后的值,如果是最大值模式,会获取父View的大小.
val size = MeasureSpec.getSize(measureSpec)
if (mode == MeasureSpec.EXACTLY) {
//当测量模式为精准模式,返回设定的值
result = size
} else {
//设置为WrapContent的默认大小,圆的直径加上画笔宽度
result = (mCircleRadius * 2 + mProgressWidth).toInt()
if (mode == MeasureSpec.AT_MOST) {
//当模式为最大值的时候,默认大小和父类View的大小进行对比,返回最小的值
result = Math.min(result, size)
}
}
return result
}
private fun init() {
val path = mCircleRadius * 2
val left = (mViewWidth - mCircleRadius * 2) / 2
val top = (mViewHeight - mCircleRadius * 2) / 2
mViewWidthCenterX = (mViewWidth / 2).toFloat()
mViewHeightCenterY = (mViewHeight / 2).toFloat()
//画圆
circleRectF = RectF(left, top, path + left, path + top)
mAnimaPath = Path()
mDstPath = Path()
// mAnimaPath.addArc(circleRectF, -90f, 359f)
mAnimaPath.arcTo(circleRectF!!, -90f, 359f, true)
mPathMeasure = PathMeasure(mAnimaPath, true)
//画居中文本,计算基准线
// 参数分别为 (文本 基线x 基线y 画笔),根据中心点以及文本基准线调整文本的中心
val fontMetrics: Paint.FontMetrics = mTextPaint.fontMetrics
val distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom
mCenterBaseline = circleRectF?.centerY()?.plus(distance)
//运行时 两行文本相距距离
mRunningTimeHintTextDistance = ScreenUtils.dip2px(context, 30f)
}
/**根据课程时段自动选择动画类型*/
fun setCircleStyleStart(
startTime: Long,
endTime: Long
) {
mCircleStartTime = startTime
mCircleEndTime = endTime
val currentTime = System.currentTimeMillis()
when {
currentTime < startTime -> {
setCircleStyleTypeNotStart(startTime, endTime)
}
currentTime in startTime until endTime -> {
setCircleStyleTypeRunning(startTime, endTime)
}
currentTime >= endTime -> {
setCircleStyleTypeEnd(startTime, endTime)
}
}
}
/**未开始*/
private fun setCircleStyleTypeNotStart(startTime: Long, endTime: Long) {
val currentTime = System.currentTimeMillis()
if (currentTime < startTime) {
setCircleCircleStyleType(CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START, startTime, endTime)
}
}
/**进行中*/
private fun setCircleStyleTypeRunning(
startTime: Long, endTime: Long
) {
setCircleCircleStyleType(CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING, startTime, endTime)
// LogUtil.d(
// TAG,
// "setCircleStyleTypeRunning:周期: ${endTime - startTime},已运行周期${System.currentTimeMillis() - startTime}"
// )
setCircleValueAnimatorStart(
startTime,
endTime,
endTime - startTime,
System.currentTimeMillis() - startTime
)
}
/**已结束*/
private fun setCircleStyleTypeEnd(startTime: Long, endTime: Long) {
val currentTime = System.currentTimeMillis()
if (currentTime >= endTime) {
setCircleCircleStyleType(CircleAnimatorViewStyleType.STYLE_TYPE_END, startTime, endTime)
}
}
/**
*
* 该方法只有 mCircleStyleType == 2时才会生效
*@param duration 总周期
*@param runningDuration 已运行周期
*/
private fun setCircleValueAnimatorStart(
startTime: Long, endTime: Long,
duration: Long,
runningDuration: Long
) {
if (mCircleStyleType == CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING) {
if (runningDuration >= duration) {
setCircleStyleTypeEnd(startTime, endTime)
return
}
//防止同样大小,覆盖的不全
mPaintBg.strokeWidth = mProgressWidth - 2
setAnimator(duration, runningDuration)
}
}
/**
*设置圆圈展示的样式
*/
private fun setCircleCircleStyleType(circleStyleType: CircleAnimatorViewStyleType, startTime: Long, endTime: Long) {
mCircleStyleType = circleStyleType
Log.e(TAG, "setCircleCircleStyleType type $circleStyleType")
when (mCircleStyleType) {
CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START -> {
mHintText = "未开始"
setStartTimeJobCollect(startTime, endTime)
}
CircleAnimatorViewStyleType.STYLE_TYPE_END -> {
mHintText = "已结束"
mObserverAnimatorEndListener?.invoke()
}
}
//防止之前是模式二 有动画
circleValueAnimator?.cancel()
mRunningFlowJob?.cancel()
mPaintBg.strokeWidth = mProgressWidth
postInvalidate()
}
/**进入时是未开始 课程到开始时间后 自动ui调整*/
private fun setStartTimeJobCollect(startTime: Long, endTime: Long) {
mStartTimeFlowJob?.cancel()
/**辅助进入的时候为未开始,到了时间点将为开始*/
val countDownStartTimeFlow = flow<Boolean> {
while (mCircleStyleType == CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START) {
delay(1000)
if (System.currentTimeMillis() > startTime) {
emit(true)
} else {
emit(false)
}
}
}.flowOn(Dispatchers.IO)
mStartTimeFlowJob = launch(Dispatchers.Main) {
countDownStartTimeFlow.collect {
if (it) {
setCircleStyleTypeRunning(startTime, endTime)
mStartTimeFlowJob?.cancel()
}
}
}
}
private fun setAnimator(
duration: Long,
runningDuration: Long
) {
if (mPathMeasure == null) {
init()
}
//倒计时时间与文案,计算剩余时间
mRunningRemainTime =
(mCircleEndTime - mCircleStartTime) - (System.currentTimeMillis() - mCircleStartTime)
mRunningTimeHintText = TimeFormatUtils.secToTime(mRunningRemainTime.toInt() / 1000)
//收集倒计时的文本
setRunningTextJobCollect()
var runningProgress = 0f
if (runningDuration <= 0) {
runningDurationLength = mPathMeasure!!.length
} else {
//动画圆倒计时,计算已运行的长度
runningDurationLength = runningDuration / duration * mPathMeasure!!.length
runningProgress = (runningDuration.toFloat() / duration.toFloat()).toFloat()
}
circleValueAnimator = ValueAnimator.ofFloat(0f, 1f)
circleValueAnimator?.let { circleValueAnimator ->
circleValueAnimator.repeatCount = 0
circleValueAnimator.duration = duration.toLong()
circleValueAnimator.interpolator = LinearInterpolator()
circleValueAnimator.addUpdateListener { animation ->
mCurAnimValue = (animation.animatedValue as Float) + runningProgress
if (mCurAnimValue <= 1f) {
postInvalidate()
} else {
circleValueAnimator.cancel()
}
}
circleValueAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
setCircleStyleTypeEnd(mCircleStartTime, mCircleEndTime)
}
})
//防止android 5.0 属性动画失效
ValueAnimatorUtil.resetDurationScaleIfDisable()
circleValueAnimator.start()
}
}
/**
*运行时动画的倒计时文案
*/
private fun setRunningTextJobCollect() {
mRunningFlowJob?.cancel()
var time = 0
val countDownRunningStyleTextHint = flow<String> {
while (mCircleStyleType == CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING) {
delay(500)
mRunningRemainTime =
(mCircleEndTime - mCircleStartTime) - (System.currentTimeMillis() - mCircleStartTime)
time = mRunningRemainTime.toInt() / 1000
emit(TimeFormatUtils.secToTime(time))
}
}.flowOn(Dispatchers.IO)
mRunningFlowJob = launch {
countDownRunningStyleTextHint.collect {
mRunningTimeHintText = it
}
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
when (mCircleStyleType) {
CircleAnimatorViewStyleType.STYLE_TYPE_NOT_START -> {
// 参数分别为 (文本 基线x 基线y 画笔),根据中心点以及文本基准线调整文本的中心
mCenterBaseline?.let {
canvas.drawText(
mHintText, mViewWidthCenterX,
it, mTextPaint
)
}
canvas.drawCircle(
mViewWidthCenterX,
mViewHeightCenterY, mCircleRadius, mPaintBg
)
}
CircleAnimatorViewStyleType.STYLE_TYPE_RUNNING -> {
// 参数分别为 (文本 基线x 基线y 画笔),根据中心点以及文本基准线调整文本的中心
mCenterBaseline?.let {
canvas.drawText(
mRunningTimeHintText, mViewWidthCenterX.toFloat(),
it - mRunningTimeHintTextDistance, mTextPaint
)
canvas.drawText(
mRunningHintText, mViewWidthCenterX.toFloat(),
it + mRunningTimeHintTextDistance, mTextRunningHintPaint
)
}
canvas.drawPath(mAnimaPath, mPaint)
drawAnimationCircle(canvas)
}
CircleAnimatorViewStyleType.STYLE_TYPE_END -> {
// 参数分别为 (文本 基线x 基线y 画笔),根据中心点以及文本基准线调整文本的中心
mCenterBaseline?.let {
canvas.drawText(
mHintText, mViewWidthCenterX,
it, mTextPaint
)
}
canvas.drawCircle(
mViewWidthCenterX,
mViewHeightCenterY, mCircleRadius, mPaint
)
}
}
}
/**先覆盖蓝色圈然后,慢慢褪去,顺时针画的圆弧,利用mPathMeasure 倒退绘制,模拟逆时针*/
private fun drawAnimationCircle(canvas: Canvas) {
if (mPathMeasure == null) {
return
}
endLength = (mPathMeasure!!.length * (1 - mCurAnimValue))
mDstPath.reset()
// LogUtil.d(
// TAG,
// "drawAnimationCircle-endLength: " + endLength + "圆绘制 动画进度" + mCurAnimValue + "mPathMeasurelength:" + mPathMeasure!!.getLength()
// )
mPathMeasure!!.getSegment(0f, endLength, mDstPath, true)
canvas.drawPath(mDstPath, mPaintBg)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
onDestroy()
}
fun onDestroy() {
mRunningFlowJob?.cancel()
mStartTimeFlowJob?.cancel()
circleValueAnimator?.cancel()
coroutineContext.cancel()
mObserverAnimatorEndListener = null
}
enum class CircleAnimatorViewStyleType{
//未开始
STYLE_TYPE_NOT_START ,
//进行中
STYLE_TYPE_RUNNING ,
//已结束
STYLE_TYPE_END ,
}
}
/**
* @author tgw
* @date 2021/12/13
* @describe 防止 Android5.0 动画无效
*/
public class ValueAnimatorUtil {
/**
* 如果动画被禁用,则重置动画缩放时长
*/
public static void resetDurationScaleIfDisable() {
if (getDurationScale() == 0)
resetDurationScale();
}
/**
* 重置动画缩放时长
*/
public static void resetDurationScale() {
try {
getField().setFloat(null, 1);
} catch (Exception e) {
e.printStackTrace();
}
}
private static float getDurationScale() {
try {
return getField().getFloat(null);
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
@NonNull
private static Field getField() throws NoSuchFieldException {
Field field = ValueAnimator.class.getDeclaredField("sDurationScale");
field.setAccessible(true);
return field;
}
}
/**
* @author tgw
* @date 2021/7/26
* @describe
*/
object TimeFormatUtils {
val YY_MM_DD_HH_MM_SS = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
val HH_MM = SimpleDateFormat("HH:mm", Locale.ENGLISH)
val YY_MM_DD = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
val HH_MM_SS = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
/*
* 将时间戳转换为时间
*/
fun stampToDate(time: Long, format: SimpleDateFormat): String? {
val res: String
val date = Date(time)
res = format.format(date)
return res
}
/*
* 将时间转换为时间戳
*/
@Throws(ParseException::class)
fun dateToStamp(time: String?, format: SimpleDateFormat): String? {
val date = format.parse(time)
val ts = date.time
return ts.toString()
}
/**将秒换为时分秒00:00:00*/
fun secToTime(time: Int): String {
var timeStr: String = ""
var hour = 0
var minute = 0
var second = 0
if (time <= 0) {
return "00:00:00"
} else {
minute = time / 60
if (minute < 60) {
second = time % 60
timeStr = "00:" + unitFormat(minute) + ":" + unitFormat(second)
} else {
hour = minute / 60
if (hour > 99) {
return "99:59:59"
}
minute %= 60
second = time - hour * 3600 - minute * 60
timeStr = unitFormat(hour) + ":" + unitFormat(minute) + ":" + unitFormat(second)
}
}
return timeStr
}
/**文案补0*/
private fun unitFormat(i: Int): String {
var retStr: String? = null
retStr = if (i >= 0 && i < 10) "0" + Integer.toString(i) else "" + i
return retStr
}
}
参考:
Android自定义view入门与实战
Android 5.0 动画失效:blog.csdn.net/u011387817/…