“我报名参加金石方案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/…