본문 바로가기

Android - 기능

[Android] 원형(Circle) 프로그레스 애니메이션

#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)
    }
}

잘못된 내용이나 궁금한 내용 있으시면 댓글 달아주세요

좋은 하루 되세요!