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

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

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

1 . 什么是ViewGroup?

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

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的定义。

好了,讲了这么多,现在来看看效果吧,我们就拿之前做的自定义View作为它的子View吧:

custom_viewgroup.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<com.sanhuimusic.mycustomview.view.CustomViewGroup
 android:background="#999999"
 xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:custom="http://schemas.android.com/apk/res-auto"
 android:id="@+id/customViewGroup"
 android:layout_width="match_parent"
 android:layout_height="match_parent">
 <com.sanhuimusic.mycustomview.view.CompositeViews
 android:background="#999999"
 android:id="@+id/topBar"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 custom:titleText="@string/titleText"
 custom:titleColor="#000000"
 custom:titleTextSize="@dimen/titleTextSize"
 custom:titleBackground="#999999"
 custom:leftText="@string/leftText"
 custom:leftTextColor="#FFFFFF"
 custom:leftBackground="#666666"
 custom:leftTextSize="@dimen/leftTextSize"
 custom:rightText="@string/rightText"
 custom:rightTextColor="#FFFFFF"
 custom:rightBackground="#666666"
 custom:rightTextSize="@dimen/rightTextSize"
 />
 <com.sanhuimusic.mycustomview.view.AudioBar
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 />
</com.sanhuimusic.mycustomview.view.CustomViewGroup>

MainActivity:

public class MainActivity extends AppCompatActivity {
 private CompositeViews topBar;
 private Context mContext;
 private CustomViewGroup mViewGroupContainer;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.custom_viewgroup);
 mContext = this;
 init();
 }

 private void init() {
 mViewGroupContainer = (CustomViewGroup) findViewById(R.id.customViewGroup);
 topBar = (CompositeViews)findViewById(R.id.topBar);
 topBar.setOnTopBarClickListener(new CompositeViews.TopBarClickListener(){
  @Override
  public void leftClickListener() {
  ToastUtil.makeText(MainActivity.this,"您点击了返回键",Toast.LENGTH_SHORT).show();
  }
  @Override
  public void rightClickListener() {
  ToastUtil.makeText(MainActivity.this,"您点击了搜索键",Toast.LENGTH_SHORT).show();
  }
 });
 }
}

效果图:

哈哈,是不是每个子View都按照我们所说的竖直依次排列下来了呢。正开心呢,然后突然冒出来一个想法,学习过Andriod 自定义控件之音频条这篇文章的你,会记得当时在定义全新的View时会遇到当我们的布局文件使用的是wrap_content时,View是不直接支持的,需要我们特殊的处理才能正确支持,而我们现在的 ViewGroup是不是也是这样的呢,赶快尝试一下。一尝试,坏了,果然不支持wrap_content。

所以,在自定义ViewGroup时,我们必须要注意以下几个问题:

1. 必须让ViewGroup支持wrap_content的情景下的布局。
2. 也需要支持本身的padding属性。

好,下面我们来一点一点的完善它。

1 . 我们让它先支持wrap_content。

这需要我们在onMeasure()方法中多出一些必要的改动。让它支持自身wrap_content那就需要我们对它惊醒测量,根据测量方式获取到测量大小,然后再调用setMeasuredDimension()决定显示大小。

@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);
 }

 /**
  * 让它支持自身wrap_content
  */
 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
 int mWidth = 0;
 int mHeight = 0;
 int mMaxWidth = 0;
 if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
  for(int i = 0 ; i < childCount ; i ++){
  View children = getChildAt(i);
  mWidth += children.getMeasuredWidth();
  mHeight += children.getMeasuredHeight();
  }
  setMeasuredDimension(mWidth, mHeight);
 } else if(widthSpecMode == MeasureSpec.AT_MOST){
  for(int i = 0 ; i < childCount ; i ++){
  View children = getChildAt(i);
  mMaxWidth = Math.max(mMaxWidth,children.getMeasuredWidth());
  }
  setMeasuredDimension(mMaxWidth,heightSpecSize);
 } else if(heightSpecMode == MeasureSpec.AT_MOST){
  for(int i = 0 ; i < childCount ; i ++){
  View children = getChildAt(i);
  mHeight += children.getMeasuredHeight();
  }
  setMeasuredDimension(widthSpecSize,mHeight);
 }
 }

我们再原来的基础上添加了可以支持wrap_content的代码,然后根据具体的情况进行获取大小。分为三种情况:

  • 当宽高属性都为wrap_content时,分别获取到子View的宽高并相加取得总宽高,在调用setMeasuredDimension(mWidth, mHeight)直接设置即可;
  • 当宽属性都为wrap_content时,分别获取到子View的宽并获取其中最大值,在调用setMeasuredDimension(mMaxWidth,heightSpecSize)直接设置即可;
  • 当高属性都为wrap_content时,分别获取到子View的高并相加取得总高,在调用setMeasuredDimension(widthSpecSize,mHeight)直接设置即可。

好,来看看是否可以达到我们的要求。

显然已达到目标。

2 . 需要支持本身的padding属性。

首先我们先获取到padding值,如下:

leftPadding = getPaddingLeft();
topPadding = getPaddingTop();
rightPadding = getPaddingRight();
bottomPadding = getPaddingBottom();

然后分别在设置大小的地方给加上这些属性值,如下:

if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
  for(int i = 0 ; i < childCount ; i ++){
  View children = getChildAt(i);
  mWidth += children.getMeasuredWidth();
  mHeight += children.getMeasuredHeight();
  }
  setMeasuredDimension(mWidth + leftPadding + rightPadding, mHeight
  + topPadding + bottomPadding);
 } else if(widthSpecMode == MeasureSpec.AT_MOST){
  for(int i = 0 ; i < childCount ; i ++){
  View children = getChildAt(i);
  mMaxWidth = Math.max(mMaxWidth,children.getMeasuredWidth());
  }
  setMeasuredDimension(mMaxWidth + leftPadding + rightPadding, heightSpecSize + topPadding + bottomPadding);
 } else if(heightSpecMode == MeasureSpec.AT_MOST){
  for(int i = 0 ; i < childCount ; i ++){
  View children = getChildAt(i);
  mHeight += children.getMeasuredHeight();
  }
  setMeasuredDimension(widthSpecSize + leftPadding + rightPadding, mHeight + topPadding + bottomPadding);
 }

最后在onlayout()方法中给添加属性值:

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

代码很简单,不再让preHeight = 0 了,而是直接设置为topPadding,最后在layout中也把属性值添加进来,看看结果。

其实除了以上两个问题需要注意的,还有其他也是需要关注的,比如说是支持子View的margin属性等,大致和解决padding属性一样的思路,大家可以尝试实现下。

好了,整个自定义ViewGroup的内容都讲完了,当然我们只是讲述了UI的显示,并没有谈及到功能的添加和实现。从上面可以看出,自定义ViewGroup要比自定义View复杂很多,但是只要一步一步的来完善还是可以实现不同的UI展示的。

从这几节自定义控件学习中,大家一定学到了很多知识,然后对自定义控件也不是那么怕了,同时也可以实现自己想要的各种UI啦,接下来我会总结下自定义控件中所需要使用的其他技术和知识下,让大家更好的加深印象。

好,今天就学习到这里吧,happy!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android中实现多行、水平滚动的分页的Gridview实例源码

    功能要求: (1)比如每页显示2X2,总共2XN,每个item显示图片+文字(点击有链接). 如果单行水平滚动,可以用Horizontalscrollview实现. 如果是多行水平滚动,则结合Gridview(一般是垂直滚动的)和Horizontalscrollview实现. (2)水平滚动翻页,下面有显示当前页的icon. 1.实现自定义的HorizontalScrollView(HorizontalScrollView.java): 因为要翻页时需要传当前页给调用者,所以fling函数中自己

  • Android使用Recyclerview实现图片水平自动循环滚动效果

    简介: 本篇博客主要介绍的是如何使用RecyclerView实现图片水平方向自动循环(跑马灯效果) 效果图:  思路: 1.准备m张图片 1.使用Recyclerview实现,返回无数个(实际Interge.MAXVALUE)item,第n个item显示第n%m张图片 3.使用recyclerview.scrollBy  每个一段时间水平滚动一段距离 4.通过layoutManager.findFirstVisibleItemPosition()获取当前显示的第一个View是第几个item,上面

  • Android中标签容器控件的实例详解

    前言 在一些APP中我们可以看到一些存放标签的容器控件,和我们平时使用的一些布局方式有些不同,它们一般都可以自动适应屏幕的宽度进行布局,根据对自定义控件的一些理解,今天写一个简单的标签容器控件,给大家参考学习. 下面这个是我在手机上截取的一个实例,是在MIUI8系统上截取的 这个是我实现的效果图 原理介绍 根据对整个控件的效果分析,大致可以将控件分别从以下这几个角度进行分析: 1.首先涉及到自定义的ViewGroup,因为现有的控件没法满足我们的布局效果,就涉及到要重写onMeasure和onL

  • Android开发实现自定义水平滚动的容器示例

    本文实例讲述了Android开发实现自定义水平滚动的容器.分享给大家供大家参考,具体如下: public class HorizontalScrollView extends ViewGroup { //手势 private GestureDetector mGestureDetector; private HorizontalScroller mScroller; private int curID; //快速滑动 private boolean isFlying; //--回调函数-----

  • Android应用开发中自定义ViewGroup视图容器的教程

    一.概述 在写代码之前,我必须得问几个问题: 1.ViewGroup的职责是啥? ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是为用于告诉容器的),我们的宽度(layout_width).高度(layout_height).对齐方式(layout_gravity)等:当然还有margin等:于是乎,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 :决定childView的位置:为什么只是

  • 详解Android使GridView横向水平滚动的实现方式

    Android为我们提供了竖直方向的滚动控件GridView,但如果我们想让它水平滚动起来,就需要自己实现了. 以下使用的测试数据datas集合都为List<ResolveInfo>类型,用来存储手机中的所有App public static List<ResolveInfo> getAppData(Context context) { PackageManager packageManager = context.getPackageManager(); Intent mainI

  • Android实现Activity水平和垂直滚动条的方法

    本文实例讲述了Android实现Activity水平和垂直滚动条的方法.分享给大家供大家参考,具体如下: <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="

  • Android使用RecyclerView实现水平滚动控件

    前言 相信大家都知道Android滚动控件的实现方式有很多, 使用RecyclerView也比较简单. 做了一个简单的年龄滚动控件, 让我们来看看RecyclerView的使用方式, 主要有以下几点: (1) 对齐控件中心位置. (2) 计算滚动距离. (3) 高亮中心视图. (4) 实时显示中心数据. (5) 停止时自动对齐. (6) 滚动时, 设置按钮状态开关. 效果 1. 框架 主要关注RecyclerView部分逻辑. /** * 初始化年龄滑动条 */ private void ini

  • android listview 水平滚动和垂直滚动的小例子

    网上有很多解决 android listview 水平和垂直滚动的代码,我没有按照他们说的做(以前没搜到 O(∩_∩)O~) 我采用的是添加HorizontalScrollViewJava代码 复制代码 代码如下: < ScrollView android:id="@+id/ScrollView01" android:layout_height="300px" android:layout_x="16px" android:layout_y

  • 从源码解析Android中View的容器ViewGroup

    这回我们是深入到ViewGroup内部\,了解ViewGroup的工作,同时会阐述更多有关于View的相关知识.以便为以后能灵活的使用自定义空间打更近一步的基础.希望有志同道合的朋友一起来探讨,深入Android内部,深入理解Android. 一.ViewGroup是什么?        一个ViewGroup是一个可以包含子View的容器,是布局文件和View容器的基类.在这个类里定义了ViewGroup.LayoutParams类,这个类是布局参数的子类. 其实ViewGroup也就是Vie

  • Android自定义ViewGroup实现标签流容器FlowLayout

    本篇文章讲的是Android 自定义ViewGroup之实现标签流式布局-FlowLayout,开发中我们会经常需要实现类似于热门标签等自动换行的流式布局的功能,网上也有很多这样的FlowLayout,但不影响我对其的学习.和往常一样,主要还是想总结一下自定义ViewGroup的开发过程以及一些需要注意的地方. 按照惯例,我们先来看看效果图 一.写代码之前,有几个是问题是我们先要弄清楚的: 1.什么是ViewGroup:从名字上来看,它可以被翻译为控件组,言外之意是ViewGroup内部包含了许

随机推荐