自定義控件罕伯,從字面意思來看我的理解是根據(jù)自己的想法定義控件。
自定義控件一般有三種類型:組合原生控件實現(xiàn)自己想要的效果
繼承原生控件實現(xiàn)自定義
完全自定義控件(繼承View 髓梅、ViewGroup)
本文的排行控件就屬于完全自定義控件,來一發(fā)效果圖:
控件效果圖.png
- 完全自定義控件中繼承View或者ViewGroup绎签,而至于你要繼承那個類枯饿,決定權在于你想做的控件中是否有子控件,有子控件則繼承ViewGroup诡必,沒有 子控件則繼承View.很顯然我們要做的這個排行控件是有一行一行的子View奢方,所以是繼承ViewGroup.
- 自定義控件中一般有三個方法 onMeasure() 、onLayout() 爸舒、onDraw() 三個方法蟋字,分別表示測量,子View的擺放和繪制內容扭勉。這個三個方法順序執(zhí)行就是Android界面繪制的流程愉老。
- 該控件目的為讓大小長度不一的小格子排放整齊,所以控件中的每一行相當于控件的子View,而每一行的每一個格子又相當于每一行的子View.根據(jù)這個思路我們可以把每一行也封裝成一個對象剖效,也在該對象中寫一個onLayout()方法來設置每個小格子的擺放位置嫉入。
- 下面開始擼這個自定義控件
首先寫個類MyFlowLayout繼承ViewGroup,繼承ViewGroup必須實現(xiàn)onLayout() 方法,該控件的子View如何擺放可以在該方法中實現(xiàn)璧尸。
/**
* Created by 毛麒添 on 2017/2/7 0007.
* 自定義排行控件
*/
public class MyFlowLayout extends ViewGroup {
public MyFlowLayout(Context context) {
super(context);
}
public MyFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
- 然后我們需要先實現(xiàn)測量的方法咒林,要把地方都給測量好了,才能擺放控件爷光。onMeasure() 方法的思路為:
- 首先獲取獲取控件的寬高垫竞,當然是去掉上下左右padding后的實際有效寬高,并獲取他們的測量模式(一般有三種模式蛀序,MeasureSpec.EXACTLY(確定模式)MeasureSpec.AT_MOST(包裹內容模式欢瞪,父容器有多大就是多大)MeasureSpec.UNSPECIFIED(沒有確定的模式));
- 遍歷所有子控件徐裸,也就是每一行遣鼓,重新測量并獲取他的寬度
- 判斷子控件的寬度是否大于上面獲取的實際有效寬度,如果沒有超出重贺,則可以添加每一行的子View骑祟,而此時如果新加入子View后寬度大于實際有效寬度,則換行气笙;如果第一次判斷已經(jīng)超出次企,而且該行沒有任何控件,一旦添加子控件潜圃,就超出寬度缸棵,則強制加入,否則先換行再加入新的每一行的子View
- 最后根據(jù)最新的高度來測量整體布局的大小
下面上代碼:
private int usedWidth;//每一行行子控件已經(jīng)使用的寬度
private int horizontalSpace= ToolUtils.dipToPx(6);//每行每個子View水平間距
private int verticalSpace= ToolUtils.dipToPx(8);//每一行豎直間距
private Line mLine;//當前行對象
private static final int MAX_LINE=100;//控件擁有的最大行數(shù)
private ArrayList<Line> lineList=new ArrayList<MyFlowLayout.Line>();//保存每一行對象的List
//測量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//獲取整體有效的高度值和寬度值
int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
//獲取寬高的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int childCount = getChildCount();//獲取所有子控件的數(shù)量
for (int i = 0; i <childCount ; i++) {//遍歷子控件
//測量每個子控件
View childView = getChildAt(i);
//如果父控件模式是確定模式EXACTLY谭期,則子控件包裹內容AT_MOST堵第,否則等于原本的模式
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, (widthMode == MeasureSpec.EXACTLY) ? MeasureSpec.AT_MOST: widthMode);
//同理高度也一樣
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, (heightMode == MeasureSpec.EXACTLY) ? MeasureSpec.AT_MOST : heightMode);
//開始測量
childView.measure(childWidthMeasureSpec,childHeightMeasureSpec);
//獲取子控件的寬度
int childWidth = childView.getMeasuredWidth();
//如果當前行對象為空稚晚。初始化一個
if(mLine==null){
mLine=new Line();
}
usedWidth+=childWidth;//已經(jīng)使用的寬度加上一個子控件的寬度
//是否超出最大寬度
if(usedWidth<width){//沒有超出
mLine.addView(childView);//當前行添加子控件
usedWidth+=horizontalSpace;//沒有超出,增加一個水平的間距
if(usedWidth>width){//如果增加間距后超出最大寬度則需要換行
if(!newLine()){//換行
break;//退出循環(huán)
}
}
}else {//已經(jīng)超出
//該行沒有任何控件型诚,一旦添加子控件客燕,就超出寬度
if(mLine.getChildSize()==0){
//強制將其加入到這一行,
mLine.addView(childView);
if (!newLine()) {//換行
break;
}
}else {
//該行有其他控件狰贯,一旦添加新控件就超出寬度也搓,先換行
if(!newLine()){//換行
break;//退出循環(huán)
}
mLine.addView(childView);
usedWidth+=childWidth+horizontalSpace;//更新已經(jīng)使用的寬度
}
}
}
//保存最后一行的數(shù)據(jù)
if(mLine!=null&&mLine.getChildSize()!=0&&!lineList.contains(mLine)){
lineList.add(mLine);
}
//獲取控件整體寬高度
int totalWidth = MeasureSpec.getSize(widthMeasureSpec);
int totalHeight=0;
for (int i = 0; i <lineList.size() ; i++) {
Line line = lineList.get(i);
totalHeight+=line.maxChildHeight;
}
//增加豎直的間距,上下邊距
totalHeight+=(lineList.size()-1)*verticalSpace+getPaddingTop()+getPaddingBottom();
//根據(jù)最新的高度來測量整體布局的大小
setMeasuredDimension(totalWidth,totalHeight);
}
- 為了屏幕適配,將dip轉換成像素的工具類
/**
* Created by 毛麒添 on 2017/1/18 0018
*/
public class ToolUtils {
/**
* @param dip dp值
* @return 返回dp轉換成的像素值
*/
public static int dipToPx(float dip){
float density = getContext().getResources().getDisplayMetrics().density;//像素密度
//dp=px/像素密度 px=dp*像素密度
int px= (int) (dip*density+0.5f);//四舍五入
return px;
}
}
- 每一行對象的封裝涵紊,根據(jù)上面的思路傍妒,每一行里面的小格子也是子View,所以也需要給每一行對象寫一個onLayout()方法,每個格子左上角的坐標就可以確定其擺放的位置摸柄,擺放小格子的思路為:
- 首先獲取每一行的實際有效寬度颤练,然后在獲取每一行除去已有子控件剩余的寬度
- 如果有剩余的寬度,則遍歷該行的所有子控件驱负,測量好寬度嗦玖,將剩余的寬度平均分配給已有的子View,
- 當一個子控件比較高度比其他的子控件高度小的時候,讓其豎直位置居中
- 如果沒有剩余空間(子控件寬度超過本身寬度跃脊,占滿整行)宇挫,強行將其設置進入該行
//每一行對象的封裝
class Line{
public ArrayList<View> childViewList=new ArrayList<View>();//當前行所有子控件的集合
public int totalChildWidth;//當前行所有子控件的總寬度
public int maxChildHeight;//當前行中所有子控件中最高的控件的高度
//添加一個子控件
public void addView(View view){
childViewList.add(view);
//獲取總寬度的值
totalChildWidth+=view.getMeasuredWidth();
//最高控件的高度
int height=view.getMeasuredHeight();
//如果當前加入的控件高度大于之前保存的高度則改變最大高度的值,否則最大高度的值保持不變
maxChildHeight=maxChildHeight<height?height:maxChildHeight;
}
//獲取子控件的個數(shù)
public int getChildSize(){
return childViewList.size();
}
//每一行設置好子view的位置
public void layout(int left,int top){
int count=getChildSize();
//如果這一行放不下要添加的控件酪术,則將該行剩余的位置平均分配給已經(jīng)存在的子控件
//屏幕的有效寬度
int valiaWidth=getMeasuredWidth()-getPaddingLeft()-getPaddingRight();
//屏幕的剩余可分配寬度
int surplusWidth=valiaWidth-totalChildWidth-(count-1)*horizontalSpace;
if(surplusWidth>=0){//如果有剩余空間
//將剩余控件平均分配給每個子控件
//每個子控件可以分配到的空間
int space= (int) (surplusWidth/count+0.5f);
//遍歷每個子控件
for (int i = 0; i <count ; i++) {
View childView = childViewList.get(i);
int measuredWidth = childView.getMeasuredWidth();
int measuredHeight = childView.getMeasuredHeight();
//將空間分配給每個子控件
measuredWidth+=space;
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY);
//重新測量
childView.measure(widthMeasureSpec,heightMeasureSpec);
//當一個子控件比較高度比其他的子控件高度小的時候器瘪,讓其豎直位置居中
//高度較小的子控件高度偏移量
int Topoffset= (int) ((maxChildHeight-measuredHeight)/2+0.5f);
if(Topoffset<0){
Topoffset=0;
}
//設置其位置
childView.layout(left,top+Topoffset,left+measuredWidth,top+Topoffset+measuredHeight);
//更新left值
left+=measuredWidth+horizontalSpace;
}
}else {//沒有剩余空間(子控件寬度超過本身寬度,占滿整行)
View childView = childViewList.get(0);
//設置位置
childView.layout(left,top,left+childView.getMeasuredWidth(),top+childView.getMeasuredHeight());
}
}
}
- 換行方法绘雁,只要調用該方法橡疼,就先保存上一行的數(shù)據(jù),并且將保存每一行已經(jīng)使用的寬度變量清零并且新建下一行的對象
/**
* 換行方法
* @return ture 創(chuàng)建新的一行成功 false 創(chuàng)建新的一行失敗
*/
private boolean newLine(){
//保存上一行的數(shù)據(jù)
lineList.add(mLine);
//如果此時的最大行數(shù)沒有超過控件最大行數(shù)限制
if(lineList.size()<MAX_LINE){
mLine=new Line();
//已經(jīng)使用的寬度清零
usedWidth=0;
return true;
}
return false;
}
- 最后在onLayout()中設置每一行的位置
//設置每一行的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left=getPaddingLeft();
int top=getPaddingTop();
//遍歷所有行對象庐舟,設置位置
for (int i = 0; i < lineList.size(); i++) {
Line line = lineList.get(i);
line.layout(left,top);
//每設置一行欣除,更新top的值
top+=line.maxChildHeight+verticalSpace;
}
}
到此,這個自定義的排行控件已經(jīng)完成继阻,上面成果圖為設置一個String類型的List,將字數(shù)不等的漢字設置給TextView,背景設置的是隨機顏色和圓角耻涛,這里就不貼代碼了。如果有哪些地方寫得不對瘟檩,請大家指出,讓我們一起共同進步澈蟆!