該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,若是能給各位看官帶來一絲啓發或者幫助,那真是極好的。android
上一篇咱們詳細分析了Android事件體系。也從源碼角度完全瞭解了爲什麼會有如此表現。不只要知其然,更要知其因此然。那麼本篇呢,咱們依然是來自定義View。與上一番外篇不一樣的是本章的重點放在ViewGroup上。咱們知道ViewGroup是View的子類,Android系統中有許多控件繼承自ViewGroup的控件。好比咱們經常使用的FrameLayout、LinearLayout、RelativeLayout等佈局都是繼承自ViewGroup。自定義ViewGroup難度比較大,是由於ViewGroup要管理子View的測量、佈局等。git
注:我真是給本身挖了個大坑,關於自定義ViewGroup的實例我想了很久也找了很久。發現想要實現一個頗有規範的自定義View是有必定代價的,這點你看看LinearLayout等系統自己的ViewGroup控件的源碼就知道,他們的實現都很複雜。想選擇一個較簡單的把,又不想注水,較難的把,感受又繞進了代碼的死衚衕。不能讓讀者對自定義ViewGroup的核心有個更清晰的認識。一直拖到今天,真是要對你們說抱歉。好了,這些「矯情」的話就很少說了,咱們仍是來看下面的實例把。github
我在拉勾網App上搜索公司或者職位的下方發現一個效果數組
拉勾網這些顯示的具體數據怎麼來的咱們不討論,咱們試着來實現一下它的這個佈局效果。ide
處於上方的Tag「猜你喜歡」、「熱門公司」能夠用一個TextView顯示,咱們忽略它。關鍵是下方的標籤流式佈局。咱們就來分析它。函數
首先流式佈局中的標籤應該是個TextView,關於它下方的橢圓形邊界,咱們能夠爲其制定background佈局
layout/tag_view.xmlthis
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="5dp" android:background="@drawable/tag_bg" android:text="Helloworld" android:textSize="15sp" android:textColor="@drawable/text_color"> </TextView>
drawable/tag_bg.xmlspa
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/checked_bg" android:state_checked="true" > </item> <item android:drawable="@drawable/normal_bg"></item> </selector>
drawable/checked_bg.xml設計
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="#88888888"/> <corners android:radius="30dp"/> <padding android:bottom="2dp" android:left="10dp" android:right="10dp" android:top="2dp"/> </shape>
drawable/normal_bg.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" > <solid android:color="#ffffff" /> <corners android:radius="30dp" /> <stroke android:color="#88888888" android:width="1dp"/> <padding android:bottom="2dp" android:left="10dp" android:right="10dp" android:top="2dp" /> </shape>
drawable/text_color.xml
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="#888888"/> </selector>
上方佈局可獲得以下預覽
至此咱們的準備工做已經完畢。
上面咱們已經獲得了一個佈局文件達到了咱們流式佈局中的子View的顯示效果。那咱們下面就來自定義ViewGroup來實現上述的流式佈局。
① 首先繼承自ViewGroup,繼承自ViewGroup重寫其構造函數以及onLayout方法,咱們使用AndroidStudio提示就好了
public class MyTagFlowLayout extends ViewGroup { public MyTagFlowLayout(Context context) { this(context, null); } public MyTagFlowLayout(Context context, AttributeSet attrs) { this(context, attrs,0); } public MyTagFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs,defStyleAttr,0); } @SuppressLint("NewApi") public MyTagFlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { } }
② 初始化一些信息
由上圖可知,咱們可將上面的流式佈局分爲三部分
//每一行的View 組成的List private List<View> lineViews = new ArrayList<>(); //每一行的高度 組成的List private List<Integer> mLineHeight = new ArrayList<Integer>(); //全部的View private List<List<View>> mAllViews = new ArrayList<List<View>>(); //適配器 private MyTagAdapter mTagAdapter;
咱們先搞定適配器,咱們提供一個數組信息
//須要顯示的數據 private String[] mGuseeYourLoveVals = new String[] {"Android", "Android移動", "Java", "UI設計師", "android實習", "android 移動","android安卓","安卓"};
適配器的實現十分簡單,咱們能夠仿照Android系統自有的適配器
/** 抽象類 */ public abstract class MyTagAdapter<T> { //數據 private List<T> mTagDatas; //構造函數 public MyTagAdapter(T[] datas) { mTagDatas = new ArrayList<T>(Arrays.asList(datas)); } //獲取總數 public int getCount() { return mTagDatas == null ? 0 : mTagDatas.size(); } //抽象方法 獲取View 由子類具體實現如何得到View public abstract View getView(MyTagFlowLayout parent, int position, T t); //獲取數據中的某個Item public T getItem(int position) { return mTagDatas.get(position); } }
咱們在MainActivity中調用以下語句
//MyTagFlowLayout使咱們自定義的ViewGroup,目前該類仍是默認實現 mGuseeYourLoveFlowLayout = (MyTagFlowLayout) findViewById(R.id.id_guess_your_love); //指定適配器,咱們這裏使用了匿名內部類的方式指定 mGuseeYourLoveFlowLayout.setAdapter(new MyTagAdapter<String>(mGuseeYourLoveVals) { //獲取LayoutInflater final LayoutInflater mInflater = LayoutInflater.from(MainActivity.this); //重點來了,咱們在該匿名內部類中實現了MyTagAdapter的getView方法 @Override public View getView(MyTagFlowLayout parent, int position, String s) { //在該方法中咱們去加載了咱們上面提到的layout/tag_view.xml,並返回TextView TextView tv = (TextView) mInflater.inflate(R.layout.tag_view, mGuseeYourLoveFlowLayout, false); tv.setText(s); return tv; } });
其中MyTagFlowLayout的setAdapter方法以下,,咱們一點點分析MyTagFlowLayout定義過程
public void setAdapter(MyTagAdapter adapter) { removeAllViews();//先清空MyTagFlowLayout下的全部View for (int i = 0; i < adapter.getCount(); i++) { //這裏的tagView 就是剛纔的TextView View tagView = adapter.getView(this, i, adapter.getItem(i)); //添加View addView(tagView); } }
咱們先來複習一下View的顯示過程measure->layout->draw。那麼顯然咱們這個要先measure,那就重寫onMeasure方法把
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //這裏咱們先獲取父View給定的測量參數,注意這個父View表明的是MyTagFlowLayout的父View int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);//獲取父View傳給MyTagFlowLayout的寬度 int modeWidth = MeasureSpec.getMode(widthMeasureSpec);//獲取父View傳給MyTagFlowLayout的寬度測量模式 int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);//獲取父View傳給MyTagFlowLayout的高度 int modeHeight = MeasureSpec.getMode(heightMeasureSpec);//獲取父View傳給MyTagFlowLayout的高度測量模式 int width = 0; int height = 0; int lineWidth = 0; int lineHeight = 0; //獲得全部的子View,在上一步的過程當中咱們已經添加的子View,按照上一步的數據,這裏的cCount 應該是8 int cCount = getChildCount(); for (int i = 0; i < cCount; i++) { //循環獲得每個子View,這個的child指向的實際是咱們上面添加TextView View child = getChildAt(i); //測量每個子View, measureChild(child, widthMeasureSpec, heightMeasureSpec); //獲得每個子View的測量寬度和高度 int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); //若是當前行的寬度+將要添加的child的寬度 > MyTagFlowLayout的寬度-pading, //說明當前行已經「滿」了,這個「滿」了意思是,當前行已經容納不了下一個子View if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {//"滿"了須要換行 width = Math.max(width, lineWidth);//MyTagFlowLayout的寬度取上一次寬度和當前lineWidth的最大值 lineWidth = childWidth;//重置當前行的lineWidth height += lineHeight;//MyTagFlowLayout的高度增長 lineHeight = childHeight;//重置當前行的lineHeight 爲子View的高度 } else {//沒「滿」,當前行能夠容納下一個子View lineWidth += childWidth;//當前行的寬度增長 lineHeight = Math.max(lineHeight, childHeight);//當前行的高度取上一次高度和子View的高度的最大值 } if (i == cCount - 1) {//若是當前View是最後的View width = Math.max(lineWidth, width);//MyTagFlowLayout的寬度取上一次寬度和當前lineWidth的最大值 height += lineHeight;//MyTagFlowLayout的高度增長 } } //設置MyTagFlowLayout的高度和寬度 //若是是在XMl指定了MyTagFlowLayout的寬度,如 android:layout_width="40dp" //那就使用指定的寬度,不然使用測量的寬度-padding,高度的設置與寬度雷同 setMeasuredDimension( modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(), modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom() ); }
上面咱們已經分析了onMeasure方法,measure是測量,後面的layout是佈局,咱們來看一下佈局
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //先清除全部的List mAllViews.clear(); mLineHeight.clear(); lineViews.clear(); //獲得MyTagFlowLayout的寬度,這個咱們已經在onMeasure方法中獲得了 int width = getWidth(); //行寬和行高初始化爲0 int lineWidth = 0; int lineHeight = 0; //同樣的獲得全部子View的數量 int cCount = getChildCount(); for (int i = 0; i < cCount; i++) { //循環獲得每個子View,這個的child指向的實際是咱們上面添加TextView View child = getChildAt(i); //View 可見性若是是View.GONE,則忽略它 if (child.getVisibility() == View.GONE) continue; //獲得子View的測量寬度和高度 int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); //若是當前行寬lineWidth + 當前子View的寬度 > MyTagFlowLayout的寬度-padding,那麼咱們該換行顯示了 if (childWidth + lineWidth > width - getPaddingLeft() - getPaddingRight()) { mLineHeight.add(lineHeight);//把當前行高lineHeight添加進表示當前全部行 行高表示的mLineHeight list中 mAllViews.add(lineViews);//一樣的加入mAllViews lineWidth = 0;//重置行寬 lineHeight = childHeight;//重置行高 lineViews = new ArrayList<View>();//重置lineViews } lineWidth += childWidth;//當前行寬lineWidth 增長 lineHeight = Math.max(lineHeight, childHeight );;//當前行高lineHeight 取前一次行高和子View的最大值 lineViews.add(child);//把子View添加進表示當前全部子View的lineViews的list中 } mLineHeight.add(lineHeight);//把當前行高lineHeight添加進表示當前全部行 行 mAllViews.add(lineViews);//一樣的加入mAllViews //獲取PaddingTop int top = getPaddingTop(); //獲取全部行的數量 int lineNum = mAllViews.size(); for (int i = 0; i < lineNum; i++) { //循環取出每一行 lineViews = mAllViews.get(i); //循環去除每一行的行高 lineHeight = mLineHeight.get(i); //獲取PaddingLeft int left = getPaddingLeft(); for (int j = 0; j < lineViews.size(); j++) { //從每一行中循環取出子View View child = lineViews.get(j); if (child.getVisibility() == View.GONE) { continue; } //調用child的layout,這裏其實是調用TextView.layout child.layout(left, top, lc + child.getMeasuredWidth(), tc + child.getMeasuredHeight()); left += child.getMeasuredWidth() ;//left遞增 } top += lineHeight;//top遞增 } }
好了,咱們來運行一下
效果並不像咱們在文章開頭給出的那樣,,可是起碼出來一個相似的了。下面要考慮的就是如何爲這些子View添加合適的間距了。。我相信聰明的讀者必定能夠自行解決這個問題的。這裏稍微提示一下間距->margin?? 若有疑問,請留言。
本篇文章咱們初探了自定義ViewGroup的一些知識和思想,很遺憾,該篇文章中許多代碼並非最佳實踐,但願各位讀者雅正。並且關於View的事件問題,我找了很久實在找不出好的例子來這裏分享給你們,若是你們有好的想法,請在評論區砸我吧,最好是把View的繪製體系和事件體系完美結合、簡單明瞭、「活血化瘀」自定義ViewGroup的實例。。我在這裏向被辜負指望的讀者們道歉。最後附上這一篇以及上一篇自定義View的所有源碼Github傳送門
若是有人提供想法,那麼下一篇咱們仍是來自定義ViewGroup,若是沒有的話,咱們就來稍微歇歇,,看看日常開發中常常遇到的內存泄漏及相關解決辦法。
此致,敬禮