개발일지

Android in A..Z - View (CustomView) 본문

Android (안드로이드)/View

Android in A..Z - View (CustomView)

강태종 2021. 4. 29. 15:40

CustomView

Android에서 기본으로 제공하는 View로 UI를 구축할 수 없을 때 사용자가 직접 View를 상속받아서 CustomView를 만들 수 있다.


ProgressView 전체코드

class ProgressView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) : View(context, attrs, defStyleAttr, defStyleRes) {
    private val progressBarBackgroundPaint = Paint().apply {
        isAntiAlias = true
        style = Paint.Style.STROKE
        color = Color.parseColor("#646F88")
        strokeWidth = 20 * resources.displayMetrics.density
    }
    private val progressBarPaint = Paint().apply {
        isAntiAlias = true
        style = Paint.Style.STROKE
        color = Color.parseColor("#61AFEF")
        strokeWidth = 20 * resources.displayMetrics.density
    }

    private val percentPaint = Paint().apply {
        isAntiAlias = true
        style = Paint.Style.FILL_AND_STROKE
        textAlign = Paint.Align.CENTER
        textSize = 20 * resources.displayMetrics.density
        color = Color.parseColor("#000000")
    }

    var maxValue = 100
        set(value) {
            field = value
            invalidate()
        }

    var value = 0
        set(value) {
            field = value
            invalidate()
        }

    var textSize: Float
        get() {
            return percentPaint.textSize
        }

        set(value) {
            percentPaint.textSize = value
            invalidate()
        }

    var textColor: Int
        get() {
            return percentPaint.color
        }
        set(value) {
            percentPaint.color = value
            invalidate()
        }

    var progressBarBackgroundColor: Int
        get() {
            return progressBarBackgroundPaint.color
        }
        set(value) {
            progressBarBackgroundPaint.color = value
            invalidate()
        }

    var progressBarColor: Int
        get() {
            return progressBarPaint.color
        }
        set(value) {
            progressBarPaint.color = value
            invalidate()
        }

    var progressBarWidth: Float
        get() {
            return progressBarPaint.strokeWidth
        }
        set(value) {
            progressBarPaint.strokeWidth = value
            progressBarBackgroundPaint.strokeWidth = value
        }

    init {
        context.theme.obtainStyledAttributes(attrs, R.styleable.ProgressView, defStyleAttr, defStyleRes).apply {
            maxValue = getInt(R.styleable.ProgressView_maxValue, maxValue)
            value = getInt(R.styleable.ProgressView_value, value)
            textSize = getDimension(R.styleable.ProgressView_textSize, textSize)
            textColor = getColor(R.styleable.ProgressView_textColor, textColor)
            progressBarBackgroundColor = getColor(R.styleable.ProgressView_progressBarBackgroundColor, progressBarBackgroundColor)
            progressBarColor = getColor(R.styleable.ProgressView_progressBarColor, progressBarColor)
            progressBarWidth = getDimension(R.styleable.ProgressView_progressBarWidth, progressBarWidth)
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var w = MeasureSpec.getSize(widthMeasureSpec)
        var h = MeasureSpec.getSize(heightMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)

        when {
            (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) && (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) -> {
                w = (100 * resources.displayMetrics.density + 0.5).toInt()
                h = (100 * resources.displayMetrics.density + 0.5).toInt()
            }
            widthMode == MeasureSpec.EXACTLY && (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) -> {
                h = w
            }
            (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) && heightMode == MeasureSpec.EXACTLY -> {
                w = h
            }
        }

        setMeasuredDimension(w, h)
    }

    override fun onDraw(canvas: Canvas) {
        val left = (paddingLeft + progressBarWidth/2)
        val top = (paddingTop + progressBarWidth/2)
        val right = (width - paddingLeft - paddingRight - progressBarWidth/2)
        val bottom = (height - paddingTop - paddingBottom - progressBarWidth/2)
        val x = (left + right)/2
        val y = (top + bottom)/2 + textSize/2

        drawProgressBarBackground(canvas, left, top, right, bottom)
        drawProgress(canvas, left, top, right, bottom, 360F * value / maxValue)
        drawPercent(canvas, x, y)
    }

    private fun drawProgressBarBackground(canvas: Canvas, left: Float, top: Float, right: Float, bottom: Float) {
        canvas.drawArc(left, top, right, bottom, -90F, 360F, false, progressBarBackgroundPaint)
    }

    private fun drawProgress(canvas: Canvas, left: Float, top: Float, right: Float, bottom: Float, angle: Float) {
        canvas.drawArc(left, top, right, bottom, -90F, angle, false, progressBarPaint)
    }

    private fun drawPercent(canvas: Canvas, x: Float, y: Float) {
        canvas.drawText("${value * 100 / maxValue}%", x, y, percentPaint)
    }
}

onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var w = MeasureSpec.getSize(widthMeasureSpec)
        var h = MeasureSpec.getSize(heightMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)

        when {
            (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) && (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) -> {
                w = (100 * resources.displayMetrics.density + 0.5).toInt()
                h = (100 * resources.displayMetrics.density + 0.5).toInt()
            }
            widthMode == MeasureSpec.EXACTLY && (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) -> {
                h = w
            }
            (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) && heightMode == MeasureSpec.EXACTLY -> {
                w = h
            }
        }

        setMeasuredDimension(w, h)
    }

 

View.onMeasure(int widthMeasureSpec, int heightMeasureSpec)

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

onMeasure는 View의 크기를 측정하는 함수이다. onMeasure 단계에서 setMeasuredDimension을 호출하지 않으면 Exception이 발생하기 때문에 setMeasuredDimension을 호출해야 한다. CustomView에 맞춰서 크기를 측정할 경우 onMeasure을 재정의하여 super.onMeasure을 호출하지 않아도 된다.

* setMeasuredDimension은 Pixel단위기 때문에 DP로 변환하는 과정이 필요하다.


onDraw(canvas: Canvas)

    override fun onDraw(canvas: Canvas) {
        val left = (paddingLeft + progressBarWidth/2)
        val top = (paddingTop + progressBarWidth/2)
        val right = (width - paddingLeft - paddingRight - progressBarWidth/2)
        val bottom = (height - paddingTop - paddingBottom - progressBarWidth/2)
        val x = (left + right)/2
        val y = (top + bottom)/2 + textSize/2

        drawProgressBarBackground(canvas, left, top, right, bottom)
        drawProgress(canvas, left, top, right, bottom, 360F * value / maxValue)
        drawPercent(canvas, x, y)
    }

    private fun drawProgressBarBackground(canvas: Canvas, left: Float, top: Float, right: Float, bottom: Float) {
        canvas.drawArc(left, top, right, bottom, -90F, 360F, false, progressBarBackgroundPaint)
    }

    private fun drawProgress(canvas: Canvas, left: Float, top: Float, right: Float, bottom: Float, angle: Float) {
        canvas.drawArc(left, top, right, bottom, -90F, angle, false, progressBarPaint)
    }

    private fun drawPercent(canvas: Canvas, x: Float, y: Float) {
        canvas.drawText("${value * 100 / maxValue}%", x, y, percentPaint)
    }

onDraw는 View를 그리는 함수이다. onDraw에서 Canvas를 통해 그래픽을 그릴 수 있다. 주의할 점은 onDraw는 자주 호출되기 때문에 onDraw에서 객체를 생성하는 행위는 하지 않는 것이 좋다. ( onDraw에서 객체를 생성하고 소멸하는 행위가 자주 호출되고 이는 GC를 자주 호출하기 때문에 성능에 좋지 않다. )


Git (예제코드)

github.com/KangTaeJong98/Example/tree/main/Android/CustomView

 

KangTaeJong98/Example

My Example Code. Contribute to KangTaeJong98/Example development by creating an account on GitHub.

github.com

 

Comments