文档章节

自定义ViewGroup (1)支持margin,gravity以及水平,垂直排列

风荷举
 风荷举
发布于 2014/01/27 00:29
字数 2098
阅读 7986
收藏 19

最近在学习android的view部分,于是动手实现了一个类似ViewPager的可上下或者左右拖动的ViewGroup,中间遇到了一些问题(例如touchEvent在onInterceptTouchEvent和onTouchEvent之间的传递流程),现在将我的实现过程记录下来。

首先,要实现一个ViewGroup,必须至少重写onLayout()方法(当然还有构造方法啦:))。onLayout()主要是用来安排子View在我们这个ViewGroup中的摆放位置的。除了onLayout()方法之外往往还需要重写onMeasure()方法,用于测算我们所需要占用的空间。

首先,我们来重写onMeasure()方法:(先只考虑水平方向)

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	// 计算所有child view 要占用的空间
	desireWidth = 0;
	desireHeight = 0;
	int count = getChildCount();
	for (int i = 0; i < count; ++i) {
		View v = getChildAt(i);
		if (v.getVisibility() != View.GONE) {
			measureChild(v, widthMeasureSpec,
					heightMeasureSpec);
			desireWidth += v.getMeasuredWidth();
			desireHeight = Math
					.max(desireHeight, v.getMeasuredHeight());
		}
	}

	// count with padding
	desireWidth += getPaddingLeft() + getPaddingRight();
	desireHeight += getPaddingTop() + getPaddingBottom();

	// see if the size is big enough
	desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
	desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());

	setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),
			resolveSize(desireHeight, heightMeasureSpec));
}



我们计算出所有Visilibity不是Gone的View的宽度的总和作为viewgroup的最大宽度,以及这些view中的最高的一个作为viewgroup的高度。这里需要注意的是要考虑咱们viewgroup自己的padding。(目前先忽略子View的margin)。

onLayout():

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
	final int parentLeft = getPaddingLeft();
	final int parentRight = r - l - getPaddingRight();
	final int parentTop = getPaddingTop();
	final int parentBottom = b - t - getPaddingBottom();

	if (BuildConfig.DEBUG)
		Log.d("onlayout", "parentleft: " + parentLeft + "   parenttop: "
				+ parentTop + "   parentright: " + parentRight
				+ "   parentbottom: " + parentBottom);

	int left = parentLeft;
	int top = parentTop;

	int count = getChildCount();
	for (int i = 0; i < count; ++i) {
		View v = getChildAt(i);
		if (v.getVisibility() != View.GONE) {
			final int childWidth = v.getMeasuredWidth();
			final int childHeight = v.getMeasuredHeight();
				v.layout(left, top, left + childWidth, top + childHeight);
				left += childWidth;
		}
	}
}



上面的layout方法写的比较简单,就是简单的计算出每个子View的left值,然后调用view的layout方法即可。

现在我们加上xml布局文件,来看一下效果:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <com.example.testslidelistview.SlideGroup
        android:id="@+id/sl"
        android:layout_width="match_parent"
        android:layout_height="500dp"
        android:layout_marginTop="50dp"
        android:background="#FFFF00" >

        <ImageView
            android:id="@+id/iv1"
            android:layout_width="150dp"
            android:layout_height="300dp"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_1" />

        <ImageView
            android:layout_width="150dp"
            android:layout_height="300dp"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_2" />

        <ImageView
            android:layout_width="150dp"
            android:layout_height="300dp"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_3" />
    </com.example.testslidelistview.SlideGroup>

</LinearLayout>



效果图如下:

从效果图中我们看到,3个小图连在一起(因为现在不支持margin),然后我们也没办法让他们垂直居中(因为现在还不支持gravity)。

现在我们首先为咱们的ViewGroup增加一个支持margin和gravity的LayoutParams。

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

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

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

	public static class LayoutParams extends MarginLayoutParams {
		public int gravity = -1;

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

			TypedArray ta = c.obtainStyledAttributes(attrs,
					R.styleable.SlideGroup);

			gravity = ta.getInt(R.styleable.SlideGroup_layout_gravity, -1);

			ta.recycle();
		}

		public LayoutParams(int width, int height) {
			this(width, height, -1);
		}

		public LayoutParams(int width, int height, int gravity) {
			super(width, height);
			this.gravity = gravity;
		}

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

		public LayoutParams(MarginLayoutParams source) {
			super(source);
		}
	}



xml的自定义属性如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="layout_gravity">
        <!-- Push object to the top of its container, not changing its size. -->
        <flag name="top" value="0x30" />
        <!-- Push object to the bottom of its container, not changing its size. -->
        <flag name="bottom" value="0x50" />
        <!-- Push object to the left of its container, not changing its size. -->
        <flag name="left" value="0x03" />
        <!-- Push object to the right of its container, not changing its size. -->
        <flag name="right" value="0x05" />
        <!-- Place object in the vertical center of its container, not changing its size. -->
        <flag name="center_vertical" value="0x10" />
        <!-- Place object in the horizontal center of its container, not changing its size. -->
        <flag name="center_horizontal" value="0x01" />
    </attr>
    
    <declare-styleable name="SlideGroup">
        <attr name="layout_gravity" />
    </declare-styleable>
</resources>



现在基本的准备工作差不多了,然后需要修改一下onMeasure()和onLayout()。

onMeasure():(上一个版本,我们在计算最大宽度和高度时忽略了margin)

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	// 计算所有child view 要占用的空间
	desireWidth = 0;
	desireHeight = 0;
	int count = getChildCount();
	for (int i = 0; i < count; ++i) {
		View v = getChildAt(i);
		if (v.getVisibility() != View.GONE) {

			LayoutParams lp = (LayoutParams) v.getLayoutParams();
			//将measureChild改为measureChildWithMargin
			measureChildWithMargins(v, widthMeasureSpec, 0,
					heightMeasureSpec, 0);
			//这里在计算宽度时加上margin
			desireWidth += v.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
			desireHeight = Math
					.max(desireHeight, v.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
		}
	}

	// count with padding
	desireWidth += getPaddingLeft() + getPaddingRight();
	desireHeight += getPaddingTop() + getPaddingBottom();

	// see if the size is big enough
	desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
	desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());

	setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),
			resolveSize(desireHeight, heightMeasureSpec));
}



onLayout()(加上margin和gravity)
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
	final int parentLeft = getPaddingLeft();
	final int parentRight = r - l - getPaddingRight();
	final int parentTop = getPaddingTop();
	final int parentBottom = b - t - getPaddingBottom();

	if (BuildConfig.DEBUG)
		Log.d("onlayout", "parentleft: " + parentLeft + "   parenttop: "
				+ parentTop + "   parentright: " + parentRight
				+ "   parentbottom: " + parentBottom);

	int left = parentLeft;
	int top = parentTop;

	int count = getChildCount();
	for (int i = 0; i < count; ++i) {
		View v = getChildAt(i);
		if (v.getVisibility() != View.GONE) {
			LayoutParams lp = (LayoutParams) v.getLayoutParams();
			final int childWidth = v.getMeasuredWidth();
			final int childHeight = v.getMeasuredHeight();
			final int gravity = lp.gravity;
			final int horizontalGravity = gravity
					& Gravity.HORIZONTAL_GRAVITY_MASK;
			final int verticalGravity = gravity
					& Gravity.VERTICAL_GRAVITY_MASK;

			left += lp.leftMargin;
			top = parentTop + lp.topMargin;
			if (gravity != -1) {
				switch (verticalGravity) {
				case Gravity.TOP:
					break;
				case Gravity.CENTER_VERTICAL:
					top = parentTop
							+ (parentBottom - parentTop - childHeight)
							/ 2 + lp.topMargin - lp.bottomMargin;
					break;
				case Gravity.BOTTOM:
					top = parentBottom - childHeight - lp.bottomMargin;
					break;
				}
			}

			if (BuildConfig.DEBUG) {
				Log.d("onlayout", "child[width: " + childWidth
						+ ", height: " + childHeight + "]");
				Log.d("onlayout", "child[left: " + left + ", top: "
						+ top + ", right: " + (left + childWidth)
						+ ", bottom: " + (top + childHeight));
			}
			v.layout(left, top, left + childWidth, top + childHeight);
			left += childWidth + lp.rightMargin;
			
		}
	}
}



现在修改一下xml布局文件,加上例如xmlns:ly="http://schemas.android.com/apk/res-auto",的xml命名空间,来引用我们设置的layout_gravity属性。(这里的“res-auto”其实还可以使用res/com/example/testslidelistview来代替,但是前一种方法相对简单,尤其是当你将某个ui组件作为library来使用的时候)

现在的效果图如下:有了margin,有了gravity。

其实在这个基础上,我们可以很容易的添加一个方向属性,使得它可以通过设置一个xml属性或者一个java api调用来实现垂直排列。

下面我们增加一个用于表示方向的枚举类型:

public static enum Orientation {
		HORIZONTAL(0), VERTICAL(1);
		
		private int value;
		private Orientation(int i) {
			value = i;
		}
		public int value() {
			return value;
		}
		public static Orientation valueOf(int i) {
			switch (i) {
			case 0:
				return HORIZONTAL;
			case 1:
				return VERTICAL;
			default:
				throw new RuntimeException("[0->HORIZONTAL, 1->VERTICAL]");
			}
		}
	}



然后我们需要改变onMeasure(),来正确的根据方向计算需要的最大宽度和高度。
@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// 计算所有child view 要占用的空间
		desireWidth = 0;
		desireHeight = 0;
		int count = getChildCount();
		for (int i = 0; i < count; ++i) {
			View v = getChildAt(i);
			if (v.getVisibility() != View.GONE) {
				LayoutParams lp = (LayoutParams) v.getLayoutParams();
				measureChildWithMargins(v, widthMeasureSpec, 0,
						heightMeasureSpec, 0);

				//只是在这里增加了垂直或者水平方向的判断
				if (orientation == Orientation.HORIZONTAL) {
					desireWidth += v.getMeasuredWidth() + lp.leftMargin
							+ lp.rightMargin;
					desireHeight = Math.max(desireHeight, v.getMeasuredHeight()
							+ lp.topMargin + lp.bottomMargin);
				} else {
					desireWidth = Math.max(desireWidth, v.getMeasuredWidth()
							+ lp.leftMargin + lp.rightMargin);
					desireHeight += v.getMeasuredHeight() + lp.topMargin
							+ lp.bottomMargin;
				}
			}
		}

		// count with padding
		desireWidth += getPaddingLeft() + getPaddingRight();
		desireHeight += getPaddingTop() + getPaddingBottom();

		// see if the size is big enough
		desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
		desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());

		setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec),
				resolveSize(desireHeight, heightMeasureSpec));
	}



onLayout():
@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		final int parentLeft = getPaddingLeft();
		final int parentRight = r - l - getPaddingRight();
		final int parentTop = getPaddingTop();
		final int parentBottom = b - t - getPaddingBottom();

		if (BuildConfig.DEBUG)
			Log.d("onlayout", "parentleft: " + parentLeft + "   parenttop: "
					+ parentTop + "   parentright: " + parentRight
					+ "   parentbottom: " + parentBottom);

		int left = parentLeft;
		int top = parentTop;

		int count = getChildCount();
		for (int i = 0; i < count; ++i) {
			View v = getChildAt(i);
			if (v.getVisibility() != View.GONE) {
				LayoutParams lp = (LayoutParams) v.getLayoutParams();
				final int childWidth = v.getMeasuredWidth();
				final int childHeight = v.getMeasuredHeight();
				final int gravity = lp.gravity;
				final int horizontalGravity = gravity
						& Gravity.HORIZONTAL_GRAVITY_MASK;
				final int verticalGravity = gravity
						& Gravity.VERTICAL_GRAVITY_MASK;

				if (orientation == Orientation.HORIZONTAL) {
					// layout horizontally, and only consider vertical gravity

					left += lp.leftMargin;
					top = parentTop + lp.topMargin;
					if (gravity != -1) {
						switch (verticalGravity) {
						case Gravity.TOP:
							break;
						case Gravity.CENTER_VERTICAL:
							top = parentTop
									+ (parentBottom - parentTop - childHeight)
									/ 2 + lp.topMargin - lp.bottomMargin;
							break;
						case Gravity.BOTTOM:
							top = parentBottom - childHeight - lp.bottomMargin;
							break;
						}
					}

					if (BuildConfig.DEBUG) {
						Log.d("onlayout", "child[width: " + childWidth
								+ ", height: " + childHeight + "]");
						Log.d("onlayout", "child[left: " + left + ", top: "
								+ top + ", right: " + (left + childWidth)
								+ ", bottom: " + (top + childHeight));
					}
					v.layout(left, top, left + childWidth, top + childHeight);
					left += childWidth + lp.rightMargin;
				} else {
					// layout vertical, and only consider horizontal gravity

					left = parentLeft;
					top += lp.topMargin;
					switch (horizontalGravity) {
					case Gravity.LEFT:
						break;
					case Gravity.CENTER_HORIZONTAL:
						left = parentLeft
								+ (parentRight - parentLeft - childWidth) / 2
								+ lp.leftMargin - lp.rightMargin;
						break;
					case Gravity.RIGHT:
						left = parentRight - childWidth - lp.rightMargin;
						break;
					}
					v.layout(left, top, left + childWidth, top + childHeight);
					top += childHeight + lp.bottomMargin;
				}
			}
		}
	}



现在我们可以增加一个xml属性:
<attr name="orientation">
            <enum name="horizontal" value="0" />
            <enum name="vertical" value="1" />
</attr>



现在就可以在布局文件中加入ly:orientation="vertical"来实现垂直排列了(ly是自定义的xml命名空间)

布局文件如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <com.example.testslidelistview.SlideGroup
        xmlns:gs="http://schemas.android.com/apk/res-auto"
        android:id="@+id/sl"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="50dp"
        android:background="#FFFF00" >

        <ImageView
            android:id="@+id/iv1"
            android:layout_width="300dp"
            android:layout_height="200dp"
            android:layout_marginBottom="20dp"
            gs:layout_gravity="left"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_1" />

        <ImageView
            android:layout_width="300dp"
            android:layout_height="200dp"
            android:layout_marginBottom="20dp"
            gs:layout_gravity="center_horizontal"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_2" />

        <ImageView
            android:layout_width="300dp"
            android:layout_height="200dp"
            android:layout_marginBottom="20dp"
            gs:layout_gravity="right"
            android:scaleType="fitXY"
            android:src="@drawable/lead_page_3" />
    </com.example.testslidelistview.SlideGroup>

</LinearLayout>



现在效果图如下:


现在基本上是实现了一个简单的基于ViewGroup的layout,但是从上面的图中可以看出,第三张都没有显示完整,那么为了能够显示更多的内容,我们需要支持滑动,那就涉及到onTouchEvent(),以及Scroller的使用,这些就在下一篇中记录吧。。。自定义ViewGroup (2)支持滑动,并处理多指触摸可能产生的跳动问题

© 著作权归作者所有

风荷举
粉丝 11
博文 24
码字总数 24803
作品 0
朝阳
程序员
私信 提问
加载中

评论(3)

J
Jonyker

引用来自“Kevin_Yao”的评论

请问 orientation 变量从何而来。如何对其复制
这个值是从xml里面读取的:


TypedArray atArray = context.obtainStyledAttributes(attrs, R.styleable.CustomViewGroup);
orientation=atArray.getInt(R.styleable.CustomViewGroup_orientation, 0);
atArray.recycle();


if(Orientation.valueOf(orientation)==Orientation.HORIZONTAL)
K
Kevin_Yao
请问 orientation 变量从何而来。如何对其复制
Summersize
Summersize
楼主为什么不直接继承自LinearLayout呢?
Android第三十二天

1、什么是View? <1>所有高级UI组件都继承View类而实现的; <2>一个View在屏幕上占据一块矩形区域; <3>负责渲染; <4>负责处理发生的事件; <5>设置是否可见; <6>设置是否可以获得焦点等 ...

黄晓磊
2016/07/13
21
0
Android 手把手教您自定义ViewGroup(一)

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38339817 , 本文出自:【张鸿洋的博客】 最近由于工作的变动,导致的博客的更新计划有点被打乱,希望可以尽快脉动回来~...

微笑的江豚
2016/08/01
7
0
设计一个FrameLayout(Kotlin)

拆零件,然后再把零件拼装回去,来来回回对其结构也就熟悉了 FrameLayout的特点 子View按照添加顺序层叠显示 FrameLayout的尺寸与其最大子View(可见的)的尺寸相等(加上padding值) 如果要...

姜康
2018/03/27
0
0
自定义ViewGroup (2)支持滑动,并处理多指触摸可能产生的跳动问题

昨天完成了一个支持设置margin,gravity,水平或者垂直排列的简单的自定义ViewGroup。但是它并不支持滑动,所以无法展现较多的内容。现在我们重写一下onTouchEvent(),来支持滑动。 重写o...

风荷举
2014/01/28
0
2
android LinearLayout

LinearLayout是线性布局控件,它包含的子控件将以横向或竖向的方式排列,按照相对位置来排列所有的widgets或者其他的containers,超过边界时,某些控件将缺失或消失。因此一个垂直列表的每一行...

amigos_wu
2012/06/07
0
0

没有更多内容

加载失败,请刷新页面

加载更多

“旧城改造”的背后——银泰新零售阿里云解决方案(上)

相关免费课程《银泰新零售上云解决方案精讲》上线中 立足实战 讲透经典案例 助你快速理解新零售 第一节学习地址 第二节学习地址 传统线下商业体上云的案例 与其说银泰上云,倒不如说银泰“旧...

阿里云官方博客
11分钟前
0
0
记一次升级Oracle驱动引发的死锁

问题描述 近期项目需要从虚拟机环境迁移到容器环境,其中有一个项目在迁移到容器环境之后的两天之内出现了2次“死锁(deadlock)”的问题,部分关键日志如下: Found one Java-level deadlock:...

ksfzhaohui
13分钟前
2
0
MySQL 中的 information_schema 数据库

欢迎查看原文 - 本博客仅记录 https://blog.csdn.net/kikajack/article/details/80065753 -- 是否开启bin_log日志: off为关闭-- show variables like 'log_%'; show variables like '......

莫库什勒
20分钟前
0
0
Random在高并发下的缺陷以及JUC对其的优化

Random可以说是每个开发都知道,而且都用的很6的类,如果你说,你没有用过Random,也不知道Random是什么鬼,那么你也不会来到这个技术类型的社区,也看不到我的博客了。但并不是每个人都知道...

编程SHA
25分钟前
0
0
T5大牛带你解析:如何实现分布式技术

1.分布式事务 2. 分布式锁 Java 原生 API 虽然有并发锁,但并没有提供分布式锁的能力,所以针对分布式场景中的锁需要解决的方案。 分布式锁的解决方案大致有以下几种: 基于数据库实现 基于缓...

李红欧巴
37分钟前
32
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部