Android/Android관련 이것 저것..

View가 그려지는 과정 알아보기

봄석 2019. 10. 2. 13:18

View가 그려지는 과정 알아보기

 

view는 포커스를 얻으면 레이아웃을 그리도록 요청합니다.

이때 레이아웃의 계층 구조중에 rootView를 제공해야합니다.

따라서 그리기는 루트 노드에서 시작되어 전위 순회방식으로 그려집니다.

부모뷰는 자식뷰가 그려지기전에 그려지고, 형제뷰는 전위방식에 따라 순서대로 그려지게 됩니다.

레이아웃을 그리는 과정은 measure단계와 layout 단계를 통해 그려지게 됩니다.

 

LifeCycle 알아보기

addView  함수를 호출했을때 위 그림과 같은 순서로 콜백함수가 실행되게 됩니다.

 

 

1. Constructor

모든 뷰는 생성자에서 출발하게됩니다.

생성자에서 초기화하고 ,default값을 설정합니다. 뷰는 초기설정을 쉽게 세팅하기 위해서 

AttributeSet 이라는 인터페이스를 지원합니다.

attrs.xml파일(res/valeus/attrs.xml)을 만들어 이것을 부름으로서 뷰의 설정값을 쉽게 설정할 수있습니다.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="pen">
        <attr name="strokeWidth" format="dimension" />
        <attr name="strokeColor" format="color" />
    </declare-styleable>
    
    ....
    
</resources>

위같은 식으로 리소스를 작성한 후

아래처럼 이것을 부름으로써 뷰의 설정값을 쉽게 설정할 수있습니다(API21 이상에서)

 

CustomView(Source File)에서는 아래와 같이 두번째 생성자 안처럼 설정값을 불러와 사용할 수 있습니다.

class MyVie : View {

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0){
        val strokeWidth = context.obtainStyledAttributes(attrs, R.styleable.NewAttr)
            .getDimensionPixelSize(
                R.styleable.NewAttr_strokeWidth,
                context.resources.getDimensionPixelSize(R.dimen._2dp)
            )

        val color = context.obtainStyledAttributes(attrs, R.styleable.NewAttr)
            .getColor(R.styleable.NewAttr_strokeColor, Color.WHITE)
    }

    constructor(
        context: Context,
        attrs: AttributeSet?,
        defStyleAttr: Int
    ) : super(context, attrs, defStyleAttr)
...
}

 

2. onAttachedToWindow()

부모 뷰가 addView(childView)를 호출한 뒤 자식뷰는 윈도우에 붙게  됩니다.

이때 뷰의 id를 통해 접근할 수 있습니다.

 

3. onMeasure()

뷰의 크기를 측정하는 단계로 중요합니다.

레이아웃에 맞게 특정 크기를 가져가야합니다.

 

여기서는 세가지의 단계가 있는데 

  1. 뷰가 원하는 사이즈를 계산합니다.
  2. MeasureSpec 에 따라 mode 를 가져옵니다.
  3. MeasureSpec의 mode를 체크하여 뷰의 크기를 적용합니다.
 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        //2
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)

        //3
        val width = when (widthMode) {
            MeasureSpec.EXACTLY -> widthSize
            MeasureSpec.AT_MOST -> (paddingLeft + paddingRight + suggestedMinimumWidth)
                .coerceAtMost(widthSize)
            else -> widthMeasureSpec
        }

        val height = when (heightMode) {
            MeasureSpec.EXACTLY -> heightSize
            MeasureSpec.AT_MOST -> (paddingTop + paddingBottom + suggestedMinimumHeight)
                .coerceAtMost(heightSize)
            else -> heightMeasureSpec
        }

        setMeasuredDimension(width, height)
    }

MeasureSpec.AT_MOST : wrap_content 에 매핑되며 뷰 내부의 크기에 따라 크기가 달라집니다.

MeasureSpec.EXACTLY : fill_parent, match_parent 로 외부에서 미리 크기가 지정되었다.

MeasureSpec.UNSPECIFIED : Mode 가 설정되지 않았을 경우. 소스상에서 직접 넣었을 때 주로 불립니다.

4. onLayout()

뷰의 위치와 크기를 할당합니다.

onMeasure 를 통해 사이즈가 결정된 후에 onLayout 이 불립니다.

부모뷰일때 주로 쓰이며, child 뷰를 붙일 때 위치를 정해주는데 사용합니다.

넘어오는 파라미터는 어플리케이션 전체를 기준으로 위치가 넘어옵니다.( 주의!! )

 

5. onDraw()

뷰를 실제로 그리는 단계입니다.

Canvas와 Paint를 이용하여 필요한 내용을 그립니다.

 

여기서 주의할 점은 onDraw함수를 호출시 많은 시간이 소요됩니다. Scroll 또는 Swipe 등을 할 경우 뷰는 다시 onDraw와 onLayout을 다시 호출하게 됩니다. 따라서 함수 내에서 객체할당을 피하고 한 번 할당한 객체를 재사용할 것을 권장합니다.

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        val width = measuredWidth + 0.0f
        val height = measuredHeight + 0.0f

        val circle = Paint()
        circle.color = this.lineColor
        circle.strokeWidth = 10f
        circle.isAntiAlias = false
        circle.style = Paint.Style.STROKE

        canvas?.drawArc(
            RectF(
                10f, 10f, width - 10f, height - 10f
            ), -90f,
            (this.curValue + 0.0f) / (this.maxValue + 0.0f) * 360, false, circle
        )

        val textp = Paint()
        textp.color = Color.BLACK
        textp.textSize = 30f
        textp.textAlign = Paint.Align.CENTER


        if (System.currentTimeMillis() / 1000 % 2 == 0L) {
            canvas?.drawText(
                "${this.curValue} / ${this.maxValue}",
                (width / 2),
                (height / 2),
                textp
            )
        }

        Observable.interval(1, TimeUnit.SECONDS)
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                invalidate()
            }, {

            })
            .addTo(disposable)
    }

퍼센티지를 보여주는 숫자 0~100과 그에 맞는 퍼센티지를 원으로 보여주는 뷰를 그립니다.

invalidate를 통하여 1초마다 갱신하여 다시그립니다.

 

ViewUpdate

  • invalidate()
    • 단순히 뷰를 다시 그릴때 사용됩니다. 예를 들어 뷰의 text 또는 color가 변경되거나 , touch interactivity가 발생할 때 onDraw()함수를 재호출하면서 뷰를 업데이트합니다.
  • requestLayout()
    • onMeasure()부터 다시 뷰의 그린다. 뷰의 사이즈가 변경될때 그것을 다시 재측정해야하기에 lifecycle을 onMeasure()부터 순회하면서 뷰를 그립니다.

 

 


 

 

 

전체 샘플소스

class MyProgressBar : View {
    private val disposable = CompositeDisposable()
    var lineColor: Int = 0
    var maxValue: Int = 0
    var curValue: Int = 0

    constructor(context: Context) : super(context, null)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, 0) {
        lineColor = context.obtainStyledAttributes(attrs, R.styleable.ProgressBar)
            .getColor(R.styleable.ProgressBar_lineColor, Color.RED)
        maxValue = context.obtainStyledAttributes(attrs, R.styleable.ProgressBar)
            .getColor(R.styleable.ProgressBar_maxValue, 100)
        curValue = context.obtainStyledAttributes(attrs, R.styleable.ProgressBar)
            .getColor(R.styleable.ProgressBar_curValue, 0)
    }

    constructor(
        context: Context,
        attrs: AttributeSet?,
        defStyleAttr: Int
    ) : super(context, attrs, defStyleAttr)


    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        Log.d("bsjo","bsjo onAttachedToWindow")
    }


    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        Log.d("bsjo","bsjo onMeasure")

        //2
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)

        //3
        val width = when (widthMode) {
            MeasureSpec.EXACTLY -> widthSize
            MeasureSpec.AT_MOST -> (paddingLeft + paddingRight + 200)
            else -> widthMeasureSpec
        }

        val height = when (heightMode) {
            MeasureSpec.EXACTLY -> heightSize
            MeasureSpec.AT_MOST -> (paddingTop + paddingBottom + 200)
            else -> heightMeasureSpec
        }

        setMeasuredDimension(width, height)
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        Log.d("bsjo","bsjo onLayout")

    }

    override fun dispatchDraw(canvas: Canvas?) {
        super.dispatchDraw(canvas)
        Log.d("bsjo","bsjo dispatchDraw")
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        Log.d("bsjo","bsjo onDraw")

        val width = measuredWidth + 0.0f
        val height = measuredHeight + 0.0f

        val circle = Paint()
        circle.color = this.lineColor
        circle.strokeWidth = 10f
        circle.isAntiAlias = false
        circle.style = Paint.Style.STROKE

        canvas?.drawArc(
            RectF(
                10f, 10f, width - 10f, height - 10f
            ), -90f,
            (this.curValue + 0.0f) / (this.maxValue + 0.0f) * 360, false, circle
        )

        val textp = Paint()
        textp.color = Color.BLACK
        textp.textSize = 30f
        textp.textAlign = Paint.Align.CENTER


        if (System.currentTimeMillis() / 1000 % 2 == 0L) {
            canvas?.drawText(
                "${this.curValue} / ${this.maxValue}",
                (width / 2),
                (height / 2),
                textp
            )
        }

        Observable.interval(1, TimeUnit.SECONDS)
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                invalidate()
            }, {

            })
            .addTo(disposable)
    }


    override fun onDetachedFromWindow() {
        disposable.clear()
        Log.d("bsjo","bsjo onDetachedFromWindow")
        super.onDetachedFromWindow()
    }
}

layout

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <com.example.myplayground.customview.MyProgressBar
            android:id="@+id/progress_circular"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:curValue="0"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:lineColor="@color/colorPrimary"
            app:maxValue="100" />

</android.support.constraint.ConstraintLayout>

percentageUse

 

  Observable.interval(100, TimeUnit.MILLISECONDS)
            .filter { it <= 100 }
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
            	//0.1초마다 1씩증가
                progress_circular.curValue = it.toInt()
            }, {

            })
            .addTo(disposabe)

 

 

result

0~100까지 게이지가 늘어나며 텍스트가 변경됩니다.

 

 

 

 

 

콜백함수 로그

2019-10-02 13:01:40.603 23068-23068/com.example.myplayground D/bsjo: bsjo onAttachedToWindow
2019-10-02 13:01:40.606 23068-23068/com.example.myplayground D/bsjo: bsjo onMeasure
2019-10-02 13:01:40.635 23068-23068/com.example.myplayground D/bsjo: bsjo onMeasure
2019-10-02 13:01:40.638 23068-23068/com.example.myplayground D/bsjo: bsjo onLayout
2019-10-02 13:01:40.707 23068-23068/com.example.myplayground D/bsjo: bsjo onDraw
2019-10-02 13:01:40.709 23068-23068/com.example.myplayground D/bsjo: bsjo dispatchDraw
2019-10-02 13:01:41.716 23068-23068/com.example.myplayground D/bsjo: bsjo onDraw
2019-10-02 13:01:41.717 23068-23068/com.example.myplayground D/bsjo: bsjo dispatchDraw
2019-10-02 13:01:42.721 23068-23068/com.example.myplayground D/bsjo: bsjo onDraw
2019-10-02 13:01:42.722 23068-23068/com.example.myplayground D/bsjo: bsjo dispatchDraw
2019-10-02 13:01:42.738 23068-23068/com.example.myplayground D/bsjo: bsjo onDraw
2019-10-02 13:01:42.739 23068-23068/com.example.myplayground D/bsjo: bsjo dispatchDraw
2019-10-02 13:01:43.727 23068-23068/com.example.myplayground D/bsjo: bsjo onDraw
2019-10-02 13:01:43.727 23068-23068/com.example.myplayground D/bsjo: bsjo dispatchDraw
2019-10-02 13:01:43.745 23068-23068/com.example.myplayground D/bsjo: bsjo onDraw
2019-10-02 13:01:43.746 23068-23068/com.example.myplayground D/bsjo: bsjo dispatchDraw
2019-10-02 13:01:43.760 23068-23068/com.example.myplayground D/bsjo: bsjo onDraw

....

2019-10-02 13:05:53.493 23068-23068/com.example.myplayground D/bsjo: bsjo onDetachedFromWindow

 

 

참고 

생성자 - https://medium.com/@futureofdev/%EC%BD%94%ED%8B%80%EB%A6%B0-kotlin-customview-774e236ca034

https://aroundck.tistory.com/234