#CircleProgress Animation (with gradient)
원형 프로그레스 Animation에 대해 간단히 필요한 부분만 알아보겠습니다.
1. CustomView에서 drawArc 이용
2. ValueAnimator를 이용해 Draw할 Progress를 계산
3. 업데이트 된 Progress를 drawArc
*Arc = 호
#1 - attr.xml 정의
따로 설정해서 사용하고 싶은 속성값들을 정의 합니다.
기본적으로 아래의 값들만 설정값으로 받고, 나머지는 하드코딩 되어 있으니
필요하다면 추가설정해서 사용 하면 됩니다.
attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleProgressView">
<!--duration-->
<attr name="animateDuration" format="integer"/>
<!--value-->
<attr name="maxValue" format="float"/> <!--최대 progress value-->
<attr name="targetValue" format="float"/> <!--표시할 타겟 progress value-->
<!--track-->
<attr name="trackColor" format="color"/>
<!--stroke-->
<attr name="strokeWidth" format="dimension"/> <!--dp로 설정-->
<attr name="strokeGradientStartColor" format="color"/>
<attr name="strokeGradientEndColor" format="color"/>
<!--text-->
<attr name="text" format="string"/>
</declare-styleable>
</resources>
레이아웃.xml
레이아웃에서 attrs.xml에 정의했던 속성들을 설정해서 사용합니다.
com.example.animatin.CircularProgressView는 생성할 예정 입니다.
<com.example.animation.CircularProgressView
android:layout_width="200dp"
android:layout_height="200dp"
app:animateDuration="1000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:maxValue="100"
app:strokeGradientEndColor="@color/stroke_gradient_end_color"
app:strokeGradientStartColor="@color/stroke_gradient_start_color"
app:strokeWidth="50dp"
app:targetValue="80"
app:trackColor="@color/track_color" />
#2 - CircleProgressView 생성
animateProgress를 보시면 ValueAnimator를 통해 얻은 progress 값을 토대로 'invalidate'를 통해 뷰를 계속 draw 하는 것을 볼 수 있습니다.
디테일한 모양은 draw 부분의 코드를 분석 후 수정 하시면 됩니다.
class CircularProgressView : View {
private var strokeGradientStartColor: Int =
ContextCompat.getColor(context, R.color.stroke_gradient_start_color)
private var strokeGradientEndColor: Int =
ContextCompat.getColor(context, R.color.stroke_gradient_end_color)
private var trackColor: Int = ContextCompat.getColor(context, R.color.track_color)
private var strokeWidth: Float = 150f
private var maxProgressValue: Float = 100f
private var targetProgressValue: Float = 80f
private var animateDuration: Int = 2000
private var animatedProgress: Float = 80f
private var text: String? = null
set(value) {
field = value
postInvalidate()
}
private var viewBound: RectF = RectF()
private var textBounds = Rect()
private var minDimen = 0f
private var textWidth = 0f
private var textHeight = 0f
//Paint 셋팅
private var strokePaint = Paint().also {
it.style = Paint.Style.STROKE
it.strokeCap = Paint.Cap.ROUND
it.flags = Paint.ANTI_ALIAS_FLAG
}
private var trackPaint = Paint().also {
it.style = Paint.Style.STROKE
it.flags = Paint.ANTI_ALIAS_FLAG
it.alpha = 0
}
private var textPaint = Paint().also {
it.textAlign = Paint.Align.CENTER
it.flags = Paint.ANTI_ALIAS_FLAG
it.color = ContextCompat.getColor(context, R.color.textcolor)
it.textSize = 100f
}
private var strokeShader: Shader? = null
constructor(context: Context) : super(context) {
init(null)
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
init(attributeSet)
}
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(
context,
attributeSet,
defStyleAttr
) {
init(attributeSet)
}
private fun init(attributeSet: AttributeSet?) {
if (attributeSet == null) return
val attrs = context.theme.obtainStyledAttributes(
attributeSet,
R.styleable.CircleProgressView,
0,
0
)
initAttrValue(attrs)
setStrokeGradient()
}
private fun initAttrValue(attrs: TypedArray) {
try {
animateDuration =
attrs.getInt(R.styleable.CircleProgressView_animateDuration, animateDuration)
maxProgressValue =
attrs.getFloat(R.styleable.CircleProgressView_maxValue, maxProgressValue)
targetProgressValue =
attrs.getFloat(R.styleable.CircleProgressView_targetValue, targetProgressValue)
strokeWidth = attrs.getDimension(
R.styleable.CircleProgressView_strokeWidth,
strokeWidth
)
strokeGradientStartColor =
attrs.getColor(
R.styleable.CircleProgressView_strokeGradientStartColor,
strokeGradientStartColor
)
strokeGradientEndColor = attrs.getColor(
R.styleable.CircleProgressView_strokeGradientEndColor,
strokeGradientEndColor
)
text = attrs.getString(R.styleable.CircleProgressView_text)
} finally {
attrs.recycle()
}
}
private fun setStrokeGradient() {
if (minDimen == 0f) return
val center = viewBound.width() / 2f
val halfGradientPositions = floatArrayOf(0f, 1 / 4f, 3 / 4f, 1f)
val details = Pair(
intArrayOf(
calMiddleColor(strokeGradientStartColor, strokeGradientEndColor),
strokeGradientEndColor,
strokeGradientStartColor,
calMiddleColor(strokeGradientStartColor, strokeGradientEndColor),
), halfGradientPositions
)
strokeShader = SweepGradient(center, center, details.first, details.second)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
Log.i("syTest", "onMeasure : $widthMeasureSpec/$heightMeasureSpec");
val targetWidth = suggestedMinimumWidth + paddingLeft + paddingRight
val targetHeight = suggestedMinimumHeight + paddingTop + paddingBottom
val calcWidth = measureDimen(targetWidth, widthMeasureSpec)
val calcHeight = measureDimen(targetHeight, widthMeasureSpec)
val minDimen = min(calcWidth, calcHeight)
setMeasuredDimension(minDimen, minDimen)
}
private fun measureDimen(targetSize: Int, measureSpec: Int): Int {
var result = 0
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
result = when (mode) {
MeasureSpec.EXACTLY -> size
MeasureSpec.AT_MOST -> min(result, size)
else -> targetSize
}
if (result < targetSize) Log.e("syTest", "error")
return result
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
animateProgress()
}
override fun onDraw(canvas: Canvas?) {
if (canvas == null) {
Log.e("syTest", "canvas is null")
return
}
minDimen = min(width, height).toFloat()
setStrokeGradient()
trackPaint.let {
it.color = trackColor
it.strokeWidth = strokeWidth //trackWidth strokeWidth와 동일하게 설정
}
strokePaint.let {
// it.color = strokeColor //Stroke Gradient을 사용하지 않을 거면 color만 설정(shader 설정 하지 말고)
it.strokeCap = Paint.Cap.ROUND
it.strokeWidth = strokeWidth
it.shader = strokeShader //Stroke Gradient를 담당
}
//set bounds
viewBound.let {
it.left = paddingLeft.toFloat()
it.top = paddingTop.toFloat()
it.right = (width - paddingRight).toFloat()
it.bottom = (height - paddingBottom).toFloat()
}
//draw track
canvas.drawArc(
viewBound.left + strokeWidth / 2,
viewBound.top + strokeWidth / 2,
viewBound.right - strokeWidth / 2,
viewBound.bottom - strokeWidth / 2,
0f, 360f, false, trackPaint
)
//draw stroke
val startAngle = -90f
val sweepAngle = (animatedProgress / maxProgressValue) * 360f
canvas.drawArc(
viewBound.left + strokeWidth / 2,
viewBound.top + strokeWidth / 2,
viewBound.right - strokeWidth / 2,
viewBound.bottom - strokeWidth / 2,
startAngle, sweepAngle, false, strokePaint
)
//draw text
val percentText = String.format("%.0f%%", animatedProgress / maxProgressValue * 100)
textPaint.getTextBounds(percentText, 0, percentText.length, textBounds)
textHeight = textBounds.height().toFloat()
textWidth = textBounds.width().toFloat()
canvas.drawText(
percentText,
viewBound.centerX(),
minDimen / 2 + textBounds.height() / 2,
textPaint
)
}
private var isAnimating = false
private fun animateProgress() {
if (isAnimating) return
ValueAnimator.ofFloat(0f, targetProgressValue).apply {
interpolator = DecelerateInterpolator()
this.duration = animateDuration.toLong()
addUpdateListener {
animatedProgress = it.animatedValue as Float
invalidate()
isAnimating = it.animatedFraction != 1f
if (!isAnimating) {
//Animate End
} else {
//Animate Progress
}
}
}.start()
isAnimating = true
}
private fun calMiddleColor(color1: Int, color2: Int): Int {
val alpha = (Color.alpha(color1) + Color.alpha(color2)) / 2
val red = (Color.red(color1) + Color.red(color2)) / 2
val green = (Color.green(color1) + Color.green(color2)) / 2
val blue = (Color.blue(color1) + Color.blue(color2)) / 2
return Color.argb(alpha, red, green, blue)
}
}
잘못된 내용이나 궁금한 내용 있으시면 댓글 달아주세요
좋은 하루 되세요!
'Android - 기능' 카테고리의 다른 글
[Android] ImageView를 Toggle 버튼처럼 사용하기 (1) | 2022.03.17 |
---|---|
[Android] 바(Bar) 프로그레스 애니메이션 (0) | 2022.03.14 |
[Andoird] 카메라 촬영/갤러리 이미지 가져오기 (0) | 2021.11.24 |
[Android] startActivityForResult @deprecated (0) | 2021.11.17 |
[Android] OutLineTextView - 글자 외곽선 더하기 (1) | 2021.11.14 |