Android深入浅出自定义控件(二)

在我的上篇博文Android深入迁出自定义控件(一)中介绍了如何自定义View控件,本篇博文主要介绍如何自定义ViewGroup

什么是ViewGroup?

在Android的树状结构图中,ViewGroup类衍生出我们所熟悉的LinearLayout、RelativeLayout等布局:

简单来说,ViewGroup其实就相当于所有布局的父亲,所以我们可以通过自定义ViewGroup类实现千变万化的布局。

自定义ViewGroup主要分为以下几个步骤:

1.创建自定义ViewGroup类,继承于ViewGroup类,重写ViewGroup的三个构造方法

2.重写onMeasure方法,设置好ViewGroup及其子View在界面上所显示的大小

3.添加参数

4.重写onLayout方法,设置好子View的布局,这个方法也是最为关键的,它决定了你所自定义的布局的特性

1.创建自定义ViewGroup类,继承于ViewGroup类,重写ViewGroup的三个构造方法

public class FlowLayoutextends ViewGroup{

	public FlowLayout(Context context) {
		// TODO Auto-generated constructor stub
		this(context,null);
	}

	public FlowLayout(Context context, AttributeSet attrs) {
		// TODO Auto-generated constructor stub
		this(context,attrs,0);

	}

	public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		// TODO Auto-generated constructor stub
	}	

}

2.重写onMeasure方法,设置好ViewGroup及其子View在界面上所显示的大小

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	// TODO Auto-generated method stub
	measureChildren(widthMeasureSpec, heightMeasureSpec);
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

对这个方法的使用见下文,此处记得添加measureChildren(widthMeasureSpec, heightMeasureSpec);表示由系统自己测量每个子View的大小

3.添加参数,比如我们想要让子View之间能有间距,则需要手动创建一个内部参数类

public static class FlowLayoutParams extends ViewGroup.MarginLayoutParams{

	public FlowLayoutParams(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}

}

4.重写onLayout方法,设置好子View的布局,这个方法也是最为关键的,它决定了你所自定义的布局的特性

@Override
protected void onLayout(boolean arg0, int left, int top, int right, int bottom) {
	// TODO Auto-generated method stub
	//获得FlowLayout所测量出来的宽度
	int mViewGroupWidth = getMeasuredWidth();
	/**
	* paintX:绘制每个View时的光标起点的横坐标
	* paintY:绘制每个View时的光标起点的纵坐标
	*/
	int paintX = left;
	int paintY = top;

	//用于记录上一行的最大高度
	int maxlineHeight = 0;

	int childCount = getChildCount();
	for(int i=0; i<childCount; i++){
		View childView = getChildAt(i);

		//获得每个子View的margin参数
		FlowLayout.FlowLayoutParams params = (FlowLayout.FlowLayoutParams)childView.getLayoutParams();
		//获得每个子View所测量出来的宽度和高度
		int childViewWidth = childView.getMeasuredWidth();
		int childViewHeight = childView.getMeasuredHeight();

		//如果绘制的起点横坐标+左间距+子View的宽度+右间距比FlowLayout的宽度还大,就需要进行换行
		if(paintX + childViewWidth + params.leftMargin + params.rightMargin> mViewGroupWidth){
			//绘制的起点的横坐标重新移回FlowLayout的横坐标
			paintX = left;
			//绘制的起点的纵坐标要向下移动一行的高度
			paintY = paintY + maxlineHeight + params.topMargin + params.bottomMargin;
			maxlineHeight = 0;
		}
		maxlineHeight = Math.max(childViewHeight, maxlineHeight);
		childView.layout(paintX+params.leftMargin, paintY+params.topMargin, paintX+childViewWidth+params.leftMargin, paintY+childViewHeight+params.topMargin);
		//每绘制一次,起点光标就要向右移动一次
		paintX = paintX + childViewWidth + params.leftMargin + params.rightMargin;
	}
}

解析:在onLayout方法中,我们对每个子View进行遍历并设置好它们应该在的位置,比如是LinearLayout的话,在onLayout中系统会规定它的子View只能按着横向或者竖向排列下去,也就是说,onLayout里子View的排布是按着我们自己的想法来决定,到底这个自定义布局会有怎样的特性?

此处我们demo是自定义FlowLayout,也就是每一行都向右排列下去,直到这一行不够容纳,则子View自动换行,进入下一行,依此类推,从而实现流式布局,为了实现这样的效果,最关键的应该是在换行的时候,需要实现让我们的子View能够判断到底换不换行,代码思路如下:

1.首先需要记录FlowLayout的宽度, 作为每一行的宽度上限:

int mViewGroupWidth = getMeasuredWidth();

2.每次绘制一个子View,是通过View.layout()方法来进行,而layout方法需要提供4个参数,即(绘制的起点横坐标,绘制的起点纵坐标,子View的宽度,子View的高度),而每一个子View的绘制起点肯定不一样,所以需要定义两个变量来记录:paintX,paintY:

/**
		 * paintX:绘制每个View时的光标起点的横坐标
		 * paintY:绘制每个View时的光标起点的纵坐标
		 */
		int paintX = left;
		int paintY = top;

3.通过for循环,遍历得到每个子View,由于要让子View之间能够有间距,所以还需要定义一个margin参数提供给子View:

FlowLayout.FlowLayoutParams params = (FlowLayout.FlowLayoutParams)childView.getLayoutParams();

以及获得每个子View的宽高:

int childViewWidth = childView.getMeasuredWidth();
int childViewHeight = childView.getMeasuredHeight();

4.判断如果再添加一个子View,需不需要换行,所以需要将这个View的宽度和当前行的宽度相加,与FlowLayout的宽度(即上限)进行对比,如果超过上限,就进行换行操作:

//绘制的起点的横坐标重新移回FlowLayout的横坐标
paintX = left;
//绘制的起点的纵坐标要向下移动一行的高度
paintY = paintY + maxlineHeight + params.topMargin + params.bottomMargin;
//由于换行,所以当前行变成了下一行,最大高度自然也就置为当前这个新起行的子View的高度(因为它是下一行的第一个View)
maxlineHeight = childViewHeight;

5.绘制当前子View,光标移动至下一个起点:

//每次都要计算当前行的最大高度
maxlineHeight = Math.max(childViewHeight, maxlineHeight);
childView.layout(paintX+params.leftMargin, paintY+params.topMargin, paintX+childViewWidth+params.leftMargin, paintY+childViewHeight+params.topMargin);
//每绘制一次,起点光标就要向右移动一次
paintX = paintX + childViewWidth + params.leftMargin + params.rightMargin;

至此,一个基本的FlowLayout布局定义完成,接下来就只需要在布局文件中来使用它:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >

    <com.example.view.FlowLayout
    	android:layout_width="fill_parent"
    	android:layout_height="fill_parent"
        >
        <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="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="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="wrap_content"
	    	android:text="赛事直播"
	    />
    </com.example.view.FlowLayout>

</RelativeLayout>

运行:

可以看到达到了我们所要的效果,但这里我们设置的FlowLayout的大小都是fill_parent,如果改为wrap_content会怎样?

将FlowLayout的layout_height设置为wrap_content,虽然屏幕上仍然呈现的是一样的效果,可是在预览窗口中点击FlowLayout,可以看到其实FlowLayout依旧是fill_parent的大小,而不是贴着子View的边缘,如图:

这不是我们想要的效果,正确的做法应该是:设置为wrap_content时,FlowLayout的边界是紧紧贴着子View的边缘的,所以我们应该修改onMeasure方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	// TODO Auto-generated method stub
	//measureChildren(widthMeasureSpec, heightMeasureSpec);
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);

	//得到FlowLayout的模式和宽高
	int widthMode = MeasureSpec.getMode(widthMeasureSpec);
	int widthSize = MeasureSpec.getSize(widthMeasureSpec);
	int heightMode = MeasureSpec.getMode(heightMeasureSpec);
	int heightSize = MeasureSpec.getSize(heightMeasureSpec);

	//记录wrap_content下的最终宽度和高度
	int width = 0;
	int height = 0;
	//记录每一行的最大宽度和最大高度
	int lineWidth = 0;
	int lineHeight = 0;

	int childCount = getChildCount();
	for(int i=0;i<childCount;i++){
		View childView = getChildAt(i);
		measureChild(childView, widthMeasureSpec, heightMeasureSpec);
		FlowLayout.FlowLayoutParams params = (FlowLayout.FlowLayoutParams)childView.getLayoutParams();

		int childWidth = params.leftMargin + childView.getMeasuredWidth() + params.rightMargin;
		int childHeight = params.topMargin + childView.getMeasuredHeight() + params.bottomMargin;

		//如果是换行,则比较宽度取当前行的最大宽度和下一个子View的宽度,将最大者暂时作为FlowLayout的宽度
		if(lineWidth + childWidth > widthSize){
			width = Math.max(lineWidth, childWidth);
			height = height + lineHeight;
			lineWidth = childWidth;
			lineHeight = childHeight;
		}else
	            // 否则累加值lineWidth,lineHeight取最大高度
	        {
	                lineWidth += childWidth;
	                lineHeight = Math.max(lineHeight, childHeight);
	        } 

		//如果是最后一个子View
		if (i == childCount - 1){
                	width = Math.max(width, lineWidth);
                	height += lineHeight;
            	}  

	}
	//根据模式来决定FlowLayout的大小,如果不是EXACTLY,则说明布局文件中设置的模式应该是wrap_content
	/**
	* 如果是wrap_content,则将FlowLayout宽度和高度设置为我们计算出来的最终宽高
	* 如果是fill_parent或者具体数值,则将FlowLayout宽度和高度设置为一开始getMode和getSize得到的那个宽高
	*/
	setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize
             : width, (heightMode == MeasureSpec.EXACTLY) ? heightSize
             : height);
}

再次查看预览窗口:

关于重写系统控件会在以后的博文中整合,希望本文能够让大家对自定义ViewGroup有所帮助。

时间: 2024-12-14 14:18:08

Android深入浅出自定义控件(二)的相关文章

android 自定义控件二之仿QQ长按删除

自定义Dialog 1.先上个效果图: 虽然效果丑了点,但主要学习修改已有的控件,例如修改Dialog控件 2.一些基本的只是进行了解 Dialog: theme是Dialog的样式,常用样式为: <style name="MyDialogStyle" parent="@android:Theme.Dialog"> <item name="android:windowFrame">@null</item> &l

【转】推荐--《Android深入浅出》----不错

原文网址:http://www.cnblogs.com/plokmju/p/Android_Book.html 承香墨影 推荐--<Android深入浅出> 基本信息 书名:Android深入浅出 作者:张旸 著 页数: 661 出版社: 机械工业出版社; 第1版 (2014年4月17日) 语种: 简体中文 ASIN: B00JR3P8X0 品牌: 北京华章图文信息有限公司 推荐理由 以前一直在博客园发表Android相关的技术博客,经过一年多的精心准备,<Android深入浅出>

Android深入浅出之Binder机制(转)

Android深入浅出之Binder机制 一 说明 Android系统最常见也是初学者最难搞明白的就是Binder了,很多很多的Service就是通过Binder机制来和客户端通讯交互的.所以搞明白Binder的话,在很大程度上就能理解程序运行的流程. 我们这里将以MediaService的例子来分析Binder的使用: l         ServiceManager,这是Android OS的整个服务的管理程序 l         MediaService,这个程序里边注册了提供媒体播放的服

Android深入浅出之 AudioTrack分析

Android深入浅出之Audio 第一部分 AudioTrack分析 一 目的 本文的目的是通过从Audio系统来分析Android的代码,包括Android自定义的那套机制和一些常见类的使用,比如Thread,MemoryBase等. 分析的流程是: l         先从API层对应的某个类开始,用户层先要有一个简单的使用流程. l         根据这个流程,一步步进入到JNI,服务层.在此过程中,碰到不熟悉或者第一次见到的类或者方法,都会解释.也就是深度优先的方法. 1.1 分析工

Android深入浅出之Binder机制【转】

Android深入浅出之Binder机制 一 说明 Android系统最常见也是初学者最难搞明白的就是Binder了,很多很多的Service就是通过Binder机制来和客户端通讯交互的.所以搞明白Binder的话,在很大程度上就能理解程序运行的流程. 我们这里将以MediaService的例子来分析Binder的使用: <!--[if !supportLists]-->l         <!--[endif]-->ServiceManager,这是Android OS的整个服务

Android学习Scroller(二)——ViewGroup调用scrollTo()

MainActivity如下: package cc.ac; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.LinearLayout; import android.app.Activity; /** * Demo描述: * 对ViewGroup调用sc

android binder 机制二(client和普通server)

在讲它们之间的通信之前,我们先以MediaServer为例看看普通Server进程都在干些什么. int main() { -- // 获得ProcessState实例 sp<ProcessState> proc(ProcessState::self()); // 得到ServiceManager的Binder客户端实例 sp<IServiceManager> sm = defaultServiceManager(); -- // 通过ServiceManager的Binder客户

详解Android ActionBar之二:ActionBar添加Tabs标签和下拉导航

本节主要讲解ActionBar如何添加Tabs标签和下拉导航. 一.添加标签 Tabs 在ActionBar中实现标签页可以实现android.app.ActionBar.TabListener ,重写onTabSelected.onTabUnselected和onTabReselected方法来关联Fragment.代码如下: Java代码 private class MyTabListener implements ActionBar.TabListener { private TabCon

Android学习Scroller(二)

MainActivity如下: package cc.testscroller1; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.os.Bundle; import android.view.View; import android.view.View.OnClick