在TitleBar类中,对于View的加载有两种方式,一种是通过自定义属性的方式在布局中添加view,一种是动态的增加view。
现在对RightView分别用这两种方法进行测试,上图分别对应了这两种方式的最终显示结果。
当tittle过长时,应该是需要省略号代替多余字的,避免遮挡两侧view的显示,可以看到,由于添加view方式的不同,造成了不能接受的结果。
int rightCustomViewRes = typedArray.getResourceId(R.styleable.TitleBar_rightCustomView, 0);
public void setRightView(View view, OnClickListener listener){
rightView.removeAllViews();
rightView.addView(view, layoutParams);
setOnRightClickListener(listener);
}
通过自定义属性的方式在布局中添加view的代码执行顺序:
public TitleBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
…
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray typedArray = getTitleBarTypedArray(context, attrs);
initViewWithAttrs(typedArray);
typedArray.recycle();
}
private void initViewWithAttrs(TypedArray typedArray) {
…
}
#initViewWithAttrs
设置左侧 icon、设置右侧 icon、设置右侧自定义 View、设置 Title
下面是关键,他通过测量leftview和rightView的宽度,来得到middleView的最大宽度和title的最大宽度
int widthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
leftView.measure(widthSpec, heightSpec);
int leftWidth = leftView.getMeasuredWidth();
rightView.measure(widthSpec, heightSpec);
int rightWidth = rightView.getMeasuredWidth();
int middleMaxWidth = getResources().getDisplayMetrics().widthPixels - (Math.max(leftWidth, rightWidth) << 1);
// middleview 最大宽度
((LayoutParams) middleView.getLayoutParams()).width = middleMaxWidth;
textView.setGravity(Gravity.CENTER);
// 设置 textview 截断
textView.setMaxWidth(middleMaxWidth);
textView.setEllipsize(TextUtils.TruncateAt.END);
通过log看执行:
假设通过动态的方式来加载RightView
结果明了,middleView的最大宽度和title的最大宽度在initViewWithAttrs方法中就已经进行了设置,之后再通过setRight方法只是单纯的进行View的add。
现在回头再看下面几行代码的意图
int widthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
rightView.measure(widthSpec, heightSpec);
int rightWidth = rightView.getMeasuredWidth();
int widthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
这个方法获取的宽度是minWidth参数设置的大小和background指定背景宽度,这两个宽度的最大值,高也是如此,也就是说如果View的xml中没有两个参数中的其中一项,那么这个方法测量的宽高也是为0的,这个方法测量的并不是获取xml中设置的android:layout_height android:layout_width的值。看图:
rightView.measure(widthSpec, heightSpec) -->View的measure方法–>onMeasure()方法。
#onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
如果背景为空,那么就取mMinHeight的值,如果背景不为空就取max(mMinHeight,mBackground.getMinimumHeight())背景高度和mMinHeight最大值。
#getMinimumWidth
mMinWidth· = android:minWidth属性所指定的值;若android:minWidth没指定,则默认为0。
由源码可知:mBackground.getMinimumWidth()的大小 = 背景图Drawable的原始宽度,若无原始宽度,则为0;
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
//返回背景图Drawable的原始宽度
return intrinsicWidth > 0 ? intrinsicWidth :0 ;
}
// 注:BitmapDrawable有原始宽度,而ShapeDrawable没有
#getDefaultSize
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
由于我们使用的是UNSPECIFIED,result = size,size来自getSuggestedMinimumWidth(),否则result=0。
#setMeasuredDimension
这里主要是保存测量值
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
最后是int rightWidth = rightView.getMeasuredWidth();
获取的就是最终测量值。
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
解决:
参考android如何获取view在布局中的高度与宽度详解
Runnable 对象中的方法会在 View 的 measure、layout 等事件完成后触发。
UI 事件队列会按顺序处理事件,在 setContentView() 被调用后,事件队列中会包含一个要求重新 layout 的 message,所以任何 post 到队列中的 Runnable 对象都会在 Layout 发生变化后执行。
@Override
public void setRightView(View view, OnClickListener listener) {
rightView.removeAllViews();
if (view != null) {
rightView.addView(view, layoutParams);
}
setOnRightClickListener(listener);
rightView.post(new Runnable() {
@Override
public void run() {
int middleMaxWidth =
getResources().getDisplayMetrics().widthPixels - (rightView.getMeasuredWidth() << 1);
middleView.getLayoutParams().width = middleMaxWidth;
// 设置 textview 截断
textView.setMaxWidth(middleMaxWidth);
textView.setEllipsize(TextUtils.TruncateAt.END);
}
});
}
补充:
getMeasuredWidth与getWidth的区别
在measure方法结束后getMeasuredWidth方法就会有值
getWidth方法是在layout方法完成后才有的值