java布局中有一個流式布局,但是android布局中并沒有。手機上用到流式布局大概就是熱門标簽的添加吧。流式布局就是控件一個一個的自動往右添加,如果超出寬度,則自動到下一行。
步驟分析
1.對于本布局,我們需要能得到margin屬性的LayoutParams,即MarginLayoutParams.
2.在onMeasure()方法中計算所有子view的高度和寬度,以便得到FlowLayout 的寬高(流式布局為warp_content模式)。
3.在onLayout()方法中放置所有子view的位置。
解決問題
1.得到MarginLayoutParams
隻需要我們重寫generateLayoutParams()方法
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)
{
return new MarginLayoutParams(getContext(), attrs);
}
2.onMeasure()計算寬高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//如果為warp_content模式下測量的寬高
int width = ;
int height = ;
//記錄每一行的寬高
int lineW = ;
int lineH = ;
int childCount = getChildCount();
for (int i = ; i < childCount; i++) {
View child = getChildAt(i);//擷取每一個子view
measureChild(child,widthMeasureSpec,heightMeasureSpec);//測量子view
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childW = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;//得到子view的寬高
int childH = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//如果放入該view是超過父布局寬度,需要換行,那麼高度累加,寬度取目前行與該子view最大的為父布局寬度
if (childW + lineW > widthSize - getPaddingLeft() - getPaddingRight()){
width = Math.max(lineW,childW);
height +=lineH;//高度累加
//開啟新行
lineW = childW;
lineH = childH;
}else {//如果不換行,則寬度累加,高度取最大值
lineW += childW;
lineH = Math.max(lineH,childH);
}
if (i == childCount -){//最後一個子view
width = Math.max(width, lineW);
height += lineH;
}
}
Log.i("FLOW",width+" "+height);
setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width +getPaddingLeft()+getPaddingRight(),
(heightMode == MeasureSpec.EXACTLY ? heightSize : height +getPaddingTop()+getPaddingBottom()));
}
首先得到父布局的測量模式和寬高,然後周遊所有的子view,得到子view的寬高,計算父布局wrap_content模式下的寬高,最後根據模式設定父布局的寬高。但是在測量時應注意一點,在周遊到最後一個子view時,可能會換行,會走換行的if語句,但是并沒有将在view的高度進行累加,是以要單獨寫一個判斷進行累加。
3.onLayout()為子view布局
List<List<View>> allViews = new ArrayList<>();
List<Integer> lineH = new ArrayList<>();
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
allViews.clear();
lineH.clear();
int width = getWidth();//父布局的寬度
int lineWidth = ;
int lineHeight = ;
//存放每一行的子view
List<View> lineViews = new ArrayList<>();
int childCount = getChildCount();
for (int i = ; i < childCount; i++) {
View child = getChildAt(i);//得到view執行個體
MarginLayoutParams lp = (MarginLayoutParams) child
.getLayoutParams();
//得到子view的寬高
int childWidth = child.getMeasuredWidth() + lp.leftMargin +lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.bottomMargin +lp.topMargin;
if (lineWidth + childWidth > (width -getPaddingLeft() - getPaddingRight())){//如果需要換行
lineH.add(lineHeight);//儲存這一行的view以及最大高度
allViews.add(lineViews);
//重置寬高
lineWidth = ;
lineHeight = ;
lineViews = new ArrayList<>();
}
//如果不換行,則行高等于最高的,行寬累加
lineWidth = lineWidth + childWidth;
lineHeight = Math.max(lineHeight,childHeight);
lineViews.add(child);
}
lineH.add(lineHeight);
allViews.add(lineViews);
int lineNums = allViews.size();
int left = getPaddingLeft();
int top = getPaddingTop();
for (int i =; i < lineNums; i++) {
lineViews = allViews.get(i);
lineHeight = lineH.get(i);
//周遊每一行的view
for (int j = ; j < lineViews.size(); j++) {
View child = lineViews.get(j);
if (child.getVisibility() == View.GONE){
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//計算子view的坐标
int lc = left +lp.leftMargin;
int tc = top +lp.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
child.layout(lc,tc,rc,bc);
left += child.getMeasuredWidth() + lp.rightMargin
+ lp.leftMargin;
}
//重置left和top 為下一行的計算坐準備
left = getPaddingLeft();
top +=lineHeight;
}
}
代碼分析:
allViews 存放所有的子view,lineH 存放每一行的最大高度,lineView 存放每一行的view。
然後周遊所有子view,設定每一行的高度,和每一行的子view,最後周遊每一行的子view。設定每一個view的left,top,right,bottom.
測試
我用幾個textView來測試,看一看效果
在res/values/styles.xml中:
<style name="text_flag_01">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_margin">dp</item>
<item name="android:background">@drawable/flag_01</item>
<item name="android:textColor">#ffffff</item>
</style>
frag_01.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#7690A5" >
</solid>
<corners android:radius="5dp"/>
<padding
android:bottom="2dp"
android:left="10dp"
android:right="10dp"
android:top="2dp" />
</shape>
item_flow.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/text_flag_01"
android:layout_margin="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</TextView>
代碼動态添加textview
FlowLayout flow;
String[] str = new String[]{"hallo world1","text","FlowLayout Image3","hallo world1",
"textView2","FlowLayout Image3","hallo world1",
"textView2","FlowLayout Image3"};
LayoutInflater inflater = LayoutInflater.from(this);
for (int i = ; i < str.length; i++) {
TextView tv = (TextView) inflater.inflate(R.layout.item_flow,flow,false);
tv.setText(str[i]);
flow.addView(tv);
}
最後效果如圖
到這裡,流式布局基本上就實作了,如果想動态添加,可以自己定義一個接口實作單個添加标簽。
優化
上面的方法實作了流式布局,但是我們可以看到,在onMeasure()和onLayout()方法中都計算了子view的寬高。如此,我們可不可以隻計算一次呢,在onMeasure()中就将view的坐标計算好呢?
要解決這個問題,就需要有一個數組或清單來儲存每一個view的坐标。
比如定義一個類,記錄坐标點
public class ViewPosition{
int left;
int top;
int right;
int bottom;
public ViewPosition(int left,int top,int right,int bottom){
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
}
在onMeasure()中實作坐标計算
List<ViewPos> vPos = new ArrayList<>();
if (childW + lineW > widthSize - getPaddingLeft() - getPaddingRight()){//如果放入該view是超過父布局寬度,換行
width = Math.max(lineW,childW);//取最大行寬為父布局行寬
height +=lineH;//高度累加
//開啟新行
lineW = childW;
lineH = childH;
vPos.add(new ViewPos(getPaddingLeft()+lp.leftMargin,
getPaddingTop()+lp.topMargin+height,
getPaddingLeft() + childW - lp.rightMargin,
getPaddingTop() + height + childH - lp.bottomMargin));
}else {//如果不換行,則寬度累加,高度取最大值
vPos.add(new ViewPos(getPaddingLeft() + lineW + lp.leftMargin,
getPaddingTop() + height + lp.topMargin,
getPaddingLeft() + lineW + childW - lp.rightMargin,
getPaddingTop() + height + childH - lp.bottomMargin));
lineW += childW;
lineH = Math.max(lineH,childH);
}
最後在onLayout()中就簡單了
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = ; i < count; i++) {
View child = getChildAt(i);
ViewPos pos = vPos.get(i);
//設定View的左邊、上邊、右邊底邊位置
child.layout(pos.left, pos.top, pos.right, pos.bottom);
}
}
參考部落格:Android 自定義ViewGroup 實戰篇 -> 實作FlowLayout