文化袁探索专栏——自定义View实现细节

文化袁探索专栏——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">&#xe607;</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)
        }
}

猜你喜欢

转载自blog.csdn.net/u012827205/article/details/123280228
今日推荐