Android 學習筆記 自定義控件之排行控件


  • 自定義控件罕伯,從字面意思來看我的理解是根據(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,背景設置的是隨機顏色和圓角耻涛,這里就不貼代碼了。如果有哪些地方寫得不對瘟檩,請大家指出,讓我們一起共同進步澈蟆!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末墨辛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子趴俘,更是在濱河造成了極大的恐慌睹簇,老刑警劉巖奏赘,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異太惠,居然都是意外死亡磨淌,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門凿渊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來梁只,“玉大人,你說我怎么就攤上這事埃脏√侣啵” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵彩掐,是天一觀的道長构舟。 經(jīng)常有香客問我,道長堵幽,這世上最難降的妖魔是什么狗超? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮朴下,結果婚禮上抡谐,老公的妹妹穿的比我還像新娘。我一直安慰自己桐猬,他們只是感情好麦撵,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著溃肪,像睡著了一般免胃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上惫撰,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天羔沙,我揣著相機與錄音,去河邊找鬼厨钻。 笑死扼雏,一個胖子當著我的面吹牛,可吹牛的內容都是我干的夯膀。 我是一名探鬼主播诗充,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼诱建!你這毒婦竟也來了蝴蜓?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎茎匠,沒想到半個月后格仲,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡诵冒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年凯肋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汽馋。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡侮东,死狀恐怖,靈堂內的尸體忽然破棺而出惭蟋,到底是詐尸還是另有隱情苗桂,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布告组,位于F島的核電站煤伟,受9級特大地震影響,放射性物質發(fā)生泄漏木缝。R本人自食惡果不足惜便锨,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望我碟。 院中可真熱鬧放案,春花似錦、人聲如沸矫俺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽厘托。三九已至友雳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間铅匹,已是汗流浹背押赊。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留包斑,地道東北人流礁。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像罗丰,于是被迫代替她去往敵國和親神帅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內容