文化袁探索专栏——Activity、Window和View三者间关系
文化袁探索专栏——View三大流程#Measure
文化袁探索专栏——View三大流程#Layout
文化袁探索专栏——消息分发机制
文化袁探索专栏——事件分发机制
文化袁探索专栏——Launcher进程启动流程’VS’APP进程启动流程
文化袁探索专栏——Activity启动流程
文化袁探索专栏——自定义View实现细节
文化袁探索专栏——线程池执行原理|线程复用|线程回收
文化袁探索专栏——React Native启动流程
这里介绍以继承布局实现方式,来探索自定义View的实现细节 ~
文件attrs.xml-属性字段定义 | format |
---|---|
枚举类型 | enum |
引用类型/参考某资源id | refrence |
font-size、view-measure | dimension |
颜色类型 | color |
布尔类型 | boolean |
单精度浮点类型 | float |
整型 | Integer |
百分数 | fraction |
自定义顶部导航:继承RelativeLayout实现
自定义通用的页面顶部导航组件。左侧按钮以返回按钮为主,右侧至少会有两个按钮(更更多、分享)。在顶部导航栏中间位置则是标题,包含副标题,标题字数超过一定宽度以末尾省略结束。
关键代码:如何定义View样式属性、如何对已定义的样式属性进行解析
定义View样式属性
定义View样式属性,需要依据需求明确自定义属性的字段,如按钮的大小、颜色,主副标题大小等。并为该组件通用性定义其默认属性样式的配置。
<!-- attrs.xml,自定义View属性集合 -->
<declare-styleable name="UINavigationBar">
<!--顶部导航的背景定义 android:navBackground = "@drawable/图片ID"-->
<atty name="navBackground" format="refrence">
<!--按钮的大小颜色,使用iconfont-->
<attr name="text_btn_text_size" format="dimension" />
<attr name="text_btn_text_color" format="color" />
<!--标题大小颜色,出现副标题时主标题的大小-->
<attr name="title_text_size" format="dimension" />
<attr name="title_text_size_with_subTitle" format="dimension" />
<attr name="title_text_color" format="color" />
<!--副标题大小颜色-->
<attr name="subTitle_text_size" format="dimension" />
<attr name="subTitle_text_color" format="color" />
<!--按钮的横向内间距-->
<attr name="hor_padding" format="dimension"/>
<!--返回按钮iconfont文本 主副标题文本-->
<attr name="nav_icon" format="string" />
<attr name="nav_title" format="string" />
<attr name="nav_subtitle" format="string" />
</declare-styleable>
<!-- 用作默认属性集合配置-->
<style name="defNavigationStyle">
<item name="hor_padding">8dp</item>
<item name="nav_icon"></item>
<item name="text_btn_text_size">16sp</item>
<item name="text_btn_text_color">#666666</item>
<item name="title_text_size">18sp</item>
<item name="title_text_color">#000000</item>
<item name="subTitle_text_size">14sp</item>
<item name="title_text_size_with_subTitle">16sp</item>
<item name="subTitle_text_color">#717882</item>
</style>
解析已定义的样式属性
// UINavigationHeader.kt
/**抽取几行具有代表性代码,介绍如何使用自定义属性*/
// 获取样式属性信息的集合;若没有对所自定义属性配置对应值。
// 则会使用默认的属性集合defNavigationStyle
val array = context.obtainStyledAttributes(
attrs,
R.styleable.UINavigationBar,
defStyleAttr,
R.style.defNavigationStyle
)
// 获取样式属性集合中单个字符串类型样式值
val navIcon = array.getString(R.styleable.UINavigationBar_nav_icon)
// 获取样式自定义属性集合中(或默认)颜色值,且有默认颜色值
val navIconColor = array.getColor(R.styleable.UINavigationBar_nav_icon_color, Color.BLACK)
// 或者获取自定义样式属性中(或默认)颜色值,返回的是ColorStateList
// 这里使用的目的是配合Button点击效果的文字颜色变化
val btnTextColor = array.getColorStateList(R.styleable.UINavigationBar_text_btn_text_color)
// 获取配置的自定义属性(或默认)-title的字体大小且设置默认尺寸
val titleTextSize = array.getDimensionPixelSize(R.styleable.UINavigationBar_title_text_size, applyUnit(
TypedValue.COMPLEX_UNIT_SP, 16f))
// 获取配置的自定属性(或默认)-分割线高度
val lineHeight = array.getDimensionPixelOffset(R.styleable.UINavigationBar_nav_line_height, 0)
在这里主要介绍在Attrs.xml文件中,
<declare-styleable/>
和<style />
的使用方式;以及如何设置默认的样式。
上述attrs.xml文件中在定义样式属性时,属性字段的类型标志
format
分别有这么几个属性类型
- dimension - {一般用来表示字体尺寸、layout宽高大小}
- color - {用来表示颜色类型}
- string - {用来表示字符串}
- reference -{用来表示引用类型/参考某资源ID}
自定义顶部导航的核心代码
在左右两侧添加按钮(View)时,关键判断逻辑是如何得知左右两侧是否已经添加过按钮。从而能确定当前将要添加的按钮(View)落到哪个位置,并据此设定样式。如何得知左右两侧是否已经添加过?通过分别定义两个View集合【mLeftViewList、mRightViewList】关联左右两侧按钮的添加状态。
核心代码中,navAttrs对象属于NavAttr类型(定义的内部类)的对象实例,为封装已解析的样式属性。
// UINavigationHeader.kt
/**添加导航栏左侧按钮*/
private fun addLeftTextButton(@StringRes stringRes: Int, viewId: Int): Button {
return addLeftTextButton(resources.getString(stringRes), viewId)
}
private fun addLeftTextButton(navIconStr: String?, viewId: Int): Button {
val button:Button = genTextButton()
button.text = navIconStr
button.id = viewId
if (mLeftViewList.isEmpty()) {//判断集合中右侧按钮没有则说明当前按钮是第一个被添加
//然后设置相应的padding距离
button.setPadding(navAttrs.horPadding*2,0, navAttrs.horPadding, 0)
} else {//若已有添加按钮,则当前按钮从右到左排列并色荷治相应padding距离
button.setPadding(navAttrs.horPadding,0, navAttrs.horPadding, 0)
}
addLeftView(button, genTextButtonLayoutParams())//添加左侧按钮到当前RelativeLayout
return button
}
private fun addLeftView(view: View, params:LayoutParams) {
val viewId = view.id
if (viewId == View.NO_ID) {
throw IllegalStateException("左侧view必须设置id")
}
if (mLeftLastViewId == View.NO_ID) {
params.addRule(ALIGN_PARENT_LEFT, viewId)//落在父布局左侧靠边
} else {
params.addRule(RIGHT_OF, mLeftLastViewId)//落在以mLeftLastViewId为锚点,在mLeftLastViewId的右侧
}
mLeftLastViewId = viewId
params.alignWithParent = true //alignParentIfMissing 自动靠边排列
mLeftViewList.add(view)
addView(view, params)
}
// UINvigationHeader.kt
/**添加导航栏右侧按钮关键代码*/
private fun addRightTextButton(@StringRes stringRes: Int, viewId: Int):Button {
return addRightTextButton(resources.getString(stringRes), viewId)
}
private fun addRightTextButton(btnText: String, viewId: Int):Button {
val button:Button = genTextButton()
button.text = btnText
button.id = viewId
if (mRightViewList.isEmpty()) {//判断集合中右侧按钮没有则说明当前按钮是第一个被添加
//然后设置相应的padding距离
button.setPadding(navAttrs.horPadding,0,navAttrs.horPadding*2,0)
} else {//若已有添加按钮,则当前按钮从右到左排列并色荷治相应padding距离
button.setPadding(navAttrs.horPadding,0,navAttrs.horPadding,0)
}
addRightView(button, genTextButtonLayoutParams())
return button
}
private fun addRightView(
button: Button,
params: LayoutParams
) {
val viewId = button.id
if (viewId == View.NO_ID) {
throw IllegalStateException("右侧view必须设置id")
}
if (mRightLastViewId == View.NO_ID) {//落在父布局贴边靠右侧
params.addRule(ALIGN_PARENT_RIGHT, viewId)
} else {//RelativeLayout.LEFT_OF以mLeftLastViewId为锚点,落在mLeftLastViewId的左侧
params.addRule(LEFT_OF, mLeftLastViewId)
}
mLeftLastViewId = viewId
params.alignWithParent = true //alignParentIfMissing
mRightViewList.add(button)
addView(button, params)
}
标题居中核心逻辑
促使标题始终居中,重写当前RelativeLayout的onMeasure方法。以左右两侧按钮所占据的宽度,计算出中间标题所占据空间val centerSpace = this.measuredWidth - Math.max(leftUsedSpace,rightUsedSpace)*2
,根据centerSpace
得到新的new_widthMeasureSpec(测量规则)
重新测量标题父布局(titleContainer)。
// UINavigationHeader.kt
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if(null == titleContainer) return
// 计算左侧按钮,占据的空间
var leftUsedSpace = paddingLeft+marginLeft
for(leftView in mLeftViewList) {
leftUsedSpace+=leftView.measuredWidth
}
// 计算右侧按钮,占据的空间
var rightUsedSpace = paddingRight+marginRight
for(rightView in mRightViewList) {
rightUsedSpace+=rightView.measuredWidth
}
// 使得 标题能够居中的宽度 (导航栏宽度 - 左右两侧最宽View宽度尺寸的两倍)
val centerSpace = this.measuredWidth - Math.max(leftUsedSpace,rightUsedSpace)*2
if (centerSpace < titleContainer!!.measuredWidth) {
val new_widthMeasureSpec= MeasureSpec.makeMeasureSpec(centerSpace, MeasureSpec.EXACTLY)
titleContainer!!.measure(new_widthMeasureSpec, heightMeasureSpec)
}
}
自定义输入框:继承LinearLayout实现
组合View,自定义出上截图中效果。LinearLayout(TextView+EditText) ~
自定义View实现登录、注册等输入框的公共使用View。定义View样式属性,依据需求明确自定义属性的字段,如组件标题、输入框输入内容格式、输入框样式等定义属性文件xml。
<!--attrs.xml-->
<declare-styleable name="InputItemLayout">
<attr name="hint" format="string"></attr>
<!-- 输入框标题 -->
<attr name="title" format="string"></attr>
<!-- app:inputType="text|password|number" 输入框输入内容格式-->
<attr name="inputType" format="enum">
<enum name="text" value="0"/>
<enum name="password" value="1"/>
<enum name="number" value="2"/>
</attr>
<!-- app:inputTextAppearance="@style/inputTextAppearance" -->
<attr name="inputTextAppearance" format="reference"></attr>
<attr name="titleTextAppearance" format="reference"></attr>
<attr name="topLineAppearance" format="reference"></attr>
<attr name="bottomLineAppearance" format="reference"></attr>
</declare-styleable>
<!--输入框内容输入字体、默认提示字体,颜色、大小-->
<declare-styleable name="inputTextAppearance">
<attr name="hintColor" format="color"/>
<attr name="inputColor" format="color"/>
<attr name="textSize" format="dimension"/>
</declare-styleable>
<!--输入框标题字体、颜色、大小-->
<declare-styleable name="titleTextAppearance">
<attr name="titleColor" format="color"/>
<attr name="titleSize" format="dimension"/>
<attr name="minWidth" format="dimension"/>
</declare-styleable>
<!--输入框底部分割线样式-->
<declare-styleable name="lineAppearance">
<attr name="color" format="color"/>
<attr name="height" format="dimension"/>
<attr name="leftMargin" format="dimension"/>
<attr name="rightMargin" format="dimension"/>
<attr name="enable" format="boolean"/>
</declare-styleable>
上述attrs.xml文件中在定义样式属性时,属性字段的类型标志
format
分别有这么几个属性类型
- dimension - {一般用来表示字体尺寸、layout宽高大小}
- color - {用来表示颜色类型}
- string - {用来表示字符串}
- boolean -{用来表示布尔类型}
- enum -{用来表示枚举}
- reference -{用来表示引用类型/参考某资源ID}
定义样式时同样使用标签<declare-styleable/>
,这里应该多关注enum(枚举)和refrence(引用类型)。举例说明:
refrence(引用类型)
自定义属性文件解析第一步,获取到样式属性集合的实例~
val array = context.obtainStyledAttributes(attributeSet, R.styleable.InputItemLayout)
且在R.styleable.InputItemLayout
样式集合中已定义有四个引用类型。
<attr name="inputTextAppearance" format="reference"></attr>
<attr name="titleTextAppearance" format="reference"></attr>
<attr name="topLineAppearance" format="reference"></attr>
<attr name="bottomLineAppearance" format="reference"></attr>
那么如何从R.styleable.InputItemLayout
引用属性集合列表中获取titleTextAppearance
集合的实例?val titleStyleId = array.getResourceId 结合
val titleArray = context.obtainStyledAttributes(titleStyleId, R.styleable.titleTextAppearance)
// InputItemLayout.kt
// 解析 <declare-styleable name="titleTextAppearance"> 中的属性
// getResourceId 获取引用属性集合实例的方法
val titleStyleId = array.getResourceId(R.styleable.InputItemLayout_titleTextAppearance, 0)
val title = array.getString(R.styleable.InputItemLayout_title)
parseTitleStyle(titleStyleId, title)
private fun parseTitleStyle(titleStyleId: Int, title: String?) {
// obtainStyledAttributes 获取属性集合实例的方法
val array = context.obtainStyledAttributes(titleStyleId, R.styleable.titleTextAppearance)
val titleColor = array.getColor(
R.styleable.titleTextAppearance_titleColor,
resources.getColor(R.color.color_565)
)
//px // 获取标题文字的大小尺寸
val titleSize = array.getDimensionPixelSize(
R.styleable.titleTextAppearance_titleSize,
applyUnit(TypedValue.COMPLEX_UNIT_SP, 15f)
)
// 获取标题文字的宽度尺寸
val minWidth = array.getDimensionPixelOffset(R.styleable.titleTextAppearance_minWidth, 0)
titleView = TextView(context)
titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize.toFloat()) //sp---当做sp在转换一次
titleView.setTextColor(titleColor)
titleView.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
titleView.minWidth = minWidth
titleView.gravity = Gravity.LEFT or (Gravity.CENTER)
titleView.text = title
addView(titleView)
array.recycle() // 资源回收
}
enum(枚举)
使用时,枚举类型inputType在布局文件中定义为app:inputType="text|password|number"
解析时,通过array.getInteger(R.styleable.InputItemLayout_inputType, 0)获取使用时定义的枚举值,并对editText.inputType在执行逻辑上设置对应文本格式。
// InputItemLayout.kt
// 解析 <declare-styleable name="inputTextAppearance"> 中的属性
val inputStyleId = array.getResourceId(R.styleable.InputItemLayout_inputTextAppearance, 0)
val hint = array.getString(R.styleable.InputItemLayout_hint)
val inputType = array.getInteger(R.styleable.InputItemLayout_inputType, 0)
parseInputStyle(inputStyleId, hint, inputType)
private fun parseInputStyle(inputStyleId: Int, hint: String?, inputType: Int) {
val typeArray =
context.obtainStyledAttributes(inputStyleId, R.styleable.inputTextAppearance)
val hintColor = typeArray.getColor(R.styleable.inputTextAppearance_hintColor, resources.getColor(R.color.color_d1d2))
val inputColor = typeArray.getColor(R.styleable.inputTextAppearance_inputColor, resources.getColor(R.color.color_565))
val textSize = typeArray.getDimensionPixelSize(R.styleable.inputTextAppearance_textSize, applyUnit(TypedValue.COMPLEX_UNIT_SP, 14f))
editText = EditText(context)
val params = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
params.weight = 1f
editText.layoutParams = params
editText.setHintTextColor(hintColor)
editText.setHint(hint)
editText.setTextColor(inputColor)
editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize.toFloat())
editText.setBackgroundColor(Color.TRANSPARENT)
editText.gravity = Gravity.LEFT or Gravity.CENTER
if (inputType == 0) {
editText.inputType = InputType.TYPE_CLASS_TEXT // 文本格式
} else if (inputType == 1) {
editText.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD or (InputType.TYPE_CLASS_TEXT) // 密码
} else if (inputType == 2) {
editText.inputType = InputType.TYPE_CLASS_NUMBER // 数字
}
addView(editText)
typeArray.recycle() // 资源回收
}
// 绘制输入框下的分割线
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (topLine.enable) {
canvas?.drawLine(topLine.leftMargin.toFloat(), 0f, (measuredWidth - topLine.rightMargin).toFloat(), 0f, topPaint)
}
if (bottomLine.enable) {
canvas?.drawLine(bottomLine.leftMargin.toFloat(), height - bottomLine.height.toFloat(), (measuredWidth - bottomLine.rightMargin).toFloat(), height - bottomLine.height.toFloat(), bottomPaint)
}
}