Android小白进阶(二)--自定义控件之自定义ViewGroup

Android小白进阶(二)–自定义控件之自定义ViewGroup

✍ Author by NamCooper

一、系列简介

  • 1、准备整理一些自己一路走过来学习到的、日常开发中比较常用的东西。
  • 2、看别人都写了那么多博客,好羡慕。
  • 3、本人并不会多么高深的东西,所以写一些入门的基础知识而已,如果你是大神,请轻轻地鄙视我
  • 4、本系列都以均以实际代码演示为主,不会涉及多少专业的名词术语。小白文,仅此而已。

二、本文内容

  • 自定义控件中的中等难度的控件——自定义ViewGroup,上一篇所讲的组合控件在严格意义上来说也属于本文所讲得控件,不过ViewGroup所涉及的API更多,也需要开发者对于Android控件的布置形式有一个比较深入的理解。

三、概念介绍

  • 1、自定义控件: 移动端开发的主要目的是展示和交互,那么我们在学习之初就会接触到很多种类的控件,例如Button、TextView、ImageView、ListView等等我们称之为Android原生控件的东西。
           而在实际的开发中,这些原生控件固然很好用,但是不够灵活,往往不能实现我们所需要的特殊效果,这个时候我们就需要根据自己的实际需要开发出一个合适的控件来用,也就是自定控件。
           自定义控件整体上可以分为三大类,由实现的难易程度排序为:组合控件、继承ViewGroup控件、继承View控件
           有的大神可能会把组合控件归为继承ViewGroup的控件,当然这没有错,不过对于一个刚入门的小白来说,组合控件这种基本形式的自定义控件非常容易理解,所以在这里我单独列出来,作为开胃菜。
  • 2、自定义ViewGroup: ViewGroup顾名思义就是View的载体,例如我们常用的LinearLayout、Relativelayout。这两种常见布局相信每个Android开发者都非常熟悉,那么我们是否可以自己做一个类似的ViewGroup来实现添加进去的View按照我们想要的规律排列呢?
           当然可以!本文以下面这个控件为例来进行自定义ViewGroup的讲解:流式布局FlowLayout。
           流式布局FlowLayout 是Android教学的经典案例,也是很多大神在讲解这一块知识所使用较多的案例,今天小白也来实战!

四、实现演练

1、想要实现的效果图(子控件自动换行排列)
效果图

        图片盗用了某位大牛的效果图,勿怪勿怪。。

2、自定义一个FlowLayout继承ViewGroup,重写onMeasure和onLayout方法
      代码如下:

public class FlowLayout extends ViewGroup {
    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);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量所有子控件,一定要先调用这个方法,否则child.geyMessureHeight/Width方法是不会返回正确结果
        measureChildren(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = 0;//公用左边界临时变量
        //第一行的top是0
        int top = 0;
        //获取子控件的总数量
        int childCount = getChildCount();
        //循环布置子控件
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);//获取子控件
            //如果子控件累计的宽度已经大于父控件的宽度,那么就需要另起一行进行布置
            if (left + child.getMeasuredWidth() > this.getMeasuredWidth()) {
                left = 0;
                top += child.getMeasuredHeight();
            }
            child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());//将child布置到viewgroup中
            left += child.getMeasuredWidth();//每布置一个子控件则将left按照刚布置的控件的宽度增加
        }
    }
}

     解释关键代码:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量所有子控件,一定要先调用这个方法,否则child.geyMessureHeight/Width方法是不会返回正确结果
        measureChildren(widthMeasureSpec, heightMeasureSpec);
    }

      注意onMeasure 方法里一定要对子控件进行测量

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = 0;//公用左边界临时变量
        //第一行的top是0
        int top = 0;
        //获取子控件的总数量
        int childCount = getChildCount();
        //循环布置子控件
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);//获取子控件
            //如果子控件累计的宽度已经大于父控件的宽度,那么就需要另起一行进行布置
            if (left + child.getMeasuredWidth() > this.getMeasuredWidth()) {
                left = 0;
                top += child.getMeasuredHeight();
            }
            child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());//将child布置到viewgroup中
            left += child.getMeasuredWidth();//每布置一个子控件则将left按照刚布置的控件的宽度增加
        }
    }

      onLayout方法中,定义left和top两个公用的左边界、上边界局部变量,在for循环中,取出每个子控件,然后将子控件按照left、top以及该子控件的真实宽高分别加left、top作为bottom和right来进行子控件的布置。当子控件的right(left+child.getMeasuredWidth())比父控件的宽度还要大的时候,就要另起一行并将top的值增加。
      代码中的注释很详细,逻辑也比较简单,稍微浏览一下就可以看懂。
3、在布局文件中使用

    <?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.namcooper.widgetdemos.activity.ViewGroupViewActivity">

    <com.namcooper.widgetdemos.widget.FlowLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#f00"
            android:text="1111"
            android:textColor="#fff"
            android:textSize="15sp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#0f0"
            android:text="23232323232"
            android:textColor="#fff"
            android:textSize="15sp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#f00"
            android:text="222222"
            android:textColor="#fff"
            android:textSize="15sp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#0f0"
            android:text="333333"
            android:textColor="#fff"
            android:textSize="15sp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#f00"
            android:text="444"
            android:textColor="#fff"
            android:textSize="15sp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#0f0"
            android:text="55555555555"
            android:textColor="#fff"
            android:textSize="15sp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#f00"
            android:text="6666666666666"
            android:textColor="#fff"
            android:textSize="15sp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#0f0"
            android:text="7777777777777777"
            android:textColor="#fff"
            android:textSize="15sp"/>
    </com.namcooper.widgetdemos.widget.FlowLayout>

</FrameLayout>

3、运行结果

      可以看出,FlowLayout的基本形式已经出现了。但是这个控件还很不完善,不仅仅是样式上差距太大,在代码处理上也有很大问题。
4、将布局文件中的FlowLayout的height由match_parent改为wrap_content

<com.namcooper.widgetdemos.widget.FlowLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#00f">

      运行结果:

      由结果可以发现,虽然我们把FlowLayout的高设置为了wrap_content,但是这个控件仍然充满了整个屏幕的剩余空间,这跟我们想要的效果肯定是不一样的。
      解决办法:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量所有子控件,一定要先调用这个方法,否则child.geyMessureHeight/Width方法是不会返回正确结果
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //解决高度Wrap_content问题
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int realHeight = 0;//记录计算后的真实高度
        int width = 0;//临时变量记录排列的宽度
        if (heightMode == MeasureSpec.AT_MOST) {//说明高度是wrap_content
            for (int i = 0; i < getChildCount(); i++) {
                if (i == 0) {//取出第一个子控件的高度作为父控件高度的初始值
                    realHeight = getChildAt(i).getMeasuredHeight();
                }
                View child = getChildAt(i);
                width += child.getMeasuredWidth();//每次循环将width累加
                if (width > getMeasuredWidth()) {//如果累加结果大于父控件宽度,那么将父控件的高度增加一次
                    realHeight += child.getMeasuredHeight();
                    width = 0;//父控件高度增加后,将临时变量width归零
                    i--;//下次循环开始前,要将指针前移1个,避免漏算当前子控件
                }
            }
            setMeasuredDimension(getMeasuredWidth(), realHeight);
        }
    }

      运行结果:

      问题解决啦!下面就要对这个控件的样式进行优化以达到示例效果!

5、优化控件之间的间距

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //测量所有子控件,一定要先调用这个方法,否则child.geyMessureHeight/Width方法是不会返回正确结果
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //解决高度Wrap_content问题
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int realHeight = 0;//记录计算后的真实高度
        int width = 0;//临时变量记录排列的宽度
        if (heightMode == MeasureSpec.AT_MOST) {//说明高度是wrap_content
            for (int i = 0; i < getChildCount(); i++) {
                if (i == 0) {//取出第一个子控件的高度作为父控件高度的初始值
                    realHeight = getChildAt(i).getMeasuredHeight() + TOP_SPACING;
                }
                View child = getChildAt(i);
                width += child.getMeasuredWidth();//每次循环将width累加
                if (width > getMeasuredWidth()) {//如果累加结果大于父控件宽度,那么将父控件的高度增加一次
                    realHeight += child.getMeasuredHeight() + TOP_SPACING;
                    width = 0;//父控件高度增加后,将临时变量width归零
                    i--;//下次循环开始前,要将指针前移1个,避免漏算当前子控件
                }
            }
            setMeasuredDimension(getMeasuredWidth(), realHeight);
        }
    }

    private int LEFT_SPACING = 20;
    private int TOP_SPACING = 20;

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = LEFT_SPACING;//公用左边界临时变量
        //第一行的top是0
        int top = TOP_SPACING;
        //获取子控件的总数量
        int childCount = getChildCount();
        //循环布置子控件
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);//获取子控件
            //如果子控件累计的宽度已经大于父控件的宽度,那么就需要另起一行进行布置
            if (left + child.getMeasuredWidth() > this.getMeasuredWidth()) {
                left = LEFT_SPACING;
                top += child.getMeasuredHeight() + TOP_SPACING;
            }
            child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());//将child布置到viewgroup中
            left += child.getMeasuredWidth() + LEFT_SPACING;//每布置一个子控件则将left按照刚布置的控件的宽度增加
        }
    }

      运行结果:

6、设计公共方法,达到动态配置内容及设置背景的效果

    public class FlowLayout extends ViewGroup {
    private Context mContext;

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

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

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

    private int bgResId;

    public void setItemBg(int bgResId) {
        this.bgResId = bgResId;
    }

    private String[] strs;

    public void setData(String[] strs) {
        this.strs = strs;
        for (int i = 0; i < this.strs.length; i++) {//根据设置的字符串数组创建出TextView
            TextView view = new TextView(mContext);
            view.setText(this.strs[i]);
            try {
                view.setBackgroundResource(bgResId);//根据设置的背景资源id给子View设置背景
            } catch (Exception e) {
                view.setBackgroundColor(Color.WHITE);//默认背景色-白色
            }
            view.setPadding(5, 5, 5, 5);//设置TextView的padding值,也可以动态配活
            this.addView(view);
        }
        invalidate();//控件重绘
    }
    
    .....
}

      布局文件更改为:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.namcooper.widgetdemos.activity.ViewGroupViewActivity">

    <com.namcooper.widgetdemos.widget.FlowLayout
        android:id="@+id/fl_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</FrameLayout>

      activity中调用:

public class ViewGroupViewActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_group_view);

        FlowLayout flContainer = (FlowLayout) findViewById(R.id.fl_container);
        String[] strs = {"小南", "飞段", "迪特拉", "干柿鬼鲛", "绝", "角都", "蝎", "大蛇丸", "宇智波鼬", "佩恩", "宇智波带土", "宇智波斑"};
        flContainer.setItemBg(R.drawable.shape_bg);
        flContainer.setData(strs);
    }
}

      shape_bg.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">

    <corners android:radius="5dp"/>
    <solid android:color="#ff0"/>
    <stroke
        android:width="1dp"
        android:color="#000"/>

</shape>

      运行结果:

      设置字体颜色、大小以及每个子控件的间距的方法就不再详细写出,原理上是一致的。
7、动态效果演示

五、总结一下

  • 自定义ViewGroup的实现步骤:
    1、明确要实现的结果,对控件的功能有直截了当的认知,写控件之前最好画图做一做数学运算。
    2、创建一个控件继承ViewGroup,重写onMeasure方法并在其中进行子控件测量以及本身的宽高设置,重写onLayout方法并在其中按照实际需求依次调用所有子控件的layout方法。
    3、增加公共方法以及自定义属性。
  • 以上就是自定义控件之自定义ViewGroup的简单实现了,这一类控件主要优势在于可以灵活控制控件的大小和位置,当然控制控件样式更不在话下。这一类控件如果可以写的很熟练,那么恭喜你,你比我强,0.0!
  • 本文仅供学习交流,不对之处欢迎指正。
源码地址:

        下载WidgetDemos

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值