Android 自定义View的各类姿式2

该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,若是能给各位看官带来一丝启发或者帮助,那真是极好的。android


前言

上一篇咱们详细分析了Android事件体系。也从源码角度完全了解了为什么会有如此表现。不只要知其然,更要知其因此然。那么本篇呢,咱们依然是来自定义View。与上一番外篇不一样的是本章的重点放在ViewGroup上。咱们知道ViewGroup是View的子类,Android系统中有许多控件继承自ViewGroup的控件。好比咱们经常使用的FrameLayout、LinearLayout、RelativeLayout等布局都是继承自ViewGroup。自定义ViewGroup难度比较大,是由于ViewGroup要管理子View的测量、布局等。git

注:我真是给本身挖了个大坑,关于自定义ViewGroup的实例我想了很久也找了很久。发现想要实现一个颇有规范的自定义View是有必定代价的,这点你看看LinearLayout等系统自己的ViewGroup控件的源码就知道,他们的实现都很复杂。想选择一个较简单的把,又不想注水,较难的把,感受又绕进了代码的死胡同。不能让读者对自定义ViewGroup的核心有个更清晰的认识。一直拖到今天,真是要对你们说抱歉。好了,这些“矫情”的话就很少说了,咱们仍是来看下面的实例把。github

实现流式布局FlowLayout

我在拉勾网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>

上方布局可获得以下预览

至此咱们的准备工做已经完毕。

自定义ViewGroup(重点)

准备工做

上面咱们已经获得了一个布局文件达到了咱们流式布局中的子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,若是没有的话,咱们就来稍微歇歇,,看看日常开发中常常遇到的内存泄漏及相关解决办法。


参考博文


此致,敬礼

相关文章
相关标签/搜索