編輯:關於Android編程
本文是參考了鴻神之後的文章之後做的一些修改與總結,添加了一些自己的筆記,增加對自定義ViewGroup的理解。文章後面會給出原文地址。
首先,什麼是流式布局(FlowLayout),我個人的理解就是各個寬高不完全相同的view控件之間按照一定的規律放置,當一行或者一列中放滿了控件,再放置下一個控件時,由於空間寬度或者高度不夠,會自動放置到下一行或者下一列。比較常見的應用是一些熱門標簽,精彩評論等。比如下圖中的尺寸大小標簽。
其中上圖中的尺寸大小作為一個整體的ViewGroup,其中放置著各個子控件,由於他們寬度大小不一,不很很好的運用現有的布局來實現,而自定義的流式ViewGroup很好的可以實現。
相比於自定義View,最重要的是onDraw()方法,自定義的ViewGroup最重要的是onMeasure(),onLayout()。一個是幫助我們去測量整個ViewGroup的子控件,另一個是幫助我們去在我們的ViewGroup中去放置我們的子控件。
1.整體分析
1、由於每一個ViewGroup都對應著一個LayoutParams,都需要指定一個LayoutParams,對於FlowLayout,我們目前只需要能夠識別控件之間的margin即可,即使用MarginLayoutParams.
2、onMeasure中計算所有childView的寬和高,然後根據childView的寬和高,計算自己的寬和高。(當然,如果父控件的寬或者高不是wrap_content,直接使用父ViewGroup傳入的計算值即可)
3、onLayout中對所有的childView進行布局。
2.LayoutParams
因為我們只需要支持margin,所以直接使用系統的MarginLayoutParams,重寫其中的generateLayoutParams方法
@Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); }3.onMeasure()方法
onMeasure完成對所有子控件的測量
/** * 負責設置子控件的測量模式和大小 根據所有子控件設置自己的寬和高 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 獲得它的父容器為它設置的測量模式和大小 int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); int modeWidth = MeasureSpec.getMode(widthMeasureSpec); int modeHeight = MeasureSpec.getMode(heightMeasureSpec); Log.e(TAG, sizeWidth + "," + sizeHeight); // 如果是warp_content情況下,記錄寬和高 int width = 0; int height = 0; /** * 記錄每一行的寬度,width不斷取最大寬度 */ int lineWidth = 0; /** * 每一行的高度,累加至height */ int lineHeight = 0; int cCount = getChildCount(); // 遍歷每個子元素 for (int i = 0; i < cCount; i++) { View child = getChildAt(i); // 測量每一個child的寬和高 measureChild(child, widthMeasureSpec, heightMeasureSpec); // 得到child的lp MarginLayoutParams lp = (MarginLayoutParams) child .getLayoutParams(); // 當前子空間實際占據的寬度 int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; // 當前子空間實際占據的高度 int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; /** * 如果加入當前child,則超出最大寬度,則的到目前最大寬度給width,類加height 然後開啟新行 */ if (lineWidth + childWidth > sizeWidth-getPaddingLeft()-getPaddingRight()) { width = Math.max(lineWidth, width);// 取最大的 lineWidth = childWidth; // 重新開啟新行,開始記錄 // 疊加當前高度, height += lineHeight; // 開啟記錄下一行的高度 lineHeight = childHeight; } else // 否則累加值lineWidth,lineHeight取最大高度 { lineWidth += childWidth; lineHeight = Math.max(lineHeight, childHeight); } // 如果是最後一個,則將當前記錄的最大寬度和當前lineWidth做比較 if (i == cCount - 1) { width = Math.max(width, lineWidth); height += lineHeight; } } setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth : width, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight : height); }首先得到其父容器傳入的測量模式和寬高的計算值,然後遍歷所有的childView,使用measureChild方法對所有的childView進行測量。然後如果父ViewGroup的寬和高設置為wrap_content,我們通過測量計算得到所有childView的寬和高。最後根據測量模式,如果是MeasureSpec.EXACTLY則直接使用父ViewGroup傳入的寬和高,否則設置為上面自己計算的寬和高。
其中第48行是考慮到假如我們父ViewGroup使用了padding這一屬性的話,那麼我們就得減去這個值。
第50行,原文中寫的是width=Math.max(lineWidth,childWidth);//取最大的 這樣會有個問題,假如第一行最寬,後面逐漸變小,那麼我們取的width也就有問題了,故修改過來。
還有第63到第67行,因為在計算最後一個控件時,不管換行不換行,我們都沒有比較最後一個控件的寬(假如沒換行)和將最後一個控件的高度加上去(假如是換行了)
4.onLayout()方法
onLayout中完成對所有childView的位置以及大小的指定
/** * 存儲所有的View,按行記錄 */ private ListallViews的每個Item為每行所有View的List集合。> mAllViews = new ArrayList
>(); /** * 記錄每一行的最大高度 */ private List
mLineHeight = new ArrayList (); @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mAllViews.clear(); mLineHeight.clear(); int width = getWidth(); int lineWidth = 0; int lineHeight = 0; // 存儲每一行所有的childView List lineViews = new ArrayList (); int cCount = getChildCount(); // 遍歷所有的孩子 for (int i = 0; i < cCount; i++) { View child = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) child .getLayoutParams(); int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); // 如果已經需要換行 if (childWidth + lp.leftMargin + lp.rightMargin + lineWidth > width-getPaddingLeft()-getPaddingRight()) { // 記錄這一行所有的View以及最大高度 mLineHeight.add(lineHeight); // 將當前行的childView保存,然後開啟新的ArrayList保存下一行的childView mAllViews.add(lineViews); lineWidth = 0;// 重置行寬 lineViews = new ArrayList (); } /** * 如果不需要換行,則累加 */ lineWidth += childWidth + lp.leftMargin + lp.rightMargin; lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin); lineViews.add(child); } // 記錄最後一行 mLineHeight.add(lineHeight); mAllViews.add(lineViews); int left = getPaddingLeft(); int top = getPaddingTop(); // 得到總行數 int lineNums = mAllViews.size(); for (int i = 0; i < lineNums; i++) { // 每一行的所有的views lineViews = mAllViews.get(i); // 當前行的最大高度 lineHeight = mLineHeight.get(i); Log.e(TAG, "第" + i + "行 :" + lineViews.size() + " , " + lineViews); Log.e(TAG, "第" + i + "行, :" + lineHeight); // 遍歷當前行所有的View for (int j = 0; j < lineViews.size(); j++) { View child = lineViews.get(j); if (child.getVisibility() == View.GONE) { continue; } MarginLayoutParams lp = (MarginLayoutParams) child .getLayoutParams(); //計算childView的left,top,right,bottom int lc = left + lp.leftMargin; int tc = top + lp.topMargin; int rc =lc + child.getMeasuredWidth(); int bc = tc + child.getMeasuredHeight(); Log.e(TAG, child + " , l = " + lc + " , t = " + t + " , r =" + rc + " , b = " + bc); child.layout(lc, tc, rc, bc); left += child.getMeasuredWidth() + lp.rightMargin + lp.leftMargin; } left = getPaddingLeft(); top += lineHeight; } }
mLineHeight記錄的為每行的最大高度。
23-48行,遍歷所有的childView,用於設置allViews的值,以及mLineHeight的值。
57行,根據allViews的長度,遍歷所有的行數
67-91行,遍歷每一行的中所有的childView,對childView的left , top , right , bottom 進行計算,和定位。
92-93行,重置left和top,准備計算下一行的childView的位置。
好了,到此完成了所有的childView的繪制區域的確定,到此,我們的FlowLayout的代碼也結束了
由於考慮到了padding,第32行要減去padding的大小,53、54行初始的left和top值也要相應的變化,同理第92行
其中第28-29行,利用的是view.getMeasureWidth()方法,為什麼不利用view.getWidth()方法呢,其實在這個例子中,你用這個也沒什麼影響。但是最好是選擇前者,其中下圖是兩者的最直接的區別的顯示。左邊是getHeight,右邊是getMeasureHeight
5.布局文件如下
style.xml
drawable設備背景
最終的效果如上圖所示。
6.動態添加View
當然,最後你的控件多半也不是固定大小的,你也初始不知道的,那得要動態添加。
那我們把布局文件改下,改成只有一個父ViewGroup,其余控件我們動態添加。其中在要添加的Activity的onCreate方法中添加initView()方法
private void initView() { myFlowLayout = (MyFlowLayout) findViewById(R.id.myFlowLayout); // ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams( // ViewGroup.LayoutParams.WRAP_CONTENT, // ViewGroup.LayoutParams.WRAP_CONTENT); // lp.leftMargin = 35; // lp.rightMargin = 35; // lp.topMargin = 10; // lp.bottomMargin = 10; // for (int i = 0; i < str.length; i++) { // Button btn=new Button(this); // btn.setText(str[i]); // myFlowLayout.addView(btn,lp); // } LayoutInflater mInflater = LayoutInflater.from(this); for (int i = 0; i < str.length; i++) { TextView tv = (TextView) mInflater.inflate(R.layout.myview, myFlowLayout, false); tv.setText(str[i]); tv.setTag(i); tv.setOnClickListener(this); myFlowLayout.addView(tv); } }
其中我們用到了LayoutInflater,它可以將我們的xml布局文件轉變為view,這是動態添加的基礎。
7.添加點擊事件
可能你會對各個textView有點擊事件的要求,我們可以使用setTag方法。後期根據tag來區分不同的textView。
@Override public void onClick(View v) { reSetTextBg(); switch ((int) v.getTag()) { case 0: v.setBackgroundColor(0xffff0000); break; case 1: v.setBackgroundColor(0xffff0000); break; case 2: v.setBackgroundColor(0xffff0000); break; default: break; } } //重置標簽顏色 private void reSetTextBg() { for (int i = 0; i < str.length; i++) { TextView tView = (TextView) myFlowLayout.getChildAt(i); tView.setBackground(getResources().getDrawable( R.drawable.my_textview_bg)); } }
好了,全文結束,有什麼問題,好的想法,歡迎留言指出!
另:如果你覺得本篇博客對你有用,那麼就頂一個~~
簡介 在ReactiveCocoa中通過對相關的控件添加了信號的特征,采用category的方法在UIButton中添加其category。可以發現在Reac
下面為控件的實現歷程: 此控件高效,直接使用ondraw繪制,先亮照: 由於Android自身的星星評分控件樣式可以改,但是他的大小不好調整的缺點,只能用small no
前言:ListView——列表,它作為一個非常重要的顯示方式,不管是在Web中還是移動平台中,都是一個非常好的、不開或缺的展示信息的工具。在And
今天和大家分享的是關於在Android中設置壁紙的方法,在Android中設置壁紙的方法有三種,分別是:1、使用WallpaperManager的setResource(