自定義控件 - 流式佈局(CofferFlowLayout)
先看效果圖:
簡介
爲了方便大家理解自定義View裏的一些細節點,我這裏把開發者模式裏的“顯示佈局邊界”打開了。這個Demo功能很基礎簡單,就是顯示標籤,然後給每一個標籤添加點擊事件,長按刪除事件。如果後續想加其他功能,可以不斷的完善,這種瀑布流佈局實現非常成熟,花樣也很多。寫這個主要就是練手,加深對Measure 和layout的理解。
佈局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<coffer.widget.CofferFlowLayout
android:id="@+id/flow"
android:padding="3dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>
CofferFlowLayout 就是此次瀑布流的實現。這個類繼承自ViewGroup。接下來看看這個類裏最核心的兩個方法:先把onMeasure 完整代碼貼出,然後拆分講解
@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);
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
// 0、初始化行寬、行高
int lineWidth = 0,lineHeight = 0;
// 0.1 初始化瀑布流佈局真正的寬、高
int realWidth = 0,realHeight = 0;
// 1、設置瀑布流的最大寬、高
int maxWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : mMaxSize;
int maxHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : mMaxSize;
// 2、測量子View的大小
int childCount = getChildCount();
mPosHelper.clear();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child != null && child.getVisibility() != GONE){
measureChild(child,widthMeasureSpec,heightMeasureSpec);
LayoutParams layoutParams = child.getLayoutParams();
int leftMargin = 0;
int rightMargin = 0;
int topMargin = 0;
int bottomMargin = 0;
if (layoutParams instanceof MarginLayoutParams){
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
leftMargin = marginLayoutParams.leftMargin;
rightMargin = marginLayoutParams.rightMargin;
topMargin = marginLayoutParams.topMargin;
bottomMargin = marginLayoutParams.bottomMargin;
}
// 2.1 計算出子View 佔據的寬、高
int childWidth = leftMargin + rightMargin + child.getMeasuredWidth();
int childHeight = topMargin + bottomMargin + child.getMeasuredHeight();
// 2.2.1換行
if (childWidth + lineWidth + paddingLeft + paddingRight > maxWidth) {
// 2.2.2 設置當前的行寬、高
lineWidth = childWidth;
realHeight = lineHeight;
lineHeight += childHeight;
// 2.2.3 計算子View的位置
ViewPosData data = new ViewPosData();
data.left = paddingLeft + leftMargin;
data.top = paddingTop + realHeight + topMargin;
data.right = paddingLeft + childWidth - rightMargin;
data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
mPosHelper.add(data);
}else {
// 2.3.1 計算子View的位置
ViewPosData data = new ViewPosData();
data.left = paddingLeft + leftMargin +lineWidth;
data.top = paddingTop + realHeight + topMargin;
data.right = paddingLeft + childWidth + lineWidth - rightMargin;
data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
mPosHelper.add(data);
// 2.3.2 不換行,計算當前的行寬、高
lineWidth += childWidth;
lineHeight = Math.max(lineHeight,childHeight);
}
}
}
// 設置最終的寬、高
realWidth = maxWidth;
realHeight = Math.min(lineHeight + paddingBottom + paddingTop,maxHeight);
setMeasuredDimension(realWidth,realHeight);
}
這個方法我加了部分註釋,這裏再補充些。測量的時候,一定要考慮View的寬高設置模式,例如:wrap_content、400dp、match_parent。相比這些大家瞭解自定義View的都知道,因此這裏的首先就是要知道ViewGroup當前的測量模式、大小。
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
這裏就是獲取ViewGroup的padding ,開頭我給大家放的gif圖,之所以打開“佈局邊界”模式,就是讓大家能對pading有更直觀的認識,有很多時候我們在自定義ViewGroup時忽略這個屬性,導致自己用的時候發現不生效。後面還有margin屬性也是一樣。大家在測量時一定要主要把這些值計算進去。
int maxWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : mMaxSize;
int maxHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : mMaxSize;
這一句的寫法,根據不同的策略模式設置ViewGroup的最大大小。
public class ViewPosData {
/**
* View 的位置
*/
public int left;
public int top;
public int right;
public int bottom;
}
。。。。。。。。
/**
* 這個集合存放所有子View的位置信息,方便後面佈局用
*/
private ArrayList<ViewPosData> mPosHelper;
這個輔助容器相當有用,其作用就是記錄所有子View的座標位置,有了玩意,可以在onLayout方法裏省略一大堆在onMeasure裏重複的邏輯。由於onMeasure會執行多次,因此在使用前一定要先清除數據。
measureChild(child,widthMeasureSpec,heightMeasureSpec);
LayoutParams layoutParams = child.getLayoutParams();
int leftMargin = 0;
int rightMargin = 0;
int topMargin = 0;
int bottomMargin = 0;
if (layoutParams instanceof MarginLayoutParams){
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
leftMargin = marginLayoutParams.leftMargin;
rightMargin = marginLayoutParams.rightMargin;
topMargin = marginLayoutParams.topMargin;
bottomMargin = marginLayoutParams.bottomMargin;
}
// 2.1 計算出子View 佔據的寬、高
int childWidth = leftMargin + rightMargin + child.getMeasuredWidth();
int childHeight = topMargin + bottomMargin + child.getMeasuredHeight();
測量ViewGroup前,一定要先測量子View 的大小。而子View的大小是有父View的MeasureSpec和自身LayoutParam所決定的。上面的這些代碼就是要計算出單個子View的寬高,注意,我這裏把子View 的margin也計算進去了,這個不要漏了!上面的那些代碼只是鋪墊,接下來重點核心來了:
// 2.2.1換行
if (childWidth + lineWidth + paddingLeft + paddingRight > maxWidth) {
// 2.2.2 設置當前的行寬、高
lineWidth = childWidth;
realHeight = lineHeight;
lineHeight += childHeight;
// 2.2.3 計算子View的位置
ViewPosData data = new ViewPosData();
data.left = paddingLeft + leftMargin;
data.top = paddingTop + realHeight + topMargin;
data.right = paddingLeft + childWidth - rightMargin;
data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
mPosHelper.add(data);
}else {
// 2.3.1 計算子View的位置
ViewPosData data = new ViewPosData();
data.left = paddingLeft + leftMargin +lineWidth;
data.top = paddingTop + realHeight + topMargin;
data.right = paddingLeft + childWidth + lineWidth - rightMargin;
data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
mPosHelper.add(data);
// 2.3.2 不換行,計算當前的行寬、高
lineWidth += childWidth;
lineHeight = Math.max(lineHeight,childHeight);
}
這裏要說幾點。1、注意將pading加進去,我再強調一次。2、就是View座標的計算,這裏和後年的onLayout有密切聯繫。
ViewPosData data = new ViewPosData();
data.left = paddingLeft + leftMargin +lineWidth;
data.top = paddingTop + realHeight + topMargin;
data.right = paddingLeft + childWidth + lineWidth - rightMargin;
data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
View 的座標是左、上、右、下。當我們水平橫着擺放時,top和bottom是不變的,bottom的值幾乎等於View的高度,這裏的幾乎是沒有包括的pading、margin的。大家還記得View的寬度 = getRight() - getLeft(),既然是橫着擺放,View 的left、right的值也是不斷累積,這裏我用了一個lineWidth做計算累積值。同理在換行時,高度也是如此。
// 設置最終的寬、高
realWidth = maxWidth;
realHeight = Math.min(lineHeight + paddingBottom + paddingTop,maxHeight);
setMeasuredDimension(realWidth,realHeight);
最後就是給ViewGroup設置所有子View累積計算的大小。最後在看看onLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child != null && child.getVisibility() != View.GONE){
ViewPosData data = mPosHelper.get(i);
child.layout(data.left,data.top,data.right,data.bottom);
}
}
}
有了ViewPosData幫忙記錄所有子View的座標,就不需要在重複計算了。沒有他,前面在onMeasure裏寫的那堆換行邏輯還有在囉嗦一遍。
至於給View設置事件啥的,我就不囉嗦了,接下來直接分享完整的源碼僅供參考:
public class CofferFlowLayout extends ViewGroup {
private static final String TAG = "CofferFlowLayout_tag";
/**
* 在wrap_content下 View的最大值
*/
private int mMaxSize;
private Context mContext;
/**
* 這個集合存放所有子View的位置信息,方便後面佈局用
*/
private ArrayList<ViewPosData> mPosHelper;
public CofferFlowLayout(Context context) {
super(context);
init(context);
}
public CofferFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context){
mContext = context;
mPosHelper = new ArrayList<>();
mMaxSize = Util.dipToPixel(context,300);
}
@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);
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
// 0、初始化行寬、行高
int lineWidth = 0,lineHeight = 0;
// 0.1 初始化瀑布流佈局真正的寬、高
int realWidth = 0,realHeight = 0;
// 1、設置瀑布流的最大寬、高
int maxWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : mMaxSize;
int maxHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : mMaxSize;
// 2、測量子View的大小
int childCount = getChildCount();
mPosHelper.clear();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child != null && child.getVisibility() != GONE){
measureChild(child,widthMeasureSpec,heightMeasureSpec);
LayoutParams layoutParams = child.getLayoutParams();
int leftMargin = 0;
int rightMargin = 0;
int topMargin = 0;
int bottomMargin = 0;
if (layoutParams instanceof MarginLayoutParams){
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
leftMargin = marginLayoutParams.leftMargin;
rightMargin = marginLayoutParams.rightMargin;
topMargin = marginLayoutParams.topMargin;
bottomMargin = marginLayoutParams.bottomMargin;
}
// 2.1 計算出子View 佔據的寬、高
int childWidth = leftMargin + rightMargin + child.getMeasuredWidth();
int childHeight = topMargin + bottomMargin + child.getMeasuredHeight();
// 2.2.1換行
if (childWidth + lineWidth + paddingLeft + paddingRight > maxWidth) {
// 2.2.2 設置當前的行寬、高
lineWidth = childWidth;
realHeight = lineHeight;
lineHeight += childHeight;
// 2.2.3 計算子View的位置
ViewPosData data = new ViewPosData();
data.left = paddingLeft + leftMargin;
data.top = paddingTop + realHeight + topMargin;
data.right = paddingLeft + childWidth - rightMargin;
data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
mPosHelper.add(data);
}else {
// 2.3.1 計算子View的位置
ViewPosData data = new ViewPosData();
data.left = paddingLeft + leftMargin +lineWidth;
data.top = paddingTop + realHeight + topMargin;
data.right = paddingLeft + childWidth + lineWidth - rightMargin;
data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
mPosHelper.add(data);
// 2.3.2 不換行,計算當前的行寬、高
lineWidth += childWidth;
lineHeight = Math.max(lineHeight,childHeight);
}
}
}
// 設置最終的寬、高
realWidth = maxWidth;
realHeight = Math.min(lineHeight + paddingBottom + paddingTop,maxHeight);
setMeasuredDimension(realWidth,realHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child != null && child.getVisibility() != View.GONE){
ViewPosData data = mPosHelper.get(i);
child.layout(data.left,data.top,data.right,data.bottom);
}
}
}
/*********** 以下是在父容器內創建子View ************/
private View createTagView(String title){
View view = LayoutInflater.from(mContext).inflate(R.layout.activity_arrage_item,
this, false);
TextView textView = view.findViewById(R.id.text);
textView.setText(title);
return view;
}
private ArrayList<String> mTitle;
private ItemClickListener mListener;
public void setTag(ArrayList<String> title, final ItemClickListener listener){
mTitle = title;
mListener = listener;
int count = title.size();
for (int i = 0; i < count; i++) {
View chid = createTagView(title.get(i));
final int finalI = i;
chid.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG,"onClick : "+finalI);
mListener.onClick(finalI);
}
});
chid.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Log.i(TAG,"onLongClick : "+finalI);
mListener.onLongClick(finalI);
return true;
}
});
addView(chid);
}
}
public interface ItemClickListener{
void onClick(int position);
void onLongClick(int position);
}
public void removeView(int position){
View child = getChildAt(position);
removeView(child);
updata();
}
private void updata(){
removeAllViews();
setTag(mTitle,mListener);
}
}
public class ArrangeViewActivity extends AppCompatActivity {
private CofferFlowLayout mCofferFlowLayout;
private int marginSize;
private int mViewSize;
private ArrayList<String> mTitle;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_arrage_main);
mCofferFlowLayout = findViewById(R.id.flow);
marginSize = Util.dipToPixel(this,3);
mViewSize = Util.dipToPixel(this,10);
// setView();
setView2();
}
/**
* 方式二
*/
private void setView2(){
mTitle = new ArrayList<>();
mTitle.add("涼宮春日的憂鬱");
mTitle.add("嘆息");
mTitle.add("煩悶");
mTitle.add("消失");
mTitle.add("動搖");
mTitle.add("暴走");
mTitle.add("陰謀");
mTitle.add("憤慨");
mTitle.add("分裂");
mTitle.add("驚愕");
mCofferFlowLayout.setTag(mTitle, new CofferFlowLayout.ItemClickListener() {
@Override
public void onClick(int position) {
Toast.makeText(ArrangeViewActivity.this,mTitle.get(position),
Toast.LENGTH_SHORT).show();
}
@Override
public void onLongClick(int position) {
mTitle.remove(position);
mCofferFlowLayout.removeView(position);
}
});
}
/**
* 方式一: 將標籤View 在這裏創建
*/
private void setView(){
mCofferFlowLayout.addView(createTagView("涼宮春日的憂鬱"));
mCofferFlowLayout.addView(createTagView("嘆息"));
mCofferFlowLayout.addView(createTagView("煩悶"));
mCofferFlowLayout.addView(createTagView("消失"));
mCofferFlowLayout.addView(createTagView("動搖"));
mCofferFlowLayout.addView(createTagView("暴走"));
mCofferFlowLayout.addView(createTagView("陰謀"));
mCofferFlowLayout.addView(createTagView("憤慨"));
mCofferFlowLayout.addView(createTagView("分裂"));
mCofferFlowLayout.addView(createTagView("驚愕"));
}
private View createTagView(String content){
TextView textView = new TextView(this);
textView.setText(content);
textView.setTextColor(Color.WHITE);
textView.setBackground(getResources().getDrawable(R.drawable.bg_gradient));
ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.leftMargin = marginSize;
layoutParams.bottomMargin = marginSize;
layoutParams.topMargin = marginSize;
layoutParams.rightMargin = marginSize;
textView.setLayoutParams(layoutParams);
return textView;
}
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text"
android:text="憂鬱"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp"
android:gravity="center"
android:layout_marginBottom="5dp"
android:layout_marginRight="5dp"
android:textColor="@color/white"
android:background="@drawable/bg_gradient"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</FrameLayout>
這個是activity_arrage_item.xml .