一、概念
二、步骤
2.1 布局文件
2.1.1 创建组合已有控件的布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:id="@+id/tvMinus"
android:layout_width="80dp"
android:layout_height="match_parent"
android:textSize="18sp"
android:gravity="center"
android:text="-"
/>
<EditText
android:id="@+id/etValue"
android:layout_width="80dp"
android:layout_height="match_parent"
android:focusable="false"
android:gravity="center"
android:text="0"
android:textSize="14sp"
/>
<TextView
android:id="@+id/tvPlus"
android:layout_width="80dp"
android:layout_height="match_parent"
android:textSize="18sp"
android:gravity="center"
android:text="+"
/>
</LinearLayout>
2.1.2 创建自定义类继承已有的容器布局类
class Counter : LinearLayout {
//通过 this 调用到三参构造中进行统一处理
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
//TODO...
}
}
2.1.3 加载组合好的布局文件并初始化控件
private lateinit var tvPlus: TextView
private lateinit var tvMinus: TextView
private lateinit var etValue: EditText
//初始化控件
private fun initView(context: Context?) {
if (context != null) {
val view = LayoutInflater.from(context).inflate(R.layout.layout_counter, this)
tvPlus = view.findViewById(R.id.tvPlus)
tvMinus = view.findViewById(R.id.tvMinus)
etValue = view.findViewById(R.id.etValue)
}
//简写
// context?.let {
// val view = LayoutInflater.from(context).inflate(R.layout.layout_counter, this).run {
// tvPlus = findViewById(R.id.tvPlus)
// tvMinus = findViewById(R.id.tvMinus)
// etValue = findViewById(R.id.etValue)
// }
// }
}
2.1.4 使用自定义控件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.jomurphys.demo.view.Counter
android:layout_width="wrap_content"
android:layout_height="100dp"
/>
</LinearLayout>
2.2 自定义类
2.2.1 处理数据
private var mCounter = 0
//获取计数
fun getCounter(): Int = mCounter
//设置计数
fun setCounter(value: Int) {
mCounter = value
updateCounter()
}
//更新计数
private fun updateCounter() {
etValue.setText(mCounter.toString())
}
2.2.2 处理事件
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
handleEvents()
}
//处理点击事件
private fun handleEvents() {
tvPlus.setOnClickListener {
mCounter++
updateCounter()
}
tvMinus.setOnClickListener {
mCounter--
updateCounter()
}
}
2.2.3 定义功能接口将数据暴露给外部使用
private var mOnCounterChangeListener: OnCounterChangeListener? = null
//暴露接口
interface OnCounterChangeListener {
fun onCounterChange(value: Int)
}
//设置计数变动监听器
fun setOnCounterChangeListener(listener: OnCounterChangeListener) {
mOnCounterChangeListener = listener
}
private fun updateCounter() {
//监听器不为null就调用回调方法
mOnCounterChangeListener?.onCounterChange(mCounter)
}
2.3 自定义属性
2.3.1 定义attr属性资源文件
values文件夹右键→New→Values Resource File→命名attrs。
<resources>
<declare-styleable name="Counter">
<attr name="max" format="integer" />
<attr name="min" format="integer" />
<attr name="step" format="integer" />
<attr name="disable" format="boolean" />
<attr name="btnColor" format="color|reference" />
<attr name="valueSize" format="dimension" />
</declare-styleable>
</resources>
2.3.2 引入属性并暴露getter/setter
//属性(保持public因为要暴露getter/setter供UI中代码调用)
var min: Int = 0
var max: Int = 0
var step: Int = 0
var disable: Boolean = false
var defaultValue: Int = 0
//调用setter的时候要赋值给mCounter并更新
set(value) {
field = value
mCounter = value
updateCounter()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initAttrs(context, attrs)
}
//初始化属性
private fun initAttrs(context: Context?, attrs: AttributeSet?) {
if (context != null && attrs != null) {
val attributes = context.obtainStyledAttributes(attrs, R.styleable.Counter)
min= attributes.getInt(R.styleable.Counter_min, 0)
max= attributes.getInt(R.styleable.Counter_max, 0)
step= attributes.getInt(R.styleable.Counter_step, 0)
disable = attributes.getBoolean(R.styleable.Counter_disable, false)
defaultValue = attributes.getInt(R.styleable.Counter_disable, 0)
mCounter = defaultValue //获取了默认值就设置给mCounter,更新动作放在initView()中
attributes.recycle()
}
//简写
// context?.obtainStyledAttributes(attrs, R.styleable.Counter)?.run {
// min= getInt(R.styleable.Counter_min, 0)
// max= getInt(R.styleable.Counter_max, 0)
// step = getInt(R.styleable.Counter_step, 0)
// disable = getBoolean(R.styleable.Counter_disable, false)
// defaultValue = getInt(R.styleable.Counter_disable, 0)
// recycle()
// }
}
2.3.3 运用属性
private fun initView(context: Context?) {
if (context != null) {
//...
//将属性中设置的值更新到控件上
updateCounter()
tvPlus.isEnabled = mEnable
tvMinus.isEnabled = mEnable
}
}
private fun handleEvents() {
tvPlus.setOnClickListener {
//按下后就不是最小值,解除tvMinus按钮禁用
tvMinus.isEnabled = true
//步进不为1的时候,要考虑值溢出的情况
mCounter += mStep
//如果计数大于等于最大值就设为最大值,并禁用按钮
if (mCounter >= mMax) {
mCounter = mMax
tvPlus.isEnabled = false
}
updateCounter()
}
tvMinus.setOnClickListener {
tvPlus.isEnabled = true
mCounter -= mStep
if (mCounter <= mMin) {
mCounter = mMin
tvMinus.isEnabled = false
}
updateCounter()
}
}
2.3.4 布局文件中使用自定义属性
根布局添加命名空间(只需要输入app,IDE会自动补全)。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.jomurphys.demo.view.Counter
android:layout_width="wrap_content"
android:layout_height="100dp"
app:min="-10"
app:max="10"
app:enable="true"
/>
</LinearLayout>