这里其实我走了一段弯路,开始参考
TextInputLayout
源码时,陷入它的2个坑里:
addView(View child, int index, LayoutParams params)
方法可能是因为编译后的缘故,Override
标志没了,我误以为此方法并非重写的方法。
params
没有标注完整的具体类型,因为TextInputLayout
本身是继承的LinearLayout
,我想当然的把它当作了LinearLayout.LayoutParams
,而实际上它始终都是ViewGroup.LayoutParams
。
本来错误2很容易发现,但是在错误1的加持下,这个问题被掩盖了,我花了几个小时在错误的方法上面,最后一无所获。无奈之下改为使用
onLayout()
方法,最终从肉眼视觉上达到想要的效果。这个方案相比直接在加载View
的时候按需配置View树
显然会差一些,不论是感觉上还是性能上均如此。
好消息是:次日我不甘心就这么算了,再次尝试
addView()
方法,终于给我发现了上述的2个坑,从而成功的使用addView()
方法做到了想要的效果。
这里说说TextInputLayout
给我的2个重要启发:
-
addView()
方法在控件从xml布局文件
转化为View
过程中发挥的作用 -
setAddStatesFromChildren()
方法
我们先看 addView()
方法,setAddStatesFromChildren()
方法会在后面进行讲解。
addView() 方法
这里我重写的是带有3个参数的 addView(View child, int index, LayoutParams params)
,因为我需要用到第3个参数,各参数分别表示:
-
child
将要add进来的View
-
index
child
将被add到的position
,-1表示add到最后 -
params
将在child
上设置的LayoutParams
参数(其实就是包含child
在xml布局文件
中配置的属性的LayoutParams
)
关于 addView()
方法在UI创建过程中的作用大概看了一下源码和网上的解析文章,有了个粗略的了解。
简单来说就是: 在Android系统解析 xml布局文件 转换成
View
的过程中,会调用当前正在解析的ViewGroup
中的addView()
方法,把 xml布局文件中该ViewGroup
包含的View
或ViewGroup
一个个的 add 进来。
接下来进入到代码解析阶段:
[图片上传失败…(image-9eb6a8-1628150391084)]
1. 先看看构造方法:
public ColorTextInputLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr, 0);
setOrientation(VERTICAL);
setWillNotDraw(true);
mTvHint = new TextView(context);
mTvHint.setTag(TAG_HINT);
mFlInputPanel = new FrameLayout(context);
mFlInputPanel.setTag(TAG_PANEL);
mFlInputPanel.setAddStatesFromChildren(true);
mFlInputPanel.setBackgroundResource(R.drawable.selector_color_hint_panel);
mIvIndicator = new ImageView(context);
FrameLayout.LayoutParams indicatorLp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.MATCH_PARENT);
mFlInputPanel.addView(mIvIndicator, 0, indicatorLp);
LinearLayout.LayoutParams rootLp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
addView(mTvHint, 0, rootLp);
addView(mFlInputPanel, 1, rootLp);
… 省略掉了OnGlobalFocusChangeListener监听代码
}
代码解析:
这部分是 ColorTextInputLayout
的构造方法,我在 ColorTextInputLayout
的构造方法中 new TextView
(提示文本 mTvHint
) 和 FrameLayout
(输入面板 mFlInputPanel
) 时分别给它们setTag()
,以便在 addView()
方法中把它们与xml布局文件
中的View
区分开来。同时 new ImageView
(状态指示器 mIvIndicator
),并add到 mFlInputPanel
中。最后,把 mTvHint
和 mFlInputPanel
add到 ColorTextInputLayout
中。
这里需要注意 mFlInputPanel.setAddStatesFromChildren(true)
这一行,这里我先不说它的作用,留到后面另一个使用到的地方再一起说。
2. 再来看看重写的 addView()
方法
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
String tag = (String) child.getTag();
// 提示和输入面板add到本ViewGroup,其他的add到容器中
if (TextUtils.equals(tag, TAG_HINT) || TextUtils.equals(tag, TAG_PANEL)) {
super.addView(child, index, params);
} else {
// 输入面板当前已经add了图标指示器,最多只能再add 1个子控件
if (mFlInputPanel.getChildCount() > 1) {
throw new IllegalStateException(“ColorTextInputLayout can host only one child”);
}
FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(params);
MarginLayoutParams marginLp = (MarginLayoutParams) params;
flp.leftMargin = marginLp.leftMargin == 0 ? marginLp.getMarginStart() : marginLp.leftMargin;
flp.rightMargin = marginLp.rightMargin == 0 ? marginLp.getMarginEnd() : marginLp.rightMargin;
flp.topMargin = marginLp.topMargin;
flp.bottomMargin = marginLp.bottomMargin;
mFlInputPanel.addView(child, flp);
if (child instanceof EditText) {
setEditText((EditText) child);
} else if (child instanceof ViewGroup) {
EditText edt = getEditTextFromViewGroup((ViewGroup) child);
if (edt == null) {
throw new IllegalStateException(“The ViewGroup in ColorTextInputLayout must have one EditText”);
}
setEditText(edt);
} else {
throw new IllegalStateException(“ColorTextInputLayout can host only an EditText or a ViewGroup containing EditText”);
}
}
}
代码解析:
if 分支表示这2个控件依然按照 ColorTextInputLayout
的父类的 addView()
方法添加到 ColorTextInputLayout
中;else 分支不用我说你们也都能猜到了,是的,这就是把 xml布局文件
中包裹在 ColorTextInputLayout
下的子View
添加到输入面板(mFlInputPanel
)的代码,这里说一下:
if (mFlInputPanel.getChildCount() > 1)
是用来限定xml布局文件
中只能包含一个child
,类似ScrollView
。为什么是大于1而不是大于0呢?因为mIvIndicator
已经在构造方法中add到mFlInputPanel
里了。
这个限定的作用是什么? 分析了项目可能的使用情况,最简单的使用情况就只包含一个
EditText
,另外的情况就是包含多个控件。但是这里我并没有办法知到使用多个控件时到底想要什么样的摆放方式,甚至可能不同的地方需要不同的摆放方式,此外多个控件的摆放过于复杂,综合以上因素,我决定效仿ScrollView
:限定只允许包含1个child
。要么仅包含一个EditText
;要么包含一个包含EditText
的ViewGroup
,ViewGroup
内部的布局方式我不关心。
-
接着往下看,到
mFlInputPanel.addView(child, flp)
为止,这部分就是从子View
配置在xml布局文件
中的params
中取出marinXXX
属性,然后按照这些属性重新把子View
add到mFlInputPanel
中。 -
再往下直到结束就是取
EditText
的过程了:若xml布局文件中,ColorTextInputLayout
当前包裹的是EditText
则直接setEditText()
;若包裹的是ViewGroup
,先通过getEditTextFromViewGroup()
方法取出ViewGroup
中的EditText
,再setEditText()
;如果以上2种条件都不满足,则抛出异常,提示xml布局文件
中包裹的子View
类型错误。setEditText()
就是一个将View
赋值给全局变量mEditText
的方法,这里不多说。
3. 下面看看getEditTextFromViewGroup() 方法
/**
-
使用递归来遍历View树,从ViewGroup中取出EditText(强烈建议只包含一个EditText)
-
@param viewGroup
-
@return 从ViewGroup中取出的EditText,若ViewGroup包含多个EditText,将始终只返回取到的第一个;
*/
private EditText getEditTextFromViewGroup(ViewGroup viewGroup) {
// 必须给ViewGroup添加此方法,否则输入面板设置的此方法不会生效
viewGroup.setAddStatesFromChildren(true);
EditText editText = null;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = viewGroup.getChildAt(i);
// EditText越靠前,在View树中同级靠后的就不用处理
if (!(child instanceof ViewGroup)) {
if (child instanceof EditText) {
editText = (EditText) child;
break;
}
} else {
editText = getEditTextFromViewGroup((ViewGroup) child);
}
}
return editText;
}
代码解析:
总体上是通过递归的方式从 ViewGroup
中取出 EditText
。这里有2个点要提一下:
1.在
ViewGroup
中,EditText
尽可能的放到前面
在多层 ViewGroup
嵌套的情况下,假定View树
上同级的View
总数不变,EditText
越靠前,与它同级但比它靠后的View
就越多,这些View
不用再处理,理论上能提升一部分性能。当然,这是我这个递归方法不完美导致的。理论上最优方法就是从根节点一层一层的往叶子节点找,而不是找到一个 ViewGroup
就进入。算法学的不太好,这里后期再优化吧。
- 必须给
ViewGroup
添加此方法,否则构造方法中mFlInputPanel
设置的此方法不会生效。
项目实施 中我提到 TextInputLayout
给我 2个启发, setAddStatesFromChildren()
就是另一个,它的作用如下: 它将设置父View
与子View
的背景联动,实质就是在构建 ViewGroup
的 drawableState
时,会将子View
的所有 drawableState
合并在一起交给父View
,并在子View
刷新drawable
时通知父View
。
什么意思呢?就是子View
的 drawableState
发生变化时,ViewGroup
也会同步到此 drawableState
状态。
[图片上传失败…(image-588c35-1628150391082)]
以我现在的需求来说,我需要在 EditText
获取到焦点时,将 mFlInputPanel
的背景图设置为获取焦点的状态图。按尝龟
[图片上传失败…(image-8dbc86-1628150391082)]
啊不,常规做法就是:对 EditText
设置焦点监听事件,在焦点变化时更换 mFlInputPanel
背景图。但是这样一来很繁琐;另外如果 ColorTextInputLayout
外部也需要监听 EditText
焦点状态,二者就会冲突了。
而有了 setAddStatesFromChildren()
方法以后,一切都简单了:mFlInputPanel
首先设置一个 drawable
类型的 selector_xx.xml
背景,然后调用 setAddStatesFromChildren()
方法。这样,在 EditText
的 drawableState
状态(包括焦点状态)变化时,mFlInputPanel
将会收到通知,自动选择 selector_xx.xml
中对应状态的背景。
需要注意的是,
setDuplicateParentStateEnabled()
方法与setAddStatesFromChildren()
刚好相反,二者不可以一起使用,否则可能引起崩溃。 此外,经过我自己的使用发现,多层嵌套时,如果响应的ViewGroup
与 想监听的View
之间还有嵌套的ViewGroup
,那么需要在每一层ViewGroup
都调用setDuplicateParentStateEnabled()
方法,响应的ViewGroup
才会生效。所以,在getEditTextFromViewGroup()
方法开头我也调用了setDuplicateParentStateEnabled()
方法。
尾声
==
到这里,本次自定义ViewGroup
的xml布局文件 + addView()
混合添加View的使用就讲完了,感谢阅读,有什么不对的地方也请指正。如有转载需要,请标明出处,谢谢。