Android 自定义ViewGroup 流式布局

自定义View的基本方法

自定义View的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw();
View在Activity中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout和draw。

  • 测量:onMeasure()决定View的大小;
  • 布局:onLayout()决定View在ViewGroup中的位置;
  • 绘制:onDraw()决定绘制这个View。

自定义控件分类

  • 自定义View: 只需要重写onMeasure()和onDraw()
  • 自定义ViewGroup: 则只需要重写onMeasure()和onLayout()

自定义View基础

View的分类

视图View主要分为两类

类别 解释 特点
单一视图 即一个View,如TextView 不包含子View
视图组 即多个View组成的ViewGroup,如LinearLayout 包含子View

View类简介

  • View类是Android中各种组件的基类,如View是ViewGroup基类
  • View表现为显示在屏幕上的各种视图

Android中的UI组件都由View、ViewGroup组成。

  • View的构造函数:共有4个
// 如果View是在Java代码里面new的,则调用第一个构造函数
 public CarsonView(Context context) {
        super(context);
    }

// 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
    public  CarsonView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //API21之后才使用
    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // 如View有style属性时
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

AttributeSet与自定义属性

系统自带的View可以在xml中配置属性,对于写的好的自定义View同样可以在xml中配置属性,为了使自定义的View的属性可以在xml中配置,需要以下4个步骤:

  1. 通过<declare-styleable>为自定义View添加属性
  2. 在xml中为相应的属性声明属性值
  3. 在运行时(一般为构造函数)获取属性值
  4. 将获取到的属性值应用到View

代码如下

package com.zthx.deviceui.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import com.zthx.deviceui.R;

import java.util.ArrayList;
import java.util.List;

/**
 * 类名称: FlowLayout.java<p>
 * 类描述: 自定义流式ViewGroup<p>
 * 
 *
 * @author darryrzhong
 * @since 2019/12/11 9:37
 */
public class FlowLayout extends ViewGroup {

    /**
     * 每一行的子view
     * */
    private List<View> lineView;

    /**
     * 整个布局的所有行
     * */
    private List<List<View>> lines;

    /**
     * 每一行的高度
     * */
    private List<Integer> heights;

    public FlowLayout(Context context) {
        super(context);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private void init(){
        lineView = new ArrayList<>();
        lines = new ArrayList<>();
        heights = new ArrayList<>();

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //测量自身
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取限制信息的 mode和 size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //记录当前行的宽度个高度

        //宽度是当前行子view的宽度之和
        int lineWidth = 0;
        //高度是当前行所有子view中高度的最大值
        int lineHeight = 0;

        //整个流式布局的宽度和高度

        //所有行中宽度的最大值
        int flowLayoutWidth = 0;
        //所有行的高度叠加
        int flowLayoutHeight = 0;

        //初始化参数列表
        init();

        //遍历所有的子view,对子view进行measure,分配到具体的行
        int countView = getChildCount();
        for (int i = 0;i<countView;i++){
            View child = this.getChildAt(i);
            //测量子View 获取到当前子View的测量的宽度 /高度
            measureChild(child,widthMeasureSpec,heightMeasureSpec);
            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //看下当前的行的剩余的宽度是否可以容纳下一个子View
            //如果放不下,换行 保存当前行的所有子View,累加行高,
            //将当前行的 高度 和宽度 置零
            if (lineWidth+childWidth > widthSize){
                //换行
              lines.add(lineView);
              lineView = new ArrayList<>();
              flowLayoutWidth = Math.max(flowLayoutWidth,lineWidth);
              flowLayoutHeight +=lineHeight;
              heights.add(lineHeight);
              lineHeight = 0;
              lineWidth = 0;
            }
            lineView.add(child);
            lineWidth +=childWidth;
            lineHeight = Math.max(lineHeight,childHeight);

        }

        //FlowLayout最终宽高
        setMeasuredDimension(widthMode == MeasureSpec.EXACTLY? widthSize:flowLayoutWidth,
                heightMode == MeasureSpec.EXACTLY?heightSize:flowLayoutHeight);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
         int lineCount = lines.size();

         int currX = 0;
         int currY = 0;

         //所有的子view,一行一行的布局
         for (int i = 0;i<lineCount;i++){
             //取出一行
             List<View> lineViews = lines.get(i);
             //取出这一行的高度值
             int lineHeight = heights.get(i);
             int lineSize = lineViews.size();
             //遍历当前行的子View
             for (int j=0;j<lineSize;j++){
                 View child = lineViews.get(j);
                 int left = currX;
                 int top = currY;
                 int right = left+child.getMeasuredWidth();
                 int bottom = top+child.getMeasuredHeight();
                 child.layout(left,top,right,bottom);
                 //确定下一个view的left
                 currX +=child.getMeasuredWidth();
             }
             currY += lineHeight;
             currX = 0;
         }
    }


    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(),attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return super.checkLayoutParams(p) && p instanceof LayoutParams;
    }

    public static class LayoutParams extends MarginLayoutParams{

        public int gravity = -1;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
            try {
                gravity = array.getInt(R.styleable.FlowLayout_android_gravity,-1);
            }finally {
                array.recycle();
            }

        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }

        @Override
        public String toString() {
            return "LayoutParams{" +
                    "gravity=" + gravity +
                    ", bottomMargin=" + bottomMargin +
                    ", leftMargin=" + leftMargin +
                    ", rightMargin=" + rightMargin +
                    ", topMargin=" + topMargin +
                    ", height=" + height +
                    ", layoutAnimationParameters=" + layoutAnimationParameters +
                    ", width=" + width +
                    '}';
        }
    }

}

布局如下:

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:tools="http://schemas.android.com/tools">

    <com.zthx.deviceui.view.FlowLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="55dp"
            android:text="view1" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="第二个view" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="三个三个view"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="90dp"
            android:text="哈哈哈" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="不知道是谁" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="啦啦啦啦啦" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="85dp"
            android:text="滴答滴答"
            android:layout_gravity="bottom"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="生活不止眼前的苟且,还有诗和远方" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="哈哈哈" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="啦啦啦啦啦" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="45dp"
            android:text="Hello hi ..." />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="他是谁,我是谁" />

    </com.zthx.deviceui.view.FlowLayout>
</ScrollView>

效果如下

欢迎关注作者darryrzhong,更多干货等你来拿哟.

请赏个小红心!因为你的鼓励是我写作的最大动力!

更多精彩文章请关注

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容