onMeasure(), MeasureSpec

김해중·2021년 1월 4일
0

graphic

목록 보기
2/2
post-thumbnail

Android Layout/View의 LifeCycle(Measure, Layout, Draw)중에 첫 번째 onMeasure와 주요 파라미터인 MeasureSpec에 대해서 알아보려고 합니다.

생성자, Attach/Detach과정에 대한 설명은 생략

View LifeCycle

  • measure(), onMeasure()
    • View가 얼마나 커야하는지 그려질 View의 크기를 측정하는 단계
      아직은 최종 크기를 결정하지 않은 상태
  • layout(), onLayout()
    • 부모 Group에서 View의 최종 배치 이후의 위치와 크기를 전달 하는 단계
  • draw(), onDraw()
    • 크기와 위치는 앞 단계에서 결정되어 있기 때문에 Canvas에 View를 그리는 단계

measure(), onMeasure()

  • measure - 최초 측정을 위해 상위 Group, Parent에 의해서 호출 되는 함수로 내부에서 onMeasure를 호출합니다.
  • onMeasure - 주어진 MeasureSpec을 이용하여 View 자기 자신의 크기를 계산합니다.

View/ViewGroup에서의 Measure

  • TextView와 같이 간단한 구조의 단일 View에서의 measure()는 MeasureSpec을 보정하는 작업을 수행 후 onMesaure(MeasureSpec, MeasureSpec)호출하여 실제 필요로 하는 크기를 계산하도록 합니다.
  • LinearLayout과 같이 ViewGroup에서는 하나 또는 그 이상의 자식 View를 가질 수 있습니다. 따라서 measure()에서는 MeasureSpec을 보정하고 소유하고 있는 자식 View를 순회(Traversals)하여 자식 View를 포함한 자기 자신 View의 크기를 계산합니다.

View의 크기 측정

View::onMeasure()

/**
 * @param widthMeasureSpec 부모에 의해 부과 된 수평 공간 요구 사항
 * @param heightMeasureSpec 부모에 의해 부과 된 수직 공간 요구 사항
 */
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

TextView::onMeasure()

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

    int width;
    int height;

    ...줄임

    int des = -1;
    boolean fromexisting = false;
    final float widthLimit = (widthMode == MeasureSpec.AT_MOST)
            ?  (float) widthSize : Float.MAX_VALUE;

    if (widthMode == MeasureSpec.EXACTLY) {
        // Parent has told us how big to be. So be it.
        width = widthSize;
    } else {
        
        ...줄임

        if (boring == null || boring == UNKNOWN_BORING) {
            if (des < 0) {
                des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
                        mTransformed.length(), mTextPaint, mTextDir, widthLimit));
            }
            width = des;
        } else {
            width = boring.width;
        }

        ...줄임

        width += getCompoundPaddingLeft() + getCompoundPaddingRight();

        if (mMaxWidthMode == EMS) {
            width = Math.min(width, mMaxWidth * getLineHeight());
        } else {
            width = Math.min(width, mMaxWidth);
        }

        if (mMinWidthMode == EMS) {
            width = Math.max(width, mMinWidth * getLineHeight());
        } else {
            width = Math.max(width, mMinWidth);
        }

        // Check against our minimum width
        width = Math.max(width, getSuggestedMinimumWidth());

        if (widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(widthSize, width);
        }
    }

    int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
    int unpaddedWidth = want;

    if (mHorizontallyScrolling) want = VERY_WIDE;

    int hintWant = want;
    int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();

    if (mLayout == null) {
        makeNewLayout(want, hintWant, boring, hintBoring,
                        width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
    } else {
        ...줄임
    }

    if (heightMode == MeasureSpec.EXACTLY) {
        // Parent has told us how big to be. So be it.
        height = heightSize;
        mDesiredHeightAtMeasure = -1;
    } else {
        int desired = getDesiredHeight();

        height = desired;
        mDesiredHeightAtMeasure = desired;

        if (heightMode == MeasureSpec.AT_MOST) {
            height = Math.min(desired, heightSize);
        }
    }

    ...줄임

    setMeasuredDimension(width, height);
}

onMeasure는 반환 형이 Void로 측정 된 결과를 직접 반환하지 않습니다. 대신 setMeasuredDimension()을 호출 함으로써 측정 된 크기(가로,세로)를 명시적으로 설정합니다.

MeasureSpec

MeasureSpec은 부모에서 자식으로 전달되는 레이아웃 요구 사항을 캡슐화합니다. 각 MeasureSpec은 너비 또는 높이에 대한 요구 사항을 나타냅니다. MeasureSpec은 크기와 모드로 구성됩니다. 세 가지 가능한 모드가 있습니다.

  • MeasureSpec.EXACTLY : 부모가 자식의 정확한 크기를 결정했습니다. 자식 View가 얼마나 큰지 관계없이 지정된 크기(경계)가 주어집니다.
  • MeasureSpec.AT_MOST : 자식은 지정된 크기까지 원하는만큼 커질 수 있습니다.
  • MeasureSpec.UNSPECIFIED : 부모가 자식에게 어떤 제약도 부과하지 않았습니다. 원하는 크기가 될 수 있습니다.
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;
}

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}

public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}

MeasureSpec은 Int의 타입으로 전달됩니다. MeasureSpec은 Int형 값에 상위 2비트는 Mode를 하위 30비트는 크기를 지정합니다. 크기는 양수로 지정되며 0부터 1073741823의 크기를 가질 수 있습니다.

Layout 관계에 따른 MeasureSpec

참고: 화면 윈도우의 가로 크기는 최대 1080
측정에 사용되는 View는 오직 TextView만을 사용합니다. 나머지 View는 관계를 형성하기 위한 부속입니다.

Case 1

<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent">
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</FrameLayout>

TextView의 onMeasure() 호출 결과
onMeasure() -> MeasureSpec: AT_MOST 1080, MeasureSpec: AT_MOST 1823

  • wrap_conent는 AT_MODE 모드로 전달되며 함께 전달 된 크기(가로:1080)내에서 View의 크기를 결정할 수 있습니다.

Case 2

<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent">
    <TextView android:layout_width="match_parent" android:layout_height="wrap_content"/>
</FrameLayout>

TextView의 onMeasure() 호출 결과
onMeasure() -> MeasureSpec: EXACTLY 1080, MeasureSpec: AT_MOST 1823

  • match_parent는 EXACTLY 모드로 지정된 크기를 전달 받습니다.
  • 부모의 최대 가로 크기는 1080이며 부모와 동일한 크기를 원하는 match_parent의 속성대로 정확히 지정된 크기를 전달됩니다.

Case 3

<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent">
    <TextView android:layout_width="100dp" android:layout_height="wrap_content"/>
</FrameLayout>

TextView의 onMeasure() 호출 결과
onMeasure() -> MeasureSpec: EXACTLY 275, MeasureSpec: AT_MOST 1823

  • 100dp의 고정크기 지정은 EXACTLY 모드로 지정된 크기를 전달 받습니다.
  • DP로 지정된 크기는 디스플레이 스펙에 따라 DP to Pixel로 변경되어 Pixel의 크기인 275로 전달됩니다.

Case 4

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="12345678901234567890"
        app:layout_constraintEnd_toStartOf="@+id/button"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/button"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

TextView의 onMeasure() 호출 결과
onMeasure() -> MeasureSpec: EXACTLY 255, MeasureSpec: AT_MOST 1823

  • ConstraintsLayout에 의해서 제약된 크기를 지정받은경우 EXACTLY 모드로 지정된 크기를 전달 받습니다.
  • 제약을 위해서 Button에 강제로 크기를 지정하고 TextView크기는 Match Constraint(0dp)로 지정하여 어떻게 크기가 결정되어 전달되는지 봤습니다.

Case 5

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView            
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="12345678901234567890" />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="button3" />
    </LinearLayout>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button1" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button2" />
</LinearLayout>

TextView의 onMeasure() 호출 결과
onMeasure -> onMeasure MeasureSpec: AT_MOST 1080, MeasureSpec: AT_MOST 1823

  • 중첩된 LinearLayout에 TextView가 위치하더라도 layout_width가 wrap_content로 설정된 경우 측정은 1번 수행하게 됩니다.

Case 6

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">
        <TextView            
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:text="12345678901234567890" />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:text="button3" />
    </LinearLayout>
    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="button1" />
    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="button2" />
</LinearLayout>

TextView의 onMeasure() 호출 결과
onMeasure -> MeasureSpec: UNSPECIFIED 1080, MeasureSpec: UNSPECIFIED 1823
onMeasure -> MeasureSpec: UNSPECIFIED 1080, MeasureSpec: EXACTLY 92
onMeasure -> MeasureSpec: AT_MOST 360, MeasureSpec: AT_MOST 1823
onMeasure -> MeasureSpec: AT_MOST 360, MeasureSpec: EXACTLY 115

  • LinearLayout내에 weight를 사용한 경우 측정 횟수에서 차이를 보입니다.
  • LinearLayout을 사용하더라도 weight속성이 없는경우 1회 측정으로 View의 크기를 산출 할 수 있습니다.
  • LinearLayout내에 있는 View가 weight 속성을 사용하게 되면 LinearLayout은 weight에 맞는 적절한 크기를 계산하기 위해 UNSPECIFIED 모드로 순회를 한 후에 이를 바탕으로 MeasureSpec을 업데이트 하고 다시 View에게 크기 계산을 요청합니다.
  • 중첩되고 weight를 사용하는 LinearLayout은 위와같이 다수의 측정의 과정을 수행합니다. 이 부분은 렌더링 성능과 밀접한 관련이 되므로 위와같은 형태의 구성은 지양하도록 합니다.

Case 7

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView            
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="12345678901234567890" />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="button3" />
    </LinearLayout>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button1" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button2" />
</LinearLayout>

TextView의 onMeasure() 호출 결과
onMeasure -> MeasureSpec: AT_MOST 1080, MeasureSpec: AT_MOST 1823
onMeasure -> MeasureSpec: EXACTLY 288, MeasureSpec: EXACTLY 53

  • 중첩된 LinearLayout에 TextView가 위치하더라도 layout_width가 match_parent로 설정된 경우 LinearLayout내에 있는 다른 View의 크기를 이용하여 LinearLayout의 최종 크기를 구하고 MeasureSpec을 업데이트 하여 다시 TextView의 측정을 요청합니다.
  • TextView의 가로폭이 변경되면 내부에 표시되는 텍스트의 길이 따라 줄내림이 발생하고 줄내림 여부에 따라 세로높이가 변경됩니다. 위가 이에 해당하는 경우로 세로의 크기도 같이 갱신됩니다.

Case 8

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="12345678901234567890"
        app:layout_constraintBottom_toTopOf="@id/bt3"
        app:layout_constraintEnd_toStartOf="@id/bt1"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/bt3"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="button3"
        app:layout_constraintStart_toStartOf="@+id/textView"
        app:layout_constraintTop_toBottomOf="@+id/textView" />
    <Button
        android:id="@+id/bt1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="button1"
        app:layout_constraintEnd_toStartOf="@+id/bt2"
        app:layout_constraintStart_toEndOf="@+id/textView"
        app:layout_constraintTop_toTopOf="parent" />
    <Button
        android:id="@+id/bt2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="button2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/bt1"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

TextView의 onMeasure() 호출 결과
onMeasure -> MeasureSpec: AT_MOST 1080, MeasureSpec: AT_MOST 1823
onMeasure -> MeasureSpec: EXACTLY 360, MeasureSpec: AT_MOST 1823

  • Case 6과 같은 Layout구성을 ConstraintsLayout으로 했을경우 보다 적은 횟수의 측정을 수행합니다.

정리

View의 LifeCycle에 따라서 View가 화면에 그려지기 이전에 몇몇의 사전 과정이 필요합니다. 이 과정들에서 View의 실제 그려질 크기를 계산하는 과정은 전체의 Layout을 배치하는 과정에서 중요한 수치로 사용됩니다.

Measure, MeasureSpec을 이해함으로써 후에 Custom View를 만드는 과정에서 보다 정확한 Rendering을 위한 지식으로 사용될 수 있습니다.

profile
WT Android

0개의 댓글