前言:
自定义View可以分为两种方式:
- 第一种通过继承ViewGroup,内部通过addView的方式将其他的View组合到一起。
- 第二种则是通过继承View,重启View的onMeasure,onLayout,onDraw方法来绘制不规则图形,如折线图等。
本文介绍的是第一种方式通过组合的方式去实现自定义View。
实现自定义View首先要自定义属性。对于自定义属性,第一步是在项目res/values文件夹中新建attrs.xml文件,在文件中设置自定义属性的名称和类型,
代码如下:
<resources> <declare-styleable name="InputItemLayout"> <attr name="hint" format="string"></attr> <attr name="title" format="string"/> <attr name="inputType" format="enum"> <enum name="text" value="0"/> <enum name="password" value="1"/> <enum name="number" value="2"/> </attr> <attr name="inputTextAppearance" format="reference"/> <attr name="titleTextAppearance" format="reference"/> <attr name="topLineAppearance" format="reference"/> <attr name="bottomLineAppearance" format="reference"/> </declare-styleable> <declare-styleable name="inputTextAppearance"> <attr name="hintColor" format="color" /> <attr name="inputColor" format="color" /> <attr name="textSize" format="dimension" /> <attr name="maxInputLength" format="integer" /> </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> </resources>
自定义属性都需要包裹在declare-styleable标签中,name属性标志这个属性集合的名字,其中的attr标志属性。对于自定义属性的类型,主要的有以下几种
string字符串类型 reference引用类型,一般是指向另外的一个资源属性 color颜色代码 dimension尺寸 float浮点型 boolean布尔型 integer整型 enum枚举型
当你定义完上面的文件,接下来我们需要在自定义View中解析它们,从而获得用户传递进来的属性。 属性的解析可以使用以下代码完成
val array = context.obtainStyledAttributes(attributeSet, R.styleable.InputItemLayout) val title = array.getString(R.styleable.InputItemLayout_title) val titleResId = array.getResourceId(R.styleable.InputItemLayout_titleTextAppearance, 0)
上面的代码中,第一句是通过obtainStyledAttributes解析上面XML文件中属性名为InputItemLayout的属性内容,并返回TypedArray,后续该命名空间中的所有属性都可以通过TypedArray.getXX()来获得XX是属性类型。
但是引用类型除外,因为引用类型中还包含了其他属性,所以需要如下代码去提取属性。
val array1 = context.obtainStyledAttributes(attributeSet, R.styleable.InputItemLayout) val titleResId = array1.getResourceId(R.styleable.InputItemLayout_titleTextAppearance, 0) val array = context.obtainStyledAttributes(titleResId, R.styleable.titleTextAppearance) val titleColor = array.getColor( R.styleable.titleTextAppearance_titleColor, resources.getColor(R.color.color_565) )
如上代码,我们先获取在InputItemLayout属性中titleTextAppearance的属性,这时候发现titleTextAppearance是一个引用类型的属性,在使用 context.obtainStyledAttributes(titleResId, R.styleable.titleTextAppearance)获取titleTextAppearance中的属性值,第一个参数titleResId是titleTextAppearance的资源ID。 最终我们获取了所有的属性,这时候就可以开始自定义你的View了。
当我们最终完成了所有的代码,怎么在布局文件中使用呢。对于普通的属性,如String Int等就和平常一样,但是对于引用类型,我们需要在style.xml文件中定义资源文件
<com.slowtd.tcommon.InputItemLayout android:layout_marginTop="10dp" android:layout_width="match_parent" android:layout_height="55dp" app:hint="请输入密码" app:title="密码" app:inputType="text" app:titleTextAppearance="@style/titleTextAppearance" app:inputTextAppearance="@style/inputTextAppearance_limitLength" app:topLineAppearance="@style/lineAppearance" app:bottomLineAppearance="@style/lineAppearance" />
<style name="inputTextAppearance"> <item name="hintColor">@color/color_C1B</item> <item name="inputColor">@color/color_565</item> <item name="textSize">15sp</item> </style> <style name="inputTextAppearance_limitLength" parent="inputTextAppearance"> <item name="maxInputLength">4</item> </style> <style name="titleTextAppearance"> <item name="titleColor">@color/color_565</item> <item name="titleSize">15sp</item> <item name="minWidth">100dp</item> </style> <style name="lineAppearance"> <item name="color">@color/black</item> <item name="height">2dp</item> <item name="leftMargin">0dp</item> <item name="rightMargin">0dp</item> <item name="enable">true</item> </style>
下面的代码是一个简单的自定义输入框代码,供大家参考,配合上面的XML属性资源就可以使用了。
class InputItemLayout : LinearLayout { private lateinit var titleView: TextView private lateinit var editText: EditText private var bottomLine: Line private var topLine: Line private var topPaint = Paint(Paint.ANTI_ALIAS_FLAG) private var bottomPaint = Paint(Paint.ANTI_ALIAS_FLAG) constructor(context: Context) : this(context, null) constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0) constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super( context, attributeSet, defStyle ) { dividerDrawable = ColorDrawable() showDividers = SHOW_DIVIDER_BEGINNING //去加载 去读取 自定义sytle属性 orientation = HORIZONTAL val array = context.obtainStyledAttributes(attributeSet, R.styleable.InputItemLayout) //解析title 属性 val title = array.getString(R.styleable.InputItemLayout_title) val titleResId = array.getResourceId(R.styleable.InputItemLayout_titleTextAppearance, 0) parseTitleStyle(title, titleResId) //解析右侧的输入框属性 val hint = array.getString(R.styleable.InputItemLayout_hint) val inputResId = array.getResourceId(R.styleable.InputItemLayout_inputTextAppearance, 0) val inputType = array.getInteger(R.styleable.InputItemLayout_inputType, 0) parseInputStyle(hint, inputResId, inputType) //上下分割线属性 val topResId = array.getResourceId(R.styleable.InputItemLayout_topLineAppearance, 0) val bottomResId = array.getResourceId(R.styleable.InputItemLayout_bottomLineAppearance, 0) topLine = parseLineStyle(topResId) bottomLine = parseLineStyle(bottomResId) if (topLine.enable) { topPaint.color = topLine.color topPaint.style = Paint.Style.FILL_AND_STROKE topPaint.strokeWidth = topLine.height } if (bottomLine.enable) { bottomPaint.color = bottomLine.color bottomPaint.style = Paint.Style.FILL_AND_STROKE bottomPaint.strokeWidth = bottomLine.height } array.recycle() } @SuppressLint("CustomViewStyleable") private fun parseLineStyle(resId: Int): Line { val line = Line() val array = context.obtainStyledAttributes(resId, R.styleable.lineAppearance) line.color = array.getColor( R.styleable.lineAppearance_color, ContextCompat.getColor(context, R.color.color_d1d2) ) line.height = array.getDimensionPixelOffset(R.styleable.lineAppearance_height, 0).toFloat() line.leftMargin = array.getDimensionPixelOffset(R.styleable.lineAppearance_leftMargin, 0).toFloat() line.rightMargin = array.getDimensionPixelOffset(R.styleable.lineAppearance_rightMargin, 0).toFloat() line.enable = array.getBoolean(R.styleable.lineAppearance_enable, false) array.recycle() return line } inner class Line { var color = 0 var height = 0f var leftMargin = 0f var rightMargin = 0f; var enable: Boolean = false } @SuppressLint("CustomViewStyleable") private fun parseInputStyle(hint: String?, resId: Int, inputType: Int) { val array = context.obtainStyledAttributes(resId, R.styleable.inputTextAppearance) val hintColor = array.getColor( R.styleable.inputTextAppearance_hintColor, ContextCompat.getColor(context, R.color.color_d1d2) ) val inputColor = array.getColor( R.styleable.inputTextAppearance_inputColor, ContextCompat.getColor(context, R.color.color_565) ) //px val textSize = array.getDimensionPixelSize( R.styleable.inputTextAppearance_textSize, applyUnit(TypedValue.COMPLEX_UNIT_SP, 15f) ) val maxInputLength = array.getInteger(R.styleable.inputTextAppearance_maxInputLength, 0) editText = EditText(context) if (maxInputLength > 0) { editText.filters = arrayOf(InputFilter.LengthFilter(maxInputLength))//最多可输入的字符数 } editText.setPadding(0, 0, 0, 0) val params = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT) params.weight = 1f editText.layoutParams = params editText.hint = hint editText.setTextColor(inputColor) editText.setHintTextColor(hintColor) editText.gravity = LEFT or (CENTER) editText.setBackgroundColor(Color.TRANSPARENT) editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize.toFloat()) /** * <enum name="text" value="0"></enum> * <enum name="password" value="1"></enum> * <enum name="number" value="2"></enum> */ 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) array.recycle() } @SuppressLint("CustomViewStyleable") private fun parseTitleStyle(title: String?, resId: Int) { val array = context.obtainStyledAttributes(resId, 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 = LEFT or (CENTER) titleView.text = title addView(titleView) array.recycle() } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) //巨坑 if (topLine.enable) { canvas!!.drawLine( topLine.leftMargin, 0f, measuredWidth - topLine.rightMargin, 0f, topPaint ) } if (bottomLine.enable) { canvas!!.drawLine( bottomLine.leftMargin, height - bottomLine.height, measuredWidth - bottomLine.rightMargin, height - bottomLine.height, bottomPaint ) } } private fun applyUnit(applyUnit: Int, value: Float): Int { return TypedValue.applyDimension(applyUnit, value, resources.displayMetrics).toInt() } fun getTitleView(): TextView { return titleView } fun getEditText(): EditText { return editText } }