跳转至

第15章 自定义View与高级绘制

自定义View与高级绘制图

学习目标:掌握自定义View的开发,理解Canvas绘制、动画和手势处理。

预计学习时间:5-7天 实践时间:2-3天


目录

  1. 自定义View基础
  2. Canvas绘制
  3. 自定义属性
  4. 动画实现
  5. 手势处理
  6. 实践练习

1. 自定义View基础

1.1 继承View

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

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.RED
        style = Paint.Style.FILL
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val centerX = width / 2f
        val centerY = height / 2f
        val radius = min(centerX, centerY) - 20
        canvas.drawCircle(centerX, centerY, radius, paint)
    }
}

1.2 测量与布局

Kotlin
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)

    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
    val heightSize = MeasureSpec.getSize(heightMeasureSpec)

    val desiredWidth = 200
    val desiredHeight = 200

    val width = when (widthMode) {
        MeasureSpec.EXACTLY -> widthSize
        MeasureSpec.AT_MOST -> min(desiredWidth, widthSize)
        else -> desiredWidth
    }

    val height = when (heightMode) {
        MeasureSpec.EXACTLY -> heightSize
        MeasureSpec.AT_MOST -> min(desiredHeight, heightSize)
        else -> desiredHeight
    }

    setMeasuredDimension(width, height)
}

2. Canvas绘制

2.1 基础图形

Kotlin
override fun onDraw(canvas: Canvas) {
    // 绘制圆形
    canvas.drawCircle(cx, cy, radius, paint)

    // 绘制矩形
    canvas.drawRect(left, top, right, bottom, paint)

    // 绘制圆角矩形
    canvas.drawRoundRect(rect, rx, ry, paint)

    // 绘制椭圆
    canvas.drawOval(rect, paint)

    // 绘制弧线
    canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint)

    // 绘制直线
    canvas.drawLine(startX, startY, stopX, stopY, paint)

    // 绘制文字
    canvas.drawText(text, x, y, paint)

    // 绘制Bitmap
    canvas.drawBitmap(bitmap, src, dst, paint)

    // 绘制Path
    canvas.drawPath(path, paint)
}

2.2 Path路径

Kotlin
val path = Path().apply {
    // 移动到某点
    moveTo(100f, 100f)

    // 画线到某点
    lineTo(200f, 200f)

    // 二次贝塞尔曲线
    quadTo(controlX, controlY, endX, endY)

    // 三次贝塞尔曲线
    cubicTo(x1, y1, x2, y2, x3, y3)

    // 画圆弧
    arcTo(rect, startAngle, sweepAngle)

    // 闭合路径
    close()
}

3. 自定义属性

XML
<!-- res/values/attrs.xml -->
<declare-styleable name="CircleView">
    <attr name="circleColor" format="color" />
    <attr name="circleRadius" format="dimension" />
    <attr name="showBorder" format="boolean" />
</declare-styleable>
Kotlin
// 在构造函数中读取属性
init {
    context.theme.obtainStyledAttributes(
        attrs,
        R.styleable.CircleView,
        defStyleAttr,
        0
    ).apply {
        try {  // try/catch捕获异常
            circleColor = getColor(R.styleable.CircleView_circleColor, Color.RED)
            circleRadius = getDimension(R.styleable.CircleView_circleRadius, 100f)
            showBorder = getBoolean(R.styleable.CircleView_showBorder, false)
        } finally {
            recycle()
        }
    }
}
XML
<!-- 使用自定义属性 -->
<com.example.CircleView
    android:layout_width="200dp"
    android:layout_height="200dp"
    app:circleColor="@color/primary"
    app:circleRadius="80dp"
    app:showBorder="true" />

4. 动画实现

4.1 属性动画

Kotlin
// 值动画
ValueAnimator.ofFloat(0f, 100f).apply {
    duration = 1000
    interpolator = AccelerateDecelerateInterpolator()
    addUpdateListener { animator ->
        val value = animator.animatedValue as Float
        // 更新View
        invalidate()
    }
    start()
}

// ObjectAnimator
ObjectAnimator.ofFloat(view, "translationX", 0f, 200f).apply {
    duration = 1000
    start()
}

// AnimatorSet
AnimatorSet().apply {
    play(animator1).with(animator2).before(animator3)
    start()
}

4.2 自定义动画

Kotlin
class PulseAnimation(private val view: View) {
    fun start() {
        val scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.2f, 1f)
        val scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.2f, 1f)

        AnimatorSet().apply {
            playTogether(scaleX, scaleY)
            duration = 1000
            repeatCount = ValueAnimator.INFINITE
            start()
        }
    }
}

5. 手势处理

Kotlin
class GestureView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {

    private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
        override fun onDown(e: MotionEvent): Boolean = true

        override fun onSingleTapUp(e: MotionEvent): Boolean {
            // 单击
            return true
        }

        override fun onDoubleTap(e: MotionEvent): Boolean {
            // 双击
            return true
        }

        override fun onLongPress(e: MotionEvent) {
            // 长按
        }

        override fun onScroll(
            e1: MotionEvent?,
            e2: MotionEvent,
            distanceX: Float,
            distanceY: Float
        ): Boolean {
            // 滑动
            return true
        }

        override fun onFling(
            e1: MotionEvent?,
            e2: MotionEvent,
            velocityX: Float,
            velocityY: Float
        ): Boolean {
            // 快速滑动
            return true
        }
    })

    private val scaleGestureDetector = ScaleGestureDetector(context,
        object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
            override fun onScale(detector: ScaleGestureDetector): Boolean {
                scaleFactor *= detector.scaleFactor
                invalidate()
                return true
            }
        })

    override fun onTouchEvent(event: MotionEvent): Boolean {
        scaleGestureDetector.onTouchEvent(event)
        return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event)
    }
}

6. 实践练习

练习1:自定义进度条

任务:实现圆形进度条

要求: - 可设置进度 - 支持渐变色 - 带动画效果

练习2:手势缩放图片

任务:实现可缩放拖拽的图片View

要求: - 双指缩放 - 单指拖拽 - 边界检测


本章小结

核心要点

  1. 自定义View需要处理测量、布局和绘制
  2. Canvas提供丰富的绘制API
  3. 属性动画是实现动画的首选方案
  4. GestureDetector简化手势处理

下一步

完成本章学习后,请进入第16章:NDK与JNI开发


本章完成时间:预计5-7天