📜  如何在 Android 中创建自定义开关按钮?(1)

📅  最后修改于: 2023-12-03 15:24:06.509000             🧑  作者: Mango

在 Android 中创建自定义开关按钮

开关按钮是常见的用户界面元素之一,Android 自带的 Switch 控件可以实现类似的功能。但是,在某些场景下,我们可能需要自定义开关按钮的样式和行为。

本篇文章将介绍如何在 Android 中创建自定义开关按钮。我们将使用 Kotlin 语言编写示例代码。让我们开始吧!

步骤 1:创建布局文件

首先,我们需要在项目的布局文件中定义自定义开关按钮的外观和布局。

<com.example.customswitch.CustomButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:checked="true" />

在这个布局文件中,我们使用自定义视图 CustomButton 替代了系统的 Switch 控件。这个自定义视图将会在后面的步骤中实现。

我们将开关按钮的默认状态设置为选中(checked 属性为 true)。

步骤 2:创建自定义视图

接下来,我们需要创建一个自定义视图 CustomButton,并在其中实现我们需要的功能。我们将扩展 Android 框架中的 CompoundButton 类,因为它已经实现了一些我们需要的核心功能,比如选中状态和清晰的点击事件。

class CustomButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = R.attr.switchStyle
) : CompoundButton(context, attrs, defStyleAttr) {

    init {
        // 初始化时执行的代码
    }

    // 其他方法和属性
}

在这个自定义视图中,我们定义了一个带有三个参数的构造函数。这些参数分别是 context: Contextattrs: AttributeSet?defStyleAttr: Int

  • context:视图所在的上下文环境。
  • attrs:这个视图的 XML 属性集合。
  • defStyleAttr:应用主题中指定的属性样式。

在这个构造函数中,我们调用了父类的构造函数,并向其中传递了这些参数。我们还设置了一些初始化代码,这个代码将在实例化完成后立即执行。

现在,我们需要实现自定义开关按钮的两种状态:选中和未选中。

步骤 3:创建画笔和颜色

为了实现自定义开关按钮的外观,我们需要创建一些画笔和颜色。我们将使用 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)。

我们还定义了两个颜色常量 uncheckedColorcheckedColor,分别表示开关按钮未选中和选中状态的颜色。

步骤 4:重写 onDraw() 方法

在自定义视图中,我们需要重写 onDraw() 方法来实现自定义绘制。我们将使用 GPU 执行这个任务,而不是使用 CanvasdrawXxx() 方法。这将使我们能够更快地绘制和更好地处理大量的自定义视图。

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 变量来定义背景的矩形边界。我们还使用圆心 (circleCenterXcircleCenterY) 变量和半径 (circleRadius) 变量来定义圆形按钮的位置和大小。

步骤 5:实现触摸事件

我们还需要实现 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(),用于判断给定的坐标是否位于圆形按钮内。

步骤 6:完成自定义开关按钮

现在,我们已经完成了自定义开关按钮的代码。我们将所有的代码组合在一起,并对其进行优化和调试。以下是完整的实现代码:

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 应用程序的复杂度和功能的增加,自定义开关按钮正在变得越来越重要。通过本文介绍的步骤,你可以快速地创建一个自定义开关按钮,并让它完美地适应你的应用程序。