[컴][안드로이드] Android 의 View 에서 onMeasure 와 onLayout 의 의미

android view 에서 onMeasure / onLayout 작성하는 방법 , onMeasure , onLayout, how to write / custome layout 에서 onMeasure 와 onLayout 에 어떤 코드를 넣어야 하는가.

 

화면 구성하는 2가지의 pass 를 수행하게 된다. 순서는 아래와 같다.
  1. Measure pass
  2. Layout Pass
Measure 는 size(크기) 와 관련된 pass 이고, layout 은 좌표와 관련된 pass 라고 생각하면 된다. 그렇기 때문에 size 를 알아야 사용할 수 있는 좌표의 범위를 정할 수 있기 때문에 어쩌면 size 가 정해지고 나서 좌표를 정하는 것이 당연한 것일지 모른다.

measure vs onMeasure()

measure -> onMeasure()
layout -> onLayout()
View.java
measure 나 layout 이나 호출을 하는 모양새는 비슷하다. 모두 measure/layout 을 호출하고 그 내부에서 다시 onMeasure()/onLayout() 을 호출하고 있다.

measure() 내부에서 onMeasure() 를 호출한다. 이것을 design 관점에서 본다면, measure 는 필수적으로 수행돼야 하는 부분들을 모아놓은 것이고, 그 중에 override 를 해야 하거나, 할 수 있는 것들을 onMeasure() 로 분리해 놓은 듯이 보인다.

 

onMeasure()

View 는 layout 안에서 너비(width) 나 높이(height) 가 각 View 마다 자신의 값을 가질 수 있다. 그래서 View 는 자신의 width 와 height 의 값을 가지고 있다. 이것이 member variable 인 mMeasuredWidth / mMeasuredHeight 이다.

그래서, 기본적으로 이 mMeasuredWidth / mMeasuredHeight 이 set 돼야 한다.
View 는 그래서
View.measure() >> View.onMeasure() >> View.setMeasuredDimesion()
를 호출해서 이 작업을 한다.

그런데 우리가 customView 를 만들 때는 이 onMeasure() 를 override 하게 된다. 그러면 setMeausredDimesion() 이 호출이 안되기 때문에 우리가 만든 customView 의 onMeasure() 에서 setMeasuredDimension() 을 호출해 줘서 mMeasuredWidth / mMeasuredHeight 을 설정해야 한다.

그러면 onMeasure() 에서 할 일은 onMeasure() 를 가지고 있는 class 의 width 와 height 를 설정 해 주는 일을 한다고 보면 된다. View 를 extends 했다면 view 의 width 와 height 를 설정해 주는 작업을 onMeasure() 에 작성하면 되고, layout 을 extends 했다면, layout 의 width와 height 를 작성하는 코드를 만들면 된다.

간단히 얘기하면, size 계산(measure)해서 set 까지 해 주는 것이다.


onLayout()

FrameLayout 를 보자. FrameLayout 는 ViewGroup 이다.
ViewGroup.layout() >> View.layout() >> View.onLayout()
gravity 가 있다면 gravity 에 따라서 위치가 달라질 것이다. 그렇기 때문에 gravity 설정과 관련된 계산도 onLayout 에서 해준다.

그리고 ViewGroup() 인 경우에는 그 안에 여러 child View 들을 가지고 있다. 이 child view 들은 또 ViewGroup() 일 수도, 그냥 View 일 수도 있다. 그렇기 때문에 childView.layout() 을 호출해서 자신의 child 에게 layout 을 하라고 한다.

ViewGroup 은 자신이 가지고 있는 child View 들이 어떤 식으로 배치(layout) 되는 지를 결정해야 한다. 이런 결정은 결국 onLayout() 에서 child View 들의 구역(region) 을 정해주는 것으로 가능하다.

그러므로 현재 View 의 onLayout() 에서는 child 에게 그려지는 범위까지만 알려주면 된다. 그러면 child 는 그 범위를 자신의 범위로 인식하고 그 범위에 따른 상대적인 좌표를 계산하게 된다.
ViewGroup.layout() >> View.layout() >> View.onLayout() : empty >> ViewGroup.onLayout() : empty >> inherited class' onLayout()
View.layout() 에서 setFrame() 함수를 통해 View의 left, top, right, bottom 을 set 한다. 각 View 에서 이렇게 layout() 을 호출하기 때문에 각 View 가 setFrame() 을 호출한다고 보면 된다.
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;

Sample code[ref. 2]

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
        } else if (heightMode != MeasureSpec.EXACTLY) {
            throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
        }

        int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
        int panelHeight = mPanelHeight;

        final int childCount = getChildCount();

        if (childCount > 2) {
            Log.e(TAG, "onMeasure: More than two child views are not supported.");
        } else if (getChildAt(1).getVisibility() == GONE) {
            panelHeight = 0;
        }

        // We'll find the current one below.
        mSlideableView = null;
        mCanSlide = false;

  /**
   namh
   
   <core>
   for (int i = 0; i < childCount; i++)
    child.measure(childWidthSpec, childHeightSpec);
   </core>
   */
        // First pass. Measure based on child LayoutParams width/height.
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            int height = layoutHeight;
            if (child.getVisibility() == GONE) {
                lp.dimWhenOffset = false;
                continue;
            }

            if (i == 1) {
                lp.slideable = true;
                lp.dimWhenOffset = true;
                mSlideableView = child;
                mCanSlide = true;
            } else {
                height -= panelHeight;
            }

            int childWidthSpec;
            if (lp.width == LayoutParams.WRAP_CONTENT) {
                childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
            } else if (lp.width == LayoutParams.MATCH_PARENT) {
                childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
            } else {
                childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
            }

            int childHeightSpec;
            if (lp.height == LayoutParams.WRAP_CONTENT) {
                childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
            } else if (lp.height == LayoutParams.MATCH_PARENT) {
                childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
            } else {
                childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
            }

            child.measure(childWidthSpec, childHeightSpec);
        }

        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int paddingLeft = getPaddingLeft();
        final int paddingTop = getPaddingTop();

        final int childCount = getChildCount();
        int yStart = paddingTop;
        int nextYStart = yStart;

        if (mFirstLayout) {
            mSlideOffset = mCanSlide && mPreservedExpandedState ? 0.f : 1.f;
        }

        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);

            if (child.getVisibility() == GONE) {
                continue;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            int childHeight = child.getMeasuredHeight();

            if (lp.slideable) {
                mSlideRange = childHeight - mPanelHeight;
                yStart += (int) (mSlideRange * mSlideOffset);
            } else {
                yStart = nextYStart;
            }

            final int childTop = yStart;
            final int childBottom = childTop + childHeight;
            final int childLeft = paddingLeft;
            final int childRight = childLeft + child.getMeasuredWidth();
            child.layout(childLeft, childTop, childRight, childBottom);

            nextYStart += child.getHeight();
        }

        if (mFirstLayout) {
            updateObscuredViewVisibility();
        }

        mFirstLayout = false;
    }



References

  1. CustomLayout, 33:24
  2. SlidingUpPanelLayout.java

댓글 2개:

  1. 머리속에 정리도 안되고 많이 헷갈렸던 내용인데 잘 정리된 글이네요. 정말 감사드립니다.

    답글삭제
  2. 분명 좋은 글입니다.

    답글삭제