PS:未来がどのようになるか予測できないのであれば、今をしっかりと生きることが大切です。
今日は、カスタム View で遭遇した問題を共有します。分析に誤りがあれば、ぜひ指摘してください。カスタム View のプロセスでは、必ず一つの問題に直面します。カスタム View には問題がないのですが、カスタム View 内で match_parent と wrap_content の効果が一致するという問題です。onMeasure () メソッドは以下の通りです:
/**
*
* Viewの幅と高さを測定します。このメソッドはmeasureメソッドによって呼び出され、一般的にはサブクラスがこのメソッドをオーバーライドして、より正確かつ効率的な測定を提供します。
*
* 規定:このメソッドをオーバーライドする場合、必ずsetMeasuredDimension(int, int)メソッドを呼び出して、Viewの測定幅と高さを保存する必要があります。
* 測定に失敗した場合はIllegalStateExceptionをスローします。
*
* 測定の基底クラス実装はデフォルトで背景サイズとなります。より大きなサイズの測定規範が許可されない限り、サブクラスはこのメソッドをオーバーライドして、より良い測定を提供する必要があります。
*
* このメソッドがオーバーライドされた場合、サブクラスの責任は、測定された高さと幅が少なくともViewの最小幅と高さであることを保証することです。
*
* @param widthMeasureSpec 親コンテナが適用する水平方向の測定規範。
* @param heightMeasureSpec 親コンテナが適用する垂直方向の測定規範。
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//測定された幅と高さを保存
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
getDefaultSize メソッドの具体的な実装を見てみましょう:
//指定された測定モードに基づいて対応するサイズを取得
public static int getDefaultSize(int size, int measureSpec) {
//デフォルトサイズ、そのサイズは背景が設定されているかどうかに関連します
int result = size;
//Viewの測定規範から対応する測定モードと測定サイズを取得
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//Viewの測定モードに基づいて現在のViewのサイズを設定
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
//Viewのサイズを返す
return result;
}
明らかに、デフォルトでは View が指定された幅と高さが match_parent と wrap_content の場合、つまり測定モードが AT_MOST と EXACTLY の場合、最終的に返されるのは指定された MeasureSpec から取得されたサイズです。したがって、デフォルトでは View の幅と高さを match_parent と wrap_content に設定すると、効果は同じです。
onMeasure 内の幅と高さの MeasureSpec は、親 View の MeasureSpec と自身のレイアウトパラメータ LayoutParams によって決定され、具体的には ViewGroup 内の getChildMeasureSpec () メソッドに反映されます。ソースコードは以下の通りです:
/**
* 子Viewの測定の難点:MeasureSpecを指定された子Viewに渡すことを見つける。このメソッドは、子Viewの幅または高さの測定に使用する正しいMeasureSpecを見つけます。
*
* このメソッドの目標は、MeasureSpecと子ViewのLayoutParamsを組み合わせて最も正確な結果を得ることです。
* 例えば、親ViewのMeasureSpecで測定モードがEXACTLYに指定され、子Viewが確定したサイズを指定した場合、親Viewは子Viewに確定したサイズを与えます。
*
* @param spec 親Viewが指定した幅または高さのMeasureSpec
* @param padding 現在のViewが外部マージンと内部パディングを設定している場合、現在のViewの内部パディングと外部マージンを示します。
* 例えば、mPaddingLeft + mPaddingRight + mLeftMargin + mRightMargin
* @param childDimension 現在のViewの幅または高さ
* @return 子View測定後の対応するサイズ
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//現在の測定次元の親Viewの測定モードと測定サイズを取得
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//子Viewが実際に使用できるサイズを取得
int size = Math.max(0, specSize - padding);
//子Viewの測定モードとサイズ、まだ測定が完了していない
int resultSize = 0;
int resultMode = 0;
//親Viewが指定した異なる測定モードに基づいて子Viewを測定
switch (specMode) {
//EXACTLY:親Viewが確定したサイズを指定する、例えばmatch_parent、100dp
case MeasureSpec.EXACTLY:
//子Viewが確定したサイズを指定した場合
if (childDimension >= 0) {
//指定されたサイズは子View自身が設定したサイズ
resultSize = childDimension;
//子Viewの測定モードをEXACTLYに指定
resultMode = MeasureSpec.EXACTLY;
//子ViewがサイズをMATCH_PARENTに設定した場合
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
//子Viewのサイズを子Viewが使用できる最大サイズに指定
resultSize = size;
//子Viewの測定モードをEXACTLYに指定
resultMode = MeasureSpec.EXACTLY;
//子ViewがサイズをWRAP_CONTENTに設定した場合
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
//子Viewのサイズは自身が包む内容によって決定されるが、親Viewが指定したサイズを超えてはいけない
resultSize = size;
//子Viewの測定モードをEXACTLYに指定
resultMode = MeasureSpec.AT_MOST;
}
break;
//AT_MOST:親Viewが子Viewに達成可能な最大サイズを許可する、例えばwrap_content
case MeasureSpec.AT_MOST:
//子Viewが確定したサイズを指定した場合
if (childDimension >= 0) {
//子Viewの測定モードと測定サイズを指定、ここで子Viewの測定モードのサイズを確定できる
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
//子Viewは親Viewが指定した最大サイズを取得したいが、親Viewのサイズは確定できないため、子Viewのサイズは自身が包む内容によって決定される
//したがって、測定モードはMeasureSpec.AT_MOSTに指定される
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
//子Viewは自身のサイズを決定したい。自身より大きくなることはできない。
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//UNSPECIFIED:親Viewは子Viewに制約をかけず、子Viewは希望するサイズを設定できる
//親Viewが指定した測定モードがUNSPECIFIEDの場合、子View自身のサイズが確定していない場合、
//子Viewのサイズは2つの範囲を持つ:0または子Viewが許可される最大サイズ
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//子Viewは特定のサイズを希望している...それを許可する
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
//子Viewは自身のサイズを希望している...どれくらい大きくすべきかを見つける
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
//子Viewは自身のサイズを決定したい....どれくらい大きくすべきかを見つける
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//子Viewに対応するMeasureSpecを返す
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上記は getChildMeasureSpec () のソースコードの分析に過ぎません。問題自体に戻ると、なぜカスタム View ではデフォルトで幅と高さを match_parent と wrap_content に設定すると効果が同じになるのか、以下の表は上記のコード分析に基づいてまとめたものです:
親 View の MeasureSpec | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
具体サイズ | childDimension(EXACTLY) | childDimension(EXACTLY) | childDimension(EXACTLY) |
match_parent | size(EXACTLY) | size(AT_MOST) | size(UNSPECIFIED) |
wrap_content | size(AT_MOST) | size(AT_MOST) | size または 0 (UNSPECIFIED) |
上記の表の内容はすべて getChildMeasureSpec () メソッドの内部実装に基づいており、デフォルト状態では子 View の幅と高さが match_parent または wrap_content に設定されると、その子 View の実際のサイズは size です。size は子 View が実際に使用できるサイズを示し、内側のパディングと外側のマージンを除いた子 View が使用できるサイズです。カスタム View を作成する際に座標を計算する際に使用されるのはこのサイズです。最終的に子 View の MeasureSpec が異なる可能性がありますが、特定の MeasureSpec から解析された size は同じです。したがって、カスタム View ではデフォルトで幅と高さを match_parent と wrap_content に設定すると効果が同じになる理由がわかりました。
この問題を解決するには、もちろんカスタム View の幅と高さを取得する前に適切な幅と高さのサイズを再保存する必要があります。このサイズは具体的な要件に基づいてデフォルトのサイズを設定できます。つまり、カスタム View の LayoutParams が wrap_content に設定されている場合、デフォルトのサイズを設定する必要があります。例えば、以下のように処理します:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//wrap_contentのデフォルト幅と高さ
Rect mRect = new Rect();
mLetterPaint.getTextBounds("A", 0, 1, mRect);
int mDefaultWidth = mRect.width() + dpToPx(mContext, 12);
int mDefaultHeight = mRect.height() + dpToPx(mContext, 5);
//wrap_content時のデフォルト幅と高さを再設定
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT &&
getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
//適切な幅と高さを再保存
setMeasuredDimension(mDefaultWidth, mHeight);
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mDefaultWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(widthSize, mDefaultHeight);
}
//現在のViewのLayoutParamsがwrap_contentの場合、取得した幅と高さは対応するデフォルト幅と高さです
int mWidth = getMeasuredWidth();
int mHeight = getMeasuredHeight();
}
上記はカスタム View で日付選択器を実装するという記事の関連コードです。具体的にはクリックして確認できます。