Android自定義流式布局/自動換行布局實(shí)例
最近,Google開源了一個(gè)流式排版庫“FlexboxLayout”,功能強(qiáng)大,支持多種排版方式,如各種方向的自動換行等,具體資料各位可搜索學(xué)習(xí)^_^。
由于我的項(xiàng)目中,只需要從左到右S型的自動換行,需求效果圖如下:

使用FlexboxLayout這個(gè)框架未免顯得有些臃腫,所以自己動手寫了一個(gè)流式ViewGroup。
安卓中自定義ViewGroup的步驟是:
1. 新建一個(gè)類,繼承ViewGroup
2. 重寫構(gòu)造方法
3. 重寫onMeasure、onLayout方法
onMeasuer方法里一般寫測量子View寬高、確定此控件寬高的代碼;onLayout方法則是確定子View如何擺放(排版)。
代碼如下:
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
public class FlexBoxLayout extends ViewGroup {
private int mScreenWidth;
private int horizontalSpace, verticalSpace;
private float mDensity;//設(shè)備密度,用于將dp轉(zhuǎn)為px
public FlexBoxLayout(Context context) {
this(context, null);
}
public FlexBoxLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//獲取屏幕寬高、設(shè)備密度
mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
mDensity = context.getResources().getDisplayMetrics().density;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//確定此容器的寬高
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//測量子View的寬高
int childCount = getChildCount();
View child = null;
//子view擺放的起始位置
int left = getPaddingLeft();
//一行view中將最大的高度存于此變量,用于子view進(jìn)行換行時(shí)高度的計(jì)算
int maxHeightInLine = 0;
//存儲所有行的高度相加,用于確定此容器的高度
int allHeight = 0;
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
//測量子View寬高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//兩兩對比,取得一行中最大的高度
if (child.getMeasuredHeight() + child.getPaddingTop() + child.getPaddingBottom() > maxHeightInLine) {
maxHeightInLine = child.getMeasuredHeight() + child.getPaddingTop() + child.getPaddingBottom();
}
left += child.getMeasuredWidth() + dip2px(horizontalSpace) + child.getPaddingLeft() + child.getPaddingRight();
if (left >= widthSize - getPaddingRight() - getPaddingLeft()) {//換行
left = getPaddingLeft();
//累積行的總高度
allHeight += maxHeightInLine + dip2px(verticalSpace);
//因?yàn)閾Q行了,所以每行的最大高度置0
maxHeightInLine = 0;
}
}
//再加上最后一行的高度,因?yàn)橹暗母叨壤鄯e條件是換行
//最后一行沒有換行操作,所以高度應(yīng)該再加上
allHeight += maxHeightInLine;
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mScreenWidth;//如果沒有指定寬,則默認(rèn)為屏幕寬
}
if (heightMode != MeasureSpec.EXACTLY) {//如果沒有指定高度
heightSize = allHeight + getPaddingBottom() + getPaddingTop();
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
//擺放子view
View child = null;
//初始子view擺放的左上位置
int left = getPaddingLeft();
int top = getPaddingTop();
//一行view中將最大的高度存于此變量,用于子view進(jìn)行換行時(shí)高度的計(jì)算
int maxHeightInLine = 0;
for (int i = 0, len = getChildCount(); i < len; i++) {
child = getChildAt(i);
//從第二個(gè)子view開始算起
//因?yàn)榈谝粋€(gè)子view默認(rèn)從頭開始擺放
if (i > 0) {
//兩兩對比,取得一行中最大的高度
if (getChildAt(i - 1).getMeasuredHeight() > maxHeightInLine) {
maxHeightInLine = getChildAt(i - 1).getMeasuredHeight();
}
//當(dāng)前子view的起始left為 上一個(gè)子view的寬度+水平間距
left += getChildAt(i - 1).getMeasuredWidth() + dip2px(horizontalSpace);
if (left + child.getMeasuredWidth() >= getWidth() - getPaddingRight() - getPaddingLeft()) {//這一行所有子view相加的寬度大于容器的寬度,需要換行
//換行的首個(gè)子view,起始left應(yīng)該為0+容器的paddingLeft
left = getPaddingLeft();
//top的位置為上一行中擁有最大高度的某個(gè)View的高度+垂直間距
top += maxHeightInLine + dip2px(verticalSpace);
//將上一行View的最大高度置0
maxHeightInLine = 0;
}
}
//擺放子view
child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
}
}
}
/**
* dp轉(zhuǎn)為px
*
* @param dpValue
* @return
*/
private int dip2px(float dpValue) {
return (int) (dpValue * mDensity + 0.5f);
}
/**
* 設(shè)置子view間的水平間距 單位dp
*
* @param horizontalSpace
*/
public void setHorizontalSpace(int horizontalSpace) {
this.horizontalSpace = horizontalSpace;
}
/**
* 設(shè)置子view間的垂直間距 單位dp
*
* @param verticalSpace
*/
public void setVerticalSpace(int verticalSpace) {
this.verticalSpace = verticalSpace;
}
}
使用如下:
xml文件:
<com.zengd.FlexBoxLayout
android:id="@+id/flexBoxLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--這里寫子View,也可代碼動態(tài)添加-->
……
</com.zengd.FlexBoxLayout>
Activity里的代碼:
FlexBoxLayout flexBoxLayout = (FlexBoxLayout) findViewById(R.id.flex_box_layout); flexBoxLayout.setHorizontalSpace(10);//不設(shè)置默認(rèn)為0 flexBoxLayout.setVerticalSpace(10);//不設(shè)置默認(rèn)為0
運(yùn)行效果如圖:

本項(xiàng)目Demo地址:
https://github.com/zengd0/FlexBoxLayout
補(bǔ)充知識:Android 流式布局(修改版) 當(dāng)達(dá)到兩行,隱藏多余的
我就廢話不多說了,還是直接看代碼吧!
public class SearchLayout extends LinearLayout {
private final int mParentWidth;
private float textSize;
private boolean textColor;
private boolean background;
private boolean isHide = true;
public void setHide(boolean hide) {
isHide = hide;
}
public SearchLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//獲取屏幕的寬度
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
mParentWidth = metrics.widthPixels - dip2px(16f);
//自定義屬性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SearchLayout);
background = array.getBoolean(R.styleable.SearchLayout_Sear_background,false);
textColor = array.getBoolean(R.styleable.SearchLayout_Sear_textColor, false);
textSize = array.getDimension(R.styleable.SearchLayout_Sear_textSize, 0);
//方向?yàn)榭v向
setOrientation(VERTICAL);
}
//移除子控件
public void removeView() {
removeAllViews();
}
//流式布局
public void setData(List<String> data) {
if (data.isEmpty()){
return;
}
//獲取一個(gè)子布局
LinearLayout linearLayout = getLinearLayout();
for (int i = 0; i < data.size(); i++) {
//標(biāo)題
final String name = data.get(i);
//已存在的寬度
int numBar = 0;
//子控件的個(gè)數(shù)
int count = linearLayout.getChildCount();
for (int j = 0; j < count; j++) {
//一個(gè)一個(gè)獲取
ThemeTextView textView = (ThemeTextView) linearLayout.getChildAt(j);
//獲取左外邊距
LayoutParams params = (LayoutParams) textView.getLayoutParams();
int leftWidth = params.leftMargin;
int rightWidth = params.rightMargin;
//獲取寬高
textView.measure(getMeasuredWidth(), getMeasuredHeight());
//計(jì)算已存在的寬度
numBar += textView.getMeasuredWidth()+leftWidth+rightWidth;
}
//獲取一個(gè)子控件
ThemeTextView text = getText();
//給每一個(gè)控件設(shè)置點(diǎn)擊事件
text.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (onItemTitleClickListener != null){
onItemTitleClickListener.onItemTitle(name);
}
}
});
//賦值
text.setText(name);
//獲取寬高
text.measure(getMeasuredWidth(), getMeasuredHeight());
//當(dāng)前控件的寬度
int textWidth = text.getMeasuredWidth() + text.getPaddingLeft() + text.getPaddingRight();
//判斷是否超過屏幕
if (isHide && getChildCount() == 2){
ImageView imageView = getMore(false);
LayoutParams layoutParams = (LayoutParams) imageView.getLayoutParams();
int leftM = layoutParams.leftMargin;
int rightM = layoutParams.rightMargin;
imageView.measure(getMeasuredWidth(), getMeasuredHeight());
int width = imageView.getMeasuredWidth() + imageView.getPaddingLeft() + imageView.getPaddingRight();
int imageWidth = leftM + rightM + width;
if (numBar + textWidth + imageWidth >= mParentWidth){
if (numBar + textWidth + imageWidth > mParentWidth){
imageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onMoreClickListener != null){
onMoreClickListener.onShowMore(isHide);
}
}
});
linearLayout.addView(imageView);
return;
} else {
imageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onMoreClickListener != null){
onMoreClickListener.onShowMore(isHide);
}
}
});
linearLayout.addView(text);
linearLayout.addView(imageView);
return;
}
}else {
if (i + 1 <= data.size()-1) {
String title = data.get(i + 1);
ThemeTextView themeTextView = getText();
themeTextView.setText(title);
themeTextView.measure(getMeasuredWidth(),getMeasuredHeight());
int themeTextViewWidth = themeTextView.getMeasuredWidth() + themeTextView.getPaddingLeft() + themeTextView.getPaddingRight();
if (mParentWidth >= numBar + textWidth + imageWidth + themeTextViewWidth ){
linearLayout.addView(text);
continue;
}else {
imageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onMoreClickListener != null){
onMoreClickListener.onShowMore(isHide);
}
}
});
linearLayout.addView(text);
linearLayout.addView(imageView);
return;
}
}
}
}
if (i == data.size() - 1 && (getChildCount() >= 3 || (mParentWidth < numBar + textWidth) && getChildCount() == 2)){
ImageView imageView = getMore(true);
LayoutParams layoutParams = (LayoutParams) imageView.getLayoutParams();
int leftM = layoutParams.leftMargin;
int rightM = layoutParams.rightMargin;
imageView.measure(getMeasuredWidth(), getMeasuredHeight());
int width = imageView.getMeasuredWidth() + imageView.getPaddingLeft() + imageView.getPaddingRight();
int imageWidth = leftM + rightM + width;
imageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (onMoreClickListener != null){
onMoreClickListener.onShowMore(isHide);
}
}
});
if (mParentWidth >= numBar + textWidth + imageWidth){
linearLayout.addView(text);
linearLayout.addView(imageView);
}else {
if (mParentWidth >= numBar + textWidth){
linearLayout.addView(text);
linearLayout = getLinearLayout();
linearLayout.addView(imageView);
}else {
linearLayout = getLinearLayout();
linearLayout.addView(text);
linearLayout.addView(imageView);
}
}
return;
}
if (mParentWidth >= numBar + textWidth) {
//沒有,繼續(xù)添加
linearLayout.addView(text);
} else {
//否者,重新獲取一個(gè)子布局,再添加
linearLayout = getLinearLayout();
linearLayout.addView(text);
}
}
}
public LinearLayout getLinearLayout() {
//創(chuàng)建LinearLayout布局
LinearLayout linearLayout = new LinearLayout(getContext());
//設(shè)置寬高
LayoutParams params = new LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayout.setLayoutParams(params);
//添加到主布局中
this.addView(linearLayout);
return linearLayout;
}
public ThemeTextView getText() {
//創(chuàng)建TextView控件
//設(shè)置字體大小,顏色,內(nèi)邊距
ThemeTextView themeTextView = new ThemeTextView(getContext());
themeTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX , textSize);
themeTextView.setMaxEms(7);
themeTextView.setLines(1);
themeTextView.setEllipsize(TextUtils.TruncateAt.END);
themeTextView.setPadding(dip2px(8), dip2px(4), dip2px(8), dip2px(4));
if (textColor){//可以根據(jù)自己的需求修改判斷
themeTextView.setTextColor(ContextCompat.getColor(getContext(),R.color.day_text_color_thirdly));
}else {
themeTextView.setTextColor(ContextCompat.getColor(getContext(),R.color.day_text_color_thirdly));
}
if (background){
themeTextView.setBackgroundResource(R.drawable.border_search_background_day);
}
//設(shè)置寬高
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
//外邊距
params.setMargins(dip2px(8),dip2px(8),dip2px(8),dip2px(8));
themeTextView.setLayoutParams(params);
return themeTextView;
}
public ImageView getMore(boolean isHide){
ImageView imageView = new ImageView(getContext());
if (background){
imageView.setBackgroundResource(R.drawable.border_search_background_day);
}
imageView.setImageResource(R.drawable.icon_more);
if (isHide){
imageView.setRotation(180f);
}
imageView.setColorFilter(ContextCompat.getColor(getContext(),R.color.day_text_color_primary));
imageView.setPadding(dip2px(6), dip2px(6), dip2px(7), dip2px(7));
//設(shè)置寬高
LayoutParams params = new LayoutParams(ConfigSingleton.dip2px(27), ConfigSingleton.dip2px(27));
//外邊距
params.setMargins(dip2px(8),dip2px(8),dip2px(8),dip2px(8));
imageView.setLayoutParams(params);
return imageView;
}
public interface OnItemTitleClickListener{
void onItemTitle(String title);
}
public interface OnMoreClickListener{
void onShowMore(boolean ishide);
}
private OnItemTitleClickListener onItemTitleClickListener;
private OnMoreClickListener onMoreClickListener;
public void setOnItemTitleClickListener(OnItemTitleClickListener onItemTitleClickListener) {
this.onItemTitleClickListener = onItemTitleClickListener;
}
public void setOnMoreClickListener(OnMoreClickListener onMoreClickListener) {
this.onMoreClickListener = onMoreClickListener;
}
public int dip2px(float dipValue) {
float scale = getContext().getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
}
attrs文件:
<declare-styleable name="SearchLayout">
<attr name="Sear_textSize" format="dimension"/>
<attr name="Sear_textColor" format="boolean"/>
<attr name="Sear_background" format="boolean"/>
</declare-styleable>
以上這篇Android自定義流式布局/自動換行布局實(shí)例就是小編分享給大家的全部內(nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- Android流式布局FlowLayout詳解
- Android流式布局實(shí)現(xiàn)歷史搜索記錄功能
- Android實(shí)現(xiàn)熱門標(biāo)簽的流式布局
- Java Swing組件布局管理器之FlowLayout(流式布局)入門教程
- Android簡單實(shí)現(xiàn)自定義流式布局的方法
- Android自定義ViewGroup之實(shí)現(xiàn)FlowLayout流式布局
- Android 簡單實(shí)現(xiàn)一個(gè)流式布局的示例
- python GUI框架pyqt5 對圖片進(jìn)行流式布局的方法(瀑布流flowlayout)
- android流式布局onLayout()方法詳解
- Flexbox+ReclyclerView實(shí)現(xiàn)流式布局
相關(guān)文章
android使用handlerthread創(chuàng)建線程示例
這篇文章主要介紹了android使用handlerthread創(chuàng)建線程,講解了這種方式的好處及為什么不使用Thread類的原因2014-01-01
Android改變ExpandableListView的indicator圖標(biāo)實(shí)現(xiàn)方法
這篇文章主要介紹了Android改變ExpandableListView的indicator圖標(biāo)實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了改變ExpandableListView的indicator圖標(biāo)相關(guān)步驟與實(shí)現(xiàn)技巧,涉及Android配置文件的修改,需要的朋友可以參考下2016-03-03
3種Android隱藏頂部狀態(tài)欄及標(biāo)題欄的方法
這篇文章主要為大家詳細(xì)介紹了3種Android隱藏頂部狀態(tài)欄及標(biāo)題欄的方法,還涉及一種隱藏Android 4.0平板底部狀態(tài)欄的方法,感興趣的小伙伴們可以參考一下2016-02-02
Android利用shape實(shí)現(xiàn)各種簡單的形狀
這篇文章主要給大家介紹了關(guān)于Android中利用shape實(shí)現(xiàn)各種簡單的形狀的相關(guān)資料,文中給出了詳細(xì)的示例代碼供大家參考學(xué)習(xí),需要的朋友們下面跟著小編一起來學(xué)習(xí)學(xué)習(xí)吧。2017-05-05
Android鬧鈴服務(wù)AlarmManager用法深入分析
這篇文章主要介紹了Android鬧鈴服務(wù)AlarmManager用法,結(jié)合實(shí)例形式深入分析了鬧鈴服務(wù)AlarmManager的功能、原理、定義與使用方法,需要的朋友可以參考下2016-08-08
Android 將 android view 的位置設(shè)為右下角的解決方法
Android 將 android view 的位置設(shè)為右下角的解決方法,需要的朋友可以參考一下2013-05-05
Android編程實(shí)現(xiàn)基于局域網(wǎng)udp廣播自動建立socket連接的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)基于局域網(wǎng)udp廣播自動建立socket連接的方法,涉及Android使用udp廣播實(shí)現(xiàn)socket通訊的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11
Android React Native原生模塊與JS模塊通信的方法總結(jié)
這篇文章主要介紹了Android React Native原生模塊與JS模塊通信的方法總結(jié)的相關(guān)資料,需要的朋友可以參考下2017-02-02

