Android-自定义ViewGroup-官方案例初识
自定义ViewGroup相对继承View来说就要麻烦多了。主要是涉及到子控件的测量(onMeasure)和定位(onLayout)。
我们可以看下官方的介绍ViewGroup | Android Developers
ViewGroup
public abstract class ViewGroup
extends View implements ViewParent, ViewManager
java.lang.Object
↳ android.view.View
↳ android.view.ViewGroup
Known direct subclasses
AbsoluteLayout, AdapterView<T extends Adapter>, FragmentBreadCrumbs, FrameLayout, GridLayout, LinearLayout, RelativeLayout, SlidingDrawer, Toolbar, TvView
Known indirect subclasses
AbsListView, AbsSpinner, ActionMenuView, AdapterViewAnimator, AdapterViewFlipper, AppWidgetHostView, CalendarView, DatePicker, DialerFilter, ExpandableListView, Gallery, and 20 others.
A ViewGroup is a special view that can contain other views (called children.) The view group is the base class for layouts and views containers. This class also defines the ViewGroup.LayoutParams class which serves as the base class for layouts parameters.
Also see ViewGroup.LayoutParams for layout attributes.
Developer Guides
For more information about creating user interface layouts, read the XML Layouts developer guide.
Here is a complete implementation of a custom ViewGroup that implements a simple FrameLayout along with the ability to stack children in left and right gutters.
除了一些基本的继承关系,还有ViewGroup.LayoutParams的简单提及。然后就直接干到demo部分,给出了一个示例,让我们进行分析吧!
我们先把相关的定义,属性配置,布局拿过来看看效果再做简单分析吧(估计详细的还不能整清楚,还得慢慢来才能搞清楚测量、位置这些个东东):
属性配置:attrs.xml
<declare-styleable name="CustomLayoutLP">
<attr name="android:layout_gravity" />
<attr name="layout_position">
<enum name="middle" value="0" />
<enum name="left" value="1" />
<enum name="right" value="2" />
</attr>
</declare-styleable>
package me.heyclock.hl.customcopy;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RemoteViews;
/**
* 自定义左右布局组合控件
* 1\. 目前只是初识阶段,很多方法属性还不懂
* 2\. 先大体流程加深下,比单个View要难,另外还用到了一些系统的方法,或许以后我们就会也用官方一些方法
* 3\. 这篇走一下后,我们会进行自己的一个简单的自定义ViewGroup,然后逐步完善并接触更陌生的东西
*/
@RemoteViews.RemoteView
public class CustomLayout extends ViewGroup {
/** The amount of space used by children in the left gutter. */
private int mLeftWidth;
/** The amount of space used by children in the right gutter. */
private int mRightWidth;
/** These are used for computing child frames based on their gravity. */
private final Rect mTmpContainerRect = new Rect();
private final Rect mTmpChildRect = new Rect();
public CustomLayout(Context context) {
super(context);
}
public CustomLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Any layout manager that doesn't scroll will want this.
*/
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
/**
* 基于子控件计算容器的相关尺寸 - 目前理解还算比较粗浅,有待加强研究和练习...
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
// 视图放置在左边或者右边的宽度(有些控件放置在left,宽度为x, 有些则在容易右边,宽度为y)
// 所以容器最后的宽度是两者之和
mLeftWidth = 0;
mRightWidth = 0;
// 容器的最大宽度和高度(我们会取控件中最大宽度或者最大高度的作为最后的容器宽高)
// 当然最后我们还需要考虑一些情况:比如在wrap_content的情况下默认长度为默认宽/高与设置了背景图片获取的宽高进行对比
int maxHeight = 0;
int maxWidth = 0;
// 子控件状态 - 用在resolveSizeAndState中 --- 大概看了下跟之前我们做AT_MOST等模式计算相关
// 相信这个比我们之前自己做测量写的应该是要更没问题些了!(具体的后面我们再分析)
int childState = 0;
// 根据子控件的大小来计算容器的尺寸
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// 如果控件可见的情况下进行相关测量
if (child.getVisibility() != GONE) {
// 测量子控件的尺寸,包括了padding, margin以及已经被用掉的宽/高,这里给0就行
// 内部一系列做法其实我们之前测量单个View是一样一样的,这里调用系统的方法比较方便
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 1\. 获取控件的布局属性, 然后判断layout_position是左边还是右边
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 1.1 如果是left,累积记录mLeftWidth = Max(控件的宽度 + 左右两边的间距, 居中的情况下的最大宽度)
if (lp.position == LayoutParams.POSITION_LEFT) {
mLeftWidth += Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
}
// 1.2 如果是right,mRightWidth = Max(控件的宽度 + 左右两边的间距, 居中的情况下的最大宽度)
else if (lp.position == LayoutParams.POSITION_RIGHT) {
mRightWidth += Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
} else { // 1.3 POSITION_MIDDLE -- 居中的情况下记录下maxWidth
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
}
// 2\. 高度就相对好计算,就取所有控件高度最大的高度即可
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
// TODO 这个地方涉及到官方对模式的处理resolveSizeAndState,有待研究
childState = combineMeasuredStates(childState, child.getMeasuredState());
}
}
// 宽度就是左右子控件的宽度相加,里面包括的其他约束的宽度
maxWidth += mLeftWidth + mRightWidth;
// 针对宽高做一个兼容,如果你的控件设wrap_content啥的,然后设置了一个图片,这个时候需要考虑背景图的宽度作为宽高
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// 根据模式什么的设置宽高,此官方做法比我们之前写的模式的一些处理应该要更好些...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
/**
* 在容器里面放置我们的子控件
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount();
// 初始化控件的左右两边的位置, left/top, right/bottom就是我们测量的整个容器内容的宽高
// --内部包含了padding -- margin我们测量的时候已经考虑过了,所以不包含在里面
// --这样说,我们下面的左边的坐标位置和右边的位置就大概知道如何计算了...
int leftPos = getPaddingLeft();
int rightPos = right - left - getPaddingRight();
// 初始化控件的中心区域的左右两边坐标位置
final int middleLeft = leftPos + mLeftWidth;
final int middleRight = rightPos - mRightWidth;
// 同理计算得到上下两边的坐标位置
final int parentTop = getPaddingTop();
final int parentBottom = bottom - top - getPaddingBottom();
// 开始摆放我们的控件
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// 控件可见的情况下继续摆放
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 获取子控件的测量后的宽高
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
// 计算子控件显示的范围
if (lp.position == LayoutParams.POSITION_LEFT) {
mTmpContainerRect.left = leftPos + lp.leftMargin;
mTmpContainerRect.right = leftPos + width + lp.rightMargin;
// 这里left不停的在增加,也就是每放一个控件就会相应的在之前的控件右边进行放置
// --所以出现了我们之前的布局效果
// --当然你可以根据自己的需求进行调整,这就是自定义的好处
leftPos = mTmpContainerRect.right;
} else if (lp.position == LayoutParams.POSITION_RIGHT) {
mTmpContainerRect.right = rightPos - lp.rightMargin;
mTmpContainerRect.left = rightPos - width - lp.leftMargin;
rightPos = mTmpContainerRect.left;
} else { // 居中放置的情况
mTmpContainerRect.left = middleLeft + lp.leftMargin;
mTmpContainerRect.right = middleRight - lp.rightMargin;
}
mTmpContainerRect.top = parentTop + lp.topMargin;
mTmpContainerRect.bottom = parentBottom - lp.bottomMargin;
// 用子控件的gravity、大小以及显示范围,最终获取在容器中显示的位置 lp.gravity默认是左上角
// TODO 前面计算显示范围已经考虑了width之类的,具体内部还是需要研究
Gravity.apply(lp.gravity, width, height, mTmpContainerRect, mTmpChildRect);
// 获得真正的范围mTmpChildRect后进行放置
child.layout(mTmpChildRect.left, mTmpChildRect.top,
mTmpChildRect.right, mTmpChildRect.bottom);
}
}
}
// ----------------------------------------------------------------------
// The rest of the implementation is for custom per-child layout parameters.
// If you do not need these (for example you are writing a layout manager
// that does fixed positioning of its children), you can drop all of this.
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomLayout.LayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
/**
* 这个是布局相关的属性,最终继承的是ViewGroup.LayoutParams,所以上面我们可以直接进行转换
* --目的是获取自定义属性以及一些使用常量的自定义
*/
public static class LayoutParams extends MarginLayoutParams {
/**
* The gravity to apply with the View to which these layout parameters
* are associated.
*/
public int gravity = Gravity.TOP | Gravity.START;
public static int POSITION_MIDDLE = 0;
public static int POSITION_LEFT = 1;
public static int POSITION_RIGHT = 2;
public int position = POSITION_MIDDLE;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
// Pull the layout param values from the layout XML during
// inflation. This is not needed if you don't care about
// changing the layout behavior in XML.
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayoutLP);
gravity = a.getInt(R.styleable.CustomLayoutLP_android_layout_gravity, gravity);
position = a.getInt(R.styleable.CustomLayoutLP_layout_position, position);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
}
布局文件 custom_viewgroup.xml --- 里面的drawable我们修改为了color进行,方便我们看效果。
<?xml version="1.0" encoding="utf-8"?>
<me.heyclock.hl.customcopy.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:background="#00ffffff"
android:layout_height="match_parent">
<!-- put first view to left. -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="fill_vertical|center_horizontal"
android:background="@color/colorPrimary"
android:text="l1左边"
app:layout_position="left" />
<!-- stack second view to left. -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="fill_vertical|center_horizontal"
android:background="@color/colorAccent"
android:text="l2左边第二个"
app:layout_position="left" />
<!-- also put a view on the right. -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="fill_vertical|center_horizontal"
android:background="#00ffee"
android:text="r1"
app:layout_position="right" />
<!-- by default views go in the middle; use fill vertical gravity -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="fill_vertical|center_horizontal"
android:background="#0000ee"
android:text="fill-vert" />
<!-- by default views go in the middle; use fill horizontal gravity -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|fill_horizontal"
android:background="#000000"
android:text="fill-horiz" />
<!-- by default views go in the middle; use top-left gravity -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|left"
android:background="#aa0000"
android:text="top-left" />
<!-- by default views go in the middle; use center gravity -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="#aa00ee"
android:text="center" />
<!-- by default views go in the middle; use bottom-right -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:background="#aaffee"
android:text="bottom-right" />
</me.heyclock.hl.customcopy.CustomLayout>
随便一个界面就可以使用:
setContentView(R.layout.custom_viewgroup);
看效果:
image虽然控件的高度或宽度设置的wrap_content, 但是layout_gravity设置的确实:
android:layout_gravity="fill_vertical|center_horizontal"
android:layout_gravity="center_vertical|fill_horizontal"
.....
所以高度/宽度就会充满容器.....具体的可以看gravity相关的文档说明....
接下来我们可以去尝试做个分析:
void onMeasure(...)等函数
实际上前面自定义 CustomLayout.java我们贴出的代码已经有做注释说明,小白可以一起静下来熟悉下...
希望小白的我们都能硬着头皮先理解一番再说。太复杂的东西别人以理解的方式去告诉你,但是对于小白来讲还是比较困难,所以自己硬着干一遍还是好很多,慢慢的就会越来越熟悉..
其中需要注意,这个必须要:
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomLayout.LayoutParams(getContext(), attrs);
}
因为我们的参数对象是内部定义进行返回的,如果没有这个,我们没办法进行转换。其他的后面三个方法可以忽略,这个必须要有。
否则报错,所以你尝试去改变官方demo的时候可能需要考虑下一些错误原因...
image这个就先到这里吧。后面继续自定义ViewGroup的加深和实践....