文化袁探索专栏——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样式属性,需要依据需求明确自定义属性的字段,如按钮的大小、颜色,主副标题大小等。并为该组件通用性定义其默认属性样式的配置。
<declare-styleable name="UINavigationBar">
<atty name="navBackground" format="refrence">
<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"/>
<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>
解析已定义的样式属性
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)
val btnTextColor = array.getColorStateList(R.styleable.UINavigationBar_text_btn_text_color)
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类型(定义的内部类)的对象实例,为封装已解析的样式属性。
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()) {
button.setPadding(navAttrs.horPadding*2,0, navAttrs.horPadding, 0)
} else {
button.setPadding(navAttrs.horPadding,0, navAttrs.horPadding, 0)
}
addLeftView(button, genTextButtonLayoutParams())
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 = viewId
params.alignWithParent = true
mLeftViewList.add(view)
addView(view, params)
}
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()) {
button.setPadding(navAttrs.horPadding,0,navAttrs.horPadding*2,0)
} else {
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 {
params.addRule(LEFT_OF, mLeftLastViewId)
}
mLeftLastViewId = viewId
params.alignWithParent = true
mRightViewList.add(button)
addView(button, params)
}
标题居中核心逻辑 促使标题始终居中,重写当前RelativeLayout的onMeasure方法。以左右两侧按钮所占据的宽度,计算出中间标题所占据空间val centerSpace = this.measuredWidth - Math.max(leftUsedSpace,rightUsedSpace)*2 ,根据centerSpace 得到新的new_widthMeasureSpec(测量规则) 重新测量标题父布局(titleContainer)。
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
}
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)
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?) {
val array = context.obtainStyledAttributes(titleStyleId, R.styleable.titleTextAppearance)
val titleColor = array.getColor(
R.styleable.titleTextAppearance_titleColor,
resources.getColor(R.color.color_565)
)
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())
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在执行逻辑上设置对应文本格式。
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)
}
}
|