Android自定义控件之继承ViewGroup创建新容器

作者:管满满 时间:2023-06-15 08:50:11 

欢迎大家来学习本节内容,前几节我们已经学习了其他几种自定义控件,分别是Andriod 自定义控件之音频条及 Andriod 自定义控件之创建可以复用的组合控件还没有学习的同学请先去学习下,因为本节将使用到上几节所讲述的内容。

在学习新内容之前,我们先来弄清楚两个问题:

1 . 什么是ViewGroup?

ViewGroup是一种容器。它包含零个或以上的View及子View。

Android自定义控件之继承ViewGroup创建新容器

2 . ViewGroup有什么作用?

ViewGroup内部可以用来存放多个View控件,并且根据自身的测量模式,来测量View子控件,并且决定View子控件的位置。这在下面会逐步讲解它是怎么测量及决定子控件大小和位置的。

ok,弄清楚了这两个问题,那么下面我们来学习下自定义ViewGroup吧。

首先和之前几节一样,先来继承ViewGroup,并重写它们的构造方法。


public class CustomViewGroup extends ViewGroup{
public CustomViewGroup(Context context) {
this(context,null);
}

public CustomViewGroup(Context context, AttributeSet attrs) {
this(context, attrs,0);
}

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

在上面两个问题,我们知道,ViewGroup它是一个容器,它是用来存放和管理子控件的,并且子控件的测量方式是根据它的测量模式来进行的,所以我们必须重写它的onMeasure(),在该方法中进行对子View的大小进行测量,代码如下:


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for(int i = 0 ; i < childCount ; i ++){
 View children = getChildAt(i);
 measureChild(children,widthMeasureSpec,heightMeasureSpec);
}
}

其上代码,我们重写了onMeasure(),在方法里面,我们首先先获取ViewGroup中的子View的个数,然后遍历它所有的子View,得到每一个子View,调用measureChild()放来,来对子View进行测量。刚才提到子View的测量是根据ViewGroup所提供的测量模式来进行来,所以在measureChild()方法中,把ViewGroup的widthMeasureSpec 和 heightMeasureSpec和子View一起传进去了,我们可以跟进去看看是不是和我们所说的一样。

measureChild()方法源码:


protected void measureChild(View child, int parentWidthMeasureSpec,
 int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
 mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
 mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureChild()源码方法里面很好理解,它首先得到子View的LayoutParams,然后根据ViewGroup传递进来的宽高属性值和自身的LayoutParams 的宽高属性值及自身padding属性值分别调用getChildMeasureSpec()方法获取到子View的测量。由该方法我们也知道ViewGroup中在测量子View的大小时,测量结果分别是由父节点的测量模式和子View本身的LayoutParams及padding所决定的。

下面我们再来看看getChildMeasureSpec()方法的源码,看看它是怎么获取测量结果的。

getChildMeasureSpec()方法源码:


public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
 if (childDimension >= 0) {
 resultSize = childDimension;
 resultMode = MeasureSpec.EXACTLY;
 } else if (childDimension == LayoutParams.MATCH_PARENT) {
 // Child wants to be our size. So be it.
 resultSize = size;
 resultMode = MeasureSpec.EXACTLY;
 } else if (childDimension == LayoutParams.WRAP_CONTENT) {
 // Child wants to determine its own size. It can't be
 // bigger than us.
 resultSize = size;
 resultMode = MeasureSpec.AT_MOST;
 }
 break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
 if (childDimension >= 0) {
 // Child wants a specific size... so be it
 resultSize = childDimension;
 resultMode = MeasureSpec.EXACTLY;
 } else if (childDimension == LayoutParams.MATCH_PARENT) {
 // Child wants to be our size, but our size is not fixed.
 // Constrain child to not be bigger than us.
 resultSize = size;
 resultMode = MeasureSpec.AT_MOST;
 } else if (childDimension == LayoutParams.WRAP_CONTENT) {
 // Child wants to determine its own size. It can't be
 // bigger than us.
 resultSize = size;
 resultMode = MeasureSpec.AT_MOST;
 }
 break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
 if (childDimension >= 0) {
 // Child wants a specific size... let him have it
 resultSize = childDimension;
 resultMode = MeasureSpec.EXACTLY;
 } else if (childDimension == LayoutParams.MATCH_PARENT) {
 // Child wants to be our size... find out how big it should
 // be
 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
 resultMode = MeasureSpec.UNSPECIFIED;
 } else if (childDimension == LayoutParams.WRAP_CONTENT) {
 // Child wants to determine its own size.... find out how
 // big it should be
 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
 resultMode = MeasureSpec.UNSPECIFIED;
 }
 break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

该方法也很好理解:首先是获取父节点(这里是ViewGroup)的测量模式和测量的大小,并根据测量的大小值与子View自身的padding属性值相比较取最大值得到一个size的值。
然后根据父节点的测量模式分别再来判定子View的LayoutParams属性值,根据LayoutParams的属性值从而获取到子View测量的大小和模式,知道了ziView的测量模式和大小就能决定子View的大小了。

ok,子View的测量我们已经完全明白了,那么接下来,我们再来分析一下ViewGroup是怎样给子View定位的,首先我们也是必须先重写onLayout()方法,代码如下:


@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int preHeight = 0;
for(int i = 0 ; i < childCount ; i ++){
 View children = getChildAt(i);
 int cHeight = children.getMeasuredHeight();
 if(children.getVisibility() != View.GONE){
 children.layout(l, preHeight, r,preHeight += cHeight);
 }
}
}

很好理解,给子View定位,首先必须知道有多少个子View才行,所以我们先得到子View的数量,然后遍历获取每个子View。其实在定位子View的layout()方法中,系统并没有给出具体的定位方法,而是给了我们最大的限度来自己定义,下面来看下layout源码:


public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
 onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
 mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

boolean changed = isLayoutModeOptical(mParent) ?
 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
 onLayout(changed, l, t, r, b);
 mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

ListenerInfo li = mListenerInfo;
 if (li != null && li.mOnLayoutChangeListeners != null) {
 ArrayList<OnLayoutChangeListener> listenersCopy =
  (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
 int numListeners = listenersCopy.size();
 for (int i = 0; i < numListeners; ++i) {
  listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
 }
 }
}

mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

在上面一段代码中,最关键个就是setFrame(l, t, r, b);这个方法,它主要是来定位子View的四个顶点左右坐标的,然后关键的定位方法是在onLayout(changed, l, t, r, b);这个方法中,跟进去看看


protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

一看吓一跳,空的,哈哈,这也就是我上面说的,系统给了我们最大的自由,让我们自己根据需求去定义了。
而我这里是根据子View的高度让它们竖直顺序的排列下来。


View children = getChildAt(i);
int cHeight = children.getMeasuredHeight();
if(children.getVisibility() != View.GONE){
children.layout(l, preHeight, r,preHeight += cHeight);

定义一个记录上一个View的高度的变量,每次遍历以后都让它加上当前的View高度,由此就可以竖直依次地排列了每个子View,从而实现了子View的定义。

标签:Android,ViewGroup,容器
0
投稿

猜你喜欢

  • Android自定义单选多选下拉列表的实例代码

    2022-06-08 15:53:33
  • C++时间戳转换成日期时间的步骤和示例代码

    2021-05-27 18:50:07
  • Android实现截图和分享功能的代码

    2023-04-09 22:56:43
  • Spring核心IoC容器的依赖注入接口和层级包命名规范

    2021-12-31 02:22:57
  • java实现监听u盘示例分享

    2023-10-20 02:14:05
  • 分享WCF聊天程序--WCFChat实现代码

    2023-01-19 17:04:59
  • spring cloud 配置中心native配置方式

    2022-06-13 00:36:32
  • Java编写Mapreduce程序过程浅析

    2023-02-26 02:53:20
  • Android的Fragment的生命周期各状态和回调函数使用

    2022-12-10 17:44:34
  • Java继承Thread类创建线程类示例

    2023-08-04 20:27:08
  • Java 详解垃圾回收与对象生命周期

    2022-01-21 02:54:43
  • Android手机抓包步骤

    2022-05-03 18:15:59
  • C# 如何使用ajax请求

    2023-07-21 07:44:40
  • 安卓(Android)实现选择时间功能

    2023-11-05 08:20:58
  • Android运动健康睡眠自定义控件的实现

    2021-07-17 22:52:35
  • Android 仿微信发动态九宫格拖拽、删除功能

    2022-08-28 12:51:03
  • 浅谈Spring中单例Bean是线程安全的吗

    2023-07-12 23:42:21
  • Android日期选择器实现年月日三级联动

    2022-12-13 03:35:59
  • JSONObject toJSONString错误的解决

    2021-09-14 07:17:32
  • 重温C# clr 笔记总结

    2023-06-22 02:24:30
  • asp之家 软件编程 m.aspxhome.com