Custom View onMeasure
Custom View 의 크기를 지정하기 위한 작업을 onMeasure 메서드에서 해야 합니다. onMeasure 메서드의 매개변수는 부모 View 에서 측정한 자식 View 의 widthMeasureSpec 과 heightMeasureSpec 입니다. MeasureSpec 은 Mode 와 Size 를 인코딩한 정수값입니다.
CustomView 의 layout_width 또는 layout_height 를 MATCH_PARENT 또는 특정 dp 로 설정하면 , 부모 View 에서 측정하여 전달한 MeasureSpec 의 Mode 가 EXACTLY 로 설정되어 onMeasure 에 전달되며, wrap_content 로 설정하면 AT_MOST 로 전달됩니다.
EXACTLY 로 전달된 경우 부모 View 에서 측정한 크기를 그대로 자식 View 의 크기로 사용하면 되고,
AT_MOST 로 전달된 경우에는 부모 View 에서 측정한 크기는 자식 View 가 가질 수 있는 최대 크기이므로 자식 View 의 onMeasure 메서드에서 View 내부 컨텐츠의 크기를 계산하여 부모 View 에서 전달해준 크기와 비교한 후 작은 값을 사용합니다.
테스트 layout xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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"
android:orientation="vertical"
tools:context=".MainActivity">
<com.test.customview.TallyCounterView
android:id="@+id/tally_counter_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingEnd="30dp"
android:paddingStart="30dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
CustomView onMeasure()
CustomView 의 layout_width 와 layout_height 를 모두 wrap_content 로 설정하였습니다.
테스트한 기기에서 desiredWidth 는 285, desiredHeight 는 101 로 계산되었으며
widthMeasureSpec 의 size 는 1080 , heightMeasureSpec 의 size 는 2168 로 계산되었습니다.
(테스트한 기기는 갤럭시 s20 FE 로 2400 x 1080 스크린을 사용하는 기기입니다. 프로젝트에서 액션바를 제거하였기 때문에 세로 길이의 경우 시스템 바(status bar, navigation bar) 크기를 제외한 화면의 크기가 아닐까 생각합니다.)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final Paint.FontMetrics fontMetrics = numberPaint.getFontMetrics();
//text 의 가로 길이를 가져옵니다.
final float maxTextWidth = numberPaint.measureText("10000");
//text 의 세로 길이를 가져옵니다.
final float maxTextHeight = -fontMetrics.top + fontMetrics.bottom;
//text 의 가로길이 + horizontal padding 을 하여 View 의 가로 길이를 계산합니다.
final int desiredWidth = Math.round(maxTextWidth + getPaddingLeft() + getPaddingRight());
//text 의 세로길이 * 2 + vertical padding 을 하여 View 의 세로 길이를 계산합니다.
final int desiredHeight = Math.round(maxTextHeight * 2f + getPaddingTop() + getPaddingBottom());
//계산한 길이를 사용할 지 부모 View 에서 측정한 길이를 사용할 지 결정한다.
//width 와 height 모두 AT_MOST 이므로 좀더 작은 크기인 desiredWidth 와 desiredHeight 가 선택됩니다.
final int measuredWidth = reconcileSize(desiredWidth, widthMeasureSpec);
final int measuredHeight = reconcileSize(desiredHeight, heightMeasureSpec);
//desiredWidth(285) 와 desiredHeight(101) 값을 Custom View 의 크기로 설정합니다.
setMeasuredDimension(measuredWidth, measuredHeight);
}
private int reconcileSize(int contentSize, int measureSpec) {
final int mode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
switch (mode) {
//match_parent 또는 dp 로 지정한 경우 부모 View 에서 측정한 길이를 사용한다.
case MeasureSpec.EXACTLY:
return specSize;
//wrap_content 로 지정한 경우 계산한 길이가 부모 View 에서 측정한 길이보다 작은 경우 계산한 길이를 사용한다.
//즉 더 작은 길이 값을 사용한다.
case MeasureSpec.AT_MOST:
if (contentSize < specSize) {
return contentSize;
} else {
return specSize;
}
case MeasureSpec.UNSPECIFIED:
default:
return contentSize;
}
}