需求
名字是不定长的,后面显示电话号码
当宽度足够时,需要靠左排列,且整体宽度不能为充满父布局。
当宽度不足够时挤压名字,优先保证电话显示是完整的。
看了下现有的一些方案:
控制 LinearLayout 优先显示右边的布局,空间不足时挤压左边控件 CSDN博客
空间够时内容整体从左至右自适应排列,当空间不够时,右侧内容宽度固定,左侧内容显示省略号 CSDN博客
布局上仍然会有一些兼容问题,比如margin之类的控制,而且有些只支持TextView。
原理
LinearLayout原本是优先测量左边的,然后再测量右边的,当空间不足时,右边的View就被挤压了。 若重写LinearLayout的onMeasure,使其顺序反过来,那么就解决了。
这里的左和右是由子view的index来决定的, 因此我们可以copy一份LinearLayout源码,然后把measureVertical和measureHorizontal里面的
for (int i = 0;i < count; ++i){
...依次测量各个view
}
改成逆序的
for (int i = count;i >= 0;--i){
...依次测量各个view
}
就能实现优先测量右边的view。
当然copy一份源码比较繁琐,后续维护也不知道在源码里改了啥,在此我们可以拦截getChildAt来实现。
在此实现的效果为可以选定一个view,空间不足时挤压这个view,而给其他view留出空间。
当然也可以更灵活就是重新安排所有view的测量顺序,但是很少遇到,在此不做实现。
也就是选定一个view是最后测量的,其他view按原本顺序先测量 比如原本的顺序是: view1 -> view2 -> view3
也就是:
getChildAt(0)=view1
getChildAt(1)=view2
getChildAt(2)=view3
若view1是可挤压的,我们需要跳转其测量顺序变为view2 -> view3 -> view1 即可
我们拦截后变成:
getChildAt(0)=view2
getChildAt(1)=view3
getChildAt(2)=view1
拦截的核心实现如下:
@Override
public View getChildAt(int index) {
if (finalMeasureIndex < 0) {
//非测量期间
return super.getChildAt(index);
}
if (index < finalMeasureIndex) {
//可以先测量的部分
return super.getChildAt(index);
}
if (index == getChildCount() - 1) {
//最后测量的View
return super.getChildAt(finalMeasureIndex);
} else {
//跳过最后测量的View
return super.getChildAt(index + 1);
}
}
我们只需要在测量期间拦截,绘制等其他情况是不需要拦截的,所以我们添加一个标记来表示需要拦截的情况
在super.onMeasure 开始前 设置finalMeasureIndex为需要拦截的view下标 表示需要拦截
在super.onMeasure 结束后 设置finalMeasureIndex为FINAL_MEASURE_INDEX_INVALID 表示不需要拦截。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
finalMeasureIndex = FINAL_MEASURE_INDEX_INVALID;
for (int i = 0; i < count; i++) {
if (isFinalMeasure(getChildAt(i))) {
finalMeasureIndex = i;
break;
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
finalMeasureIndex = FINAL_MEASURE_INDEX_INVALID;
}
最后通过LayoutParams标记一下需要最后测量的view就完成了
源码
public class FinalMeasureLinearlayout extends LinearLayoutCompat {
public FinalMeasureLinearlayout(Context context) {
this(context, null);
}
public FinalMeasureLinearlayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
private int finalMeasureIndex = FINAL_MEASURE_INDEX_INVALID;
private static final int FINAL_MEASURE_INDEX_INVALID = -1;
@Override
public View getChildAt(int index) {
if (finalMeasureIndex < 0) {
//非测量期间
return super.getChildAt(index);
}
if (index < finalMeasureIndex) {
//可以先测量的部分
return super.getChildAt(index);
}
if (index == getChildCount() - 1) {
//最后测量的View
return super.getChildAt(finalMeasureIndex);
} else {
//跳过最后测量的View
return super.getChildAt(index + 1);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
finalMeasureIndex = FINAL_MEASURE_INDEX_INVALID;
for (int i = 0; i < count; i++) {
if (isFinalMeasure(getChildAt(i))) {
finalMeasureIndex = i;
break;
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
finalMeasureIndex = FINAL_MEASURE_INDEX_INVALID;
}
/**
* 根据
*/
private boolean isFinalMeasure(View view) {
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams instanceof LayoutParams) {
return ((LayoutParams) layoutParams).finalMeasure;
}
return false;
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
if (getOrientation() == HORIZONTAL) {
return new LayoutParams(LinearLayoutCompat.LayoutParams.WRAP_CONTENT, LinearLayoutCompat.LayoutParams.WRAP_CONTENT);
} else if (getOrientation() == VERTICAL) {
return new LayoutParams(LinearLayoutCompat.LayoutParams.MATCH_PARENT, LinearLayoutCompat.LayoutParams.WRAP_CONTENT);
}
return null;
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
public static class LayoutParams extends LinearLayoutCompat.LayoutParams {
public boolean finalMeasure;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.FinalMeasureLinearlayout_Layout);
finalMeasure = typedArray.getBoolean(R.styleable.FinalMeasureLinearlayout_Layout_layout_final_measure, false);
typedArray.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(int width, int height, float weight) {
super(width, height, weight);
}
public LayoutParams(ViewGroup.LayoutParams p) {
super(p);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
}
}
attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FinalMeasureLinearlayout_Layout">
<attr name="layout_final_measure" format="boolean" />
</declare-styleable>
</resources>
使用
只需将可挤压的view 设置
app:layout_final_measure=true
总结
通过控制View 的测量顺序实现了让LinearLayout 空间不足时挤压指定View的需求。
通过拦截getChildAt 来实现对测量顺序的控制,最大程度的复用了原有的测量逻辑,因此兼容性较好。