📅  最后修改于: 2023-12-03 15:24:06.509000             🧑  作者: Mango
开关按钮是常见的用户界面元素之一,Android 自带的 Switch
控件可以实现类似的功能。但是,在某些场景下,我们可能需要自定义开关按钮的样式和行为。
本篇文章将介绍如何在 Android 中创建自定义开关按钮。我们将使用 Kotlin 语言编写示例代码。让我们开始吧!
首先,我们需要在项目的布局文件中定义自定义开关按钮的外观和布局。
<com.example.customswitch.CustomButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true" />
在这个布局文件中,我们使用自定义视图 CustomButton
替代了系统的 Switch
控件。这个自定义视图将会在后面的步骤中实现。
我们将开关按钮的默认状态设置为选中(checked
属性为 true)。
接下来,我们需要创建一个自定义视图 CustomButton
,并在其中实现我们需要的功能。我们将扩展 Android 框架中的 CompoundButton
类,因为它已经实现了一些我们需要的核心功能,比如选中状态和清晰的点击事件。
class CustomButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.switchStyle
) : CompoundButton(context, attrs, defStyleAttr) {
init {
// 初始化时执行的代码
}
// 其他方法和属性
}
在这个自定义视图中,我们定义了一个带有三个参数的构造函数。这些参数分别是 context: Context
、attrs: AttributeSet?
和 defStyleAttr: Int
。
context
:视图所在的上下文环境。attrs
:这个视图的 XML 属性集合。defStyleAttr
:应用主题中指定的属性样式。在这个构造函数中,我们调用了父类的构造函数,并向其中传递了这些参数。我们还设置了一些初始化代码,这个代码将在实例化完成后立即执行。
现在,我们需要实现自定义开关按钮的两种状态:选中和未选中。
为了实现自定义开关按钮的外观,我们需要创建一些画笔和颜色。我们将使用 Paint
类来创建画笔。以下是我们所需的代码:
private val paint: Paint = Paint().apply {
isAntiAlias = true
style = Paint.Style.FILL
}
private val uncheckedColor: Int = Color.LTGRAY
private val checkedColor: Int = Color.BLUE
在这段代码中,我们创建了名为 paint
的画笔对象。我们设置了它的抗锯齿特性,并将画笔的样式设置为填充(Paint.Style.FILL
)。
我们还定义了两个颜色常量 uncheckedColor
和 checkedColor
,分别表示开关按钮未选中和选中状态的颜色。
在自定义视图中,我们需要重写 onDraw()
方法来实现自定义绘制。我们将使用 GPU 执行这个任务,而不是使用 Canvas
和 drawXxx()
方法。这将使我们能够更快地绘制和更好地处理大量的自定义视图。
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas ?: return
// 绘制背景
paint.color = if (isChecked) checkedColor else uncheckedColor
canvas.drawRoundRect(backgroundRectF, radius, radius, paint)
// 绘制圆形按钮
paint.color = Color.WHITE
canvas.drawCircle(circleCenterX, circleCenterY, circleRadius, paint)
}
在这个代码中,我们首先调用了父类的 onDraw()
方法,以便绘制基本的控件。然后,我们检查 canvas
参数是否为 null。如果是,我们就提前返回并退出方法。
接下来,我们绘制了开关按钮的背景和圆形按钮。我们使用了 isChecked
属性判断当前的选中状态,并设置了画笔的颜色。我们使用 backgroundRectF
变量来定义背景的矩形边界。我们还使用圆心 (circleCenterX
和 circleCenterY
) 变量和半径 (circleRadius
) 变量来定义圆形按钮的位置和大小。
我们还需要实现 onTouchEvent()
方法,以便处理用户触摸事件。在这个方法中,我们将检查用户触摸的位置是否在圆形按钮内部,并在需要时更新开关按钮的状态。以下是我们所需的代码:
override fun onTouchEvent(event: MotionEvent?): Boolean {
return when (event?.action) {
MotionEvent.ACTION_DOWN -> handleDownEvent(event)
MotionEvent.ACTION_MOVE -> handleMoveEvent(event)
MotionEvent.ACTION_UP -> handleUpEvent(event)
else -> super.onTouchEvent(event)
}
}
private fun handleDownEvent(event: MotionEvent): Boolean {
if (isEnabled && isInCircle(event.x, event.y)) {
isPressed = true
return true
}
return false
}
private fun handleMoveEvent(event: MotionEvent): Boolean {
if (isPressed) {
isChecked = isInCircle(event.x, event.y)
return true
}
return false
}
private fun handleUpEvent(event: MotionEvent): Boolean {
isPressed = false
if (isEnabled && isInCircle(event.x, event.y)) {
performClick()
return true
}
return false
}
private fun isInCircle(x: Float, y: Float): Boolean {
val dx = x - circleCenterX
val dy = y - circleCenterY
val distance = Math.sqrt((dx * dx + dy * dy).toDouble())
return distance < circleRadius
}
在这个代码中,我们首先重写了 onTouchEvent()
方法,以便处理所有的触摸事件。
我们使用 MotionEvent.ACTION_DOWN
状态处理按下事件。在这个状态下,我们检查 isEnabled
属性是否为 true,同时检查触摸事件的位置是否在圆形按钮内。如果是,我们就标记开关按钮为按下状态。
我们使用 MotionEvent.ACTION_MOVE
状态处理移动事件。在这个状态下,我们检查是否按下了开关按钮,并检查当前位置是否在圆形按钮内。如果是,我们就更新开关按钮的状态。
我们使用 MotionEvent.ACTION_UP
状态处理抬起事件。在这个状态下,我们取消按下状态,并检查触摸事件的位置是否在圆形按钮内。如果是,我们就执行 performClick()
方法,这将触发单击事件。
最后,我们实现了辅助方法 isInCircle()
,用于判断给定的坐标是否位于圆形按钮内。
现在,我们已经完成了自定义开关按钮的代码。我们将所有的代码组合在一起,并对其进行优化和调试。以下是完整的实现代码:
class CustomButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.switchStyle
) : CompoundButton(context, attrs, defStyleAttr) {
private val paint: Paint = Paint().apply {
isAntiAlias = true
style = Paint.Style.FILL
}
private val uncheckedColor: Int = Color.LTGRAY
private val checkedColor: Int = Color.BLUE
private val backgroundRectF: RectF = RectF()
private var circleCenterX: Float = 0f
private var circleCenterY: Float = 0f
private var circleRadius: Float = 0f
init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.CustomButton, defStyleAttr, 0)
try {
isChecked = ta.getBoolean(R.styleable.CustomButton_android_checked, isChecked)
} finally {
ta.recycle()
}
updateBackgroundRectF()
updateCircleCenterAndRadius()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight
val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom
val width = resolveSize(desiredWidth, widthMeasureSpec)
val height = resolveSize(desiredHeight, heightMeasureSpec)
setMeasuredDimension(width, height)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas ?: return
paint.color = if (isChecked) checkedColor else uncheckedColor
canvas.drawRoundRect(backgroundRectF, radius, radius, paint)
paint.color = Color.WHITE
canvas.drawCircle(circleCenterX, circleCenterY, circleRadius, paint)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
return when (event?.action) {
MotionEvent.ACTION_DOWN -> handleDownEvent(event)
MotionEvent.ACTION_MOVE -> handleMoveEvent(event)
MotionEvent.ACTION_UP -> handleUpEvent(event)
else -> super.onTouchEvent(event)
}
}
private fun handleDownEvent(event: MotionEvent): Boolean {
if (isEnabled && isInCircle(event.x, event.y)) {
isPressed = true
return true
}
return false
}
private fun handleMoveEvent(event: MotionEvent): Boolean {
if (isPressed) {
isChecked = isInCircle(event.x, event.y)
return true
}
return false
}
private fun handleUpEvent(event: MotionEvent): Boolean {
isPressed = false
if (isEnabled && isInCircle(event.x, event.y)) {
performClick()
return true
}
return false
}
private fun isInCircle(x: Float, y: Float): Boolean {
val dx = x - circleCenterX
val dy = y - circleCenterY
val distance = Math.sqrt((dx * dx + dy * dy).toDouble())
return distance < circleRadius
}
private fun updateBackgroundRectF() {
backgroundRectF.left = paddingStart.toFloat()
backgroundRectF.top = paddingTop.toFloat()
backgroundRectF.right = width.toFloat() - paddingEnd
backgroundRectF.bottom = height.toFloat() - paddingBottom
}
private fun updateCircleCenterAndRadius() {
circleCenterX = if (isChecked) width - paddingRight - radius else paddingLeft + radius
circleCenterY = height / 2f
circleRadius = minOf(width - paddingRight - paddingLeft, height - paddingBottom - paddingTop) / 2f - 2 * radius
}
private companion object {
private const val radius: Float = 6f
}
}
在这份代码中,我们处理了许多细节,包括视图的测量、选中和未选中状态的背景绘制、圆形按钮的绘制和位置。
自定义开关按钮是一个非常实用的用户界面元素。随着 Android 应用程序的复杂度和功能的增加,自定义开关按钮正在变得越来越重要。通过本文介绍的步骤,你可以快速地创建一个自定义开关按钮,并让它完美地适应你的应用程序。