Android DEMO Creating Custom Views

437 阅读3分钟

image.png -h80

  • 计算位置 为了正确绘制自定义视图,需要知道它的大小,View 提供多种测量处理方法,大部分方法都不需要被替换。如果您的视图不需要对其大小进行特殊控制,您只需替换一个方法,即 onSizeChanged(),在 onSizeChanged() 中计算位置、尺寸以及其他与视图大小相关的任何值.

override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
   radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
  • 定义 computeXYForSpeed,计算x,y坐标轴
private fun PointF.computeXYForSpeed(pos: FanSpeed, radius: Float) {
   // Angles are in radians.
   val startAngle = Math.PI * (9 / 8.0)   
   val angle = startAngle + pos.ordinal * (Math.PI / 4)
   x = (radius * cos(angle)).toFloat() + width / 2
   y = (radius * sin(angle)).toFloat() + height / 2
}
  • override onDraw 在调用 onDraw 方法之前,必须先创建 Paint 对象
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}

设置画笔对象属性

paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
  • 画背景 drawCircle()
//Draw the dial
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  • 绘制小圆
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
  • 绘制文字
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
   pointPosition.computeXYForSpeed(i, labelRadius)
   val label = resources.getString(i.label)
   canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}
  • 完成onDraw如下:
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    // Set dial background color based on the selection.
    paint.color = when (fanSpeed) {
        FanSpeed.OFF -> Color.GRAY
        FanSpeed.LOW -> fanSpeedLowColor
        FanSpeed.MEDIUM -> fanSpeedMediumColor
        FanSpeed.HIGH -> fanSpeedMaxColor
    }
    // Draw the dial. 背景
    canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
    // Draw the indicator circle.
    val markerRadius = radius + RADIUS_OFFSET_INDICATOR
    pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
    paint.color = Color.BLACK
    canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
    // Draw the text labels.
    val labelRadius = radius + RADIUS_OFFSET_LABEL
    for (i in FanSpeed.values()) {
        pointPosition.computeXYForSpeed(i, labelRadius)
        val label = resources.getString(i.label)
        canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
    }
}
  • DialView.kt
private enum class FanSpeed(val label: Int) {
    OFF(R.string.fan_off),
    LOW(R.string.fan_low),
    MEDIUM(R.string.fan_medium),
    HIGH(R.string.fan_high);

    fun next() = when (this) {
        OFF -> LOW
        LOW -> MEDIUM
        MEDIUM -> HIGH
        HIGH -> OFF
    }
}

private const val RADIUS_OFFSET_LABEL = 30          //Offset from dial radius to draw text label 文本半径
private const val RADIUS_OFFSET_INDICATOR = -35     //Offset from dial radius to draw indicator

class DialView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        // Paint styles used for rendering are initialized here. This
        // is a performance optimization, since onDraw() is called
        // for every screen refresh.
        style = Paint.Style.FILL
        textAlign = Paint.Align.CENTER
        textSize = 55.0f
        typeface = Typeface.create("", Typeface.BOLD)
    }

    private var radius = 0.0f                  // Radius of the circle. 半径
    private var fanSpeed = FanSpeed.OFF        // The active selection.
    //Point at which to draw label and indicator circle position. PointF is a point
    //with floating-point coordinates.
    private val pointPosition: PointF = PointF(0.0f, 0.0f)

    private val fanSpeedLowColor:Int
    private val fanSpeedMediumColor:Int
    private val fanSpeedMaxColor:Int

    init {
        isClickable = true

        val typedArray = context.obtainStyledAttributes(attrs,R.styleable.DialView)
        fanSpeedLowColor=typedArray.getColor(R.styleable.DialView_fanColor1,0)
        fanSpeedMediumColor = typedArray.getColor(R.styleable.DialView_fanColor2,0)
        fanSpeedMaxColor = typedArray.getColor(R.styleable.DialView_fanColor3,0)
        typedArray.recycle()

        updateContentDescription()

        // For minsdk >= 21, you can just add a click action. In this app since minSdk is 19,
        // you must add a delegate to handle accessibility.
        ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
            override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
                super.onInitializeAccessibilityNodeInfo(host, info)
                val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
                    AccessibilityNodeInfo.ACTION_CLICK,
                    // If the fan speed is OFF, LOW, or MEDIUM, the hint is to change the speed.
                    // If it is HIGH use reset.
                    context.getString(if (fanSpeed !=  FanSpeed.HIGH) R.string.change else R.string.reset)
                )
                info.addAction(customClick)
            }
        })
    }

    override fun performClick(): Boolean {
        // Give default click listeners priority and perform accessibility/autofill events.
        // Also calls onClickListener() to handle further subclass customizations.
        if (super.performClick()) return true

        // Rotates between each of the different selection
        // states on each click.
        fanSpeed = fanSpeed.next()
        updateContentDescription()
        // Redraw the view.
        invalidate()
        return true
    }

    /**
     * This is called during layout when the size of this view has changed. If
     * the view was just added to the view hierarchy, it is called with the old
     * values of 0. The code determines the drawing bounds for the custom view.
     *
     * @param width    Current width of this view.
     * @param height    Current height of this view.
     * @param oldWidth Old width of this view.
     * @param oldHeight Old height of this view.
     */
    override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
        // Calculate the radius from the smaller of the width and height.
        radius = (min(width, height) / 2.0 * 0.8).toFloat()
    }

    /**
     * Renders view content: an outer circle to serve as the "dial",
     * and a smaller black circle to server as the indicator.
     * The position of the indicator is based on fanSpeed.
     *
     * @param canvas The canvas on which the background will be drawn.
     */
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // Set dial background color based on the selection.
        paint.color = when (fanSpeed) {
            FanSpeed.OFF -> Color.GRAY
            FanSpeed.LOW -> fanSpeedLowColor
            FanSpeed.MEDIUM -> fanSpeedMediumColor
            FanSpeed.HIGH -> fanSpeedMaxColor
        }
        // Draw the dial. 背景
        canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
        // Draw the indicator circle.
        val markerRadius = radius + RADIUS_OFFSET_INDICATOR
        pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
        paint.color = Color.BLACK
        canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
        // Draw the text labels.
        val labelRadius = radius + RADIUS_OFFSET_LABEL
        for (i in FanSpeed.values()) {
            pointPosition.computeXYForSpeed(i, labelRadius)
            val label = resources.getString(i.label)
            canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
        }
    }

    /**
     * Computes the X/Y-coordinates for a label or indicator,
     * given the FanSpeed and radius where the label should be drawn.
     *
     * @param pos Position (FanSpeed)
     * @param radius Radius where label/indicator is to be drawn.
     * @return 2-element array. Element 0 is X-coordinate, element 1 is Y-coordinate.
     */
    private fun PointF.computeXYForSpeed(pos: FanSpeed, radius: Float) {
        // Angles are in radians.
        val startAngle = Math.PI * (9 / 8.0)
        val angle = startAngle + pos.ordinal * (Math.PI / 4)
        x = (radius * cos(angle)).toFloat() + width / 2
        y = (radius * sin(angle)).toFloat() + height / 2
    }

    /**
     * Updates the view's content description with the appropirate string for the
     * current fan speed.
     */
    private fun updateContentDescription() {
        contentDescription = resources.getString(fanSpeed.label)
    }
}
 
  • 界面引入
<com.example.android.customfancontroller.DialView
        android:id="@+id/dialView"
        android:layout_width="@dimen/fan_dimen"
        android:layout_height="@dimen/fan_dimen"
        app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginLeft="@dimen/default_margin"
        android:layout_marginRight="@dimen/default_margin"
        android:layout_marginTop="@dimen/default_margin"
        app:fanColor1="#FFEB3B"
        app:fanColor2="#CDDC39"
        app:fanColor3="#009688"/>
  • atts
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="DialView">
        <attr name="fanColor1" format="color" />
        <attr name="fanColor2" format="color" />
        <attr name="fanColor3" format="color" />
    </declare-styleable>
</resources>