Android Layout/View의 LifeCycle(Measure, Layout, Draw)중에 첫 번째 onMeasure와 주요 파라미터인 MeasureSpec에 대해서 알아보려고 합니다.
생성자, Attach/Detach과정에 대한 설명은 생략
/**
* @param widthMeasureSpec 부모에 의해 부과 된 수평 공간 요구 사항
* @param heightMeasureSpec 부모에 의해 부과 된 수직 공간 요구 사항
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
@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은 크기와 모드로 구성됩니다. 세 가지 가능한 모드가 있습니다.
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의 크기를 가질 수 있습니다.
참고: 화면 윈도우의 가로 크기는 최대 1080
측정에 사용되는 View는 오직 TextView만을 사용합니다. 나머지 View는 관계를 형성하기 위한 부속입니다.
<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의 크기를 결정할 수 있습니다.
<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의 속성대로 정확히 지정된 크기를 전달됩니다.
<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로 전달됩니다.
<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)로 지정하여 어떻게 크기가 결정되어 전달되는지 봤습니다.
<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번 수행하게 됩니다.
<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은 위와같이 다수의 측정의 과정을 수행합니다. 이 부분은 렌더링 성능과 밀접한 관련이 되므로 위와같은 형태의 구성은 지양하도록 합니다.
<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의 가로폭이 변경되면 내부에 표시되는 텍스트의 길이 따라 줄내림이 발생하고 줄내림 여부에 따라 세로높이가 변경됩니다. 위가 이에 해당하는 경우로 세로의 크기도 같이 갱신됩니다.
<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을 위한 지식으로 사용될 수 있습니다.