引言
自定义 View 是 Android 开发中的重要技能,它允许开发者创建独特的用户界面组件,满足特定的设计需求和交互体验。通过自定义 View,我们可以突破系统控件的限制,实现复杂的动画效果、特殊的绘制需求和个性化的用户交互。本文将深入探讨 Android 自定义 View 的核心概念、开发技巧和最佳实践。
自定义 View 基础
View 的绘制流程
Android 中 View 的绘制遵循三个主要步骤:
- Measure(测量):确定 View 的大小
- Layout(布局):确定 View 的位置
- Draw(绘制):将 View 绘制到屏幕上
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var centerX = 0f
private var centerY = 0f
private var radius = 0f
init {
// 初始化画笔
paint.apply {
color = Color.BLUE
style = Paint.Style.FILL
strokeWidth = 4f
}
}
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 width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> minOf(200.dp, widthSize)
else -> 200.dp
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> minOf(200.dp, heightSize)
else -> 200.dp
}
setMeasuredDimension(width, height)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
centerX = w / 2f
centerY = h / 2f
radius = minOf(w, h) / 2f - 20f
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制圆形
canvas.drawCircle(centerX, centerY, radius, paint)
}
// 扩展属性:dp 转 px
private val Int.dp: Int
get() = (this * resources.displayMetrics.density).toInt()
}
自定义属性
<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="CircleProgressView">
<attr name="circleColor" format="color" />
<attr name="progressColor" format="color" />
<attr name="strokeWidth" format="dimension" />
<attr name="progress" format="float" />
<attr name="maxProgress" format="float" />
<attr name="showText" format="boolean" />
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
<attr name="animationDuration" format="integer" />
</declare-styleable>
<declare-styleable name="WaveView">
<attr name="waveColor" format="color" />
<attr name="waveHeight" format="dimension" />
<attr name="waveLength" format="dimension" />
<attr name="waveSpeed" format="float" />
<attr name="waveCount" format="integer" />
</declare-styleable>
</resources>
class CircleProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 属性变量
private var circleColor = Color.GRAY
private var progressColor = Color.BLUE
private var strokeWidth = 10f
private var progress = 0f
private var maxProgress = 100f
private var showText = true
private var textSize = 48f
private var textColor = Color.BLACK
private var animationDuration = 1000
// 绘制相关
private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rectF = RectF()
// 动画
private var currentProgress = 0f
private var progressAnimator: ValueAnimator? = null
init {
// 解析自定义属性
context.theme.obtainStyledAttributes(
attrs,
R.styleable.CircleProgressView,
0, 0
).apply {
try {
circleColor = getColor(R.styleable.CircleProgressView_circleColor, Color.GRAY)
progressColor = getColor(R.styleable.CircleProgressView_progressColor, Color.BLUE)
strokeWidth = getDimension(R.styleable.CircleProgressView_strokeWidth, 10f)
progress = getFloat(R.styleable.CircleProgressView_progress, 0f)
maxProgress = getFloat(R.styleable.CircleProgressView_maxProgress, 100f)
showText = getBoolean(R.styleable.CircleProgressView_showText, true)
textSize = getDimension(R.styleable.CircleProgressView_textSize, 48f)
textColor = getColor(R.styleable.CircleProgressView_textColor, Color.BLACK)
animationDuration = getInt(R.styleable.CircleProgressView_animationDuration, 1000)
} finally {
recycle()
}
}
initPaints()
}
private fun initPaints() {
circlePaint.apply {
color = circleColor
style = Paint.Style.STROKE
strokeWidth = this@CircleProgressView.strokeWidth
strokeCap = Paint.Cap.ROUND
}
progressPaint.apply {
color = progressColor
style = Paint.Style.STROKE
strokeWidth = this@CircleProgressView.strokeWidth
strokeCap = Paint.Cap.ROUND
}
textPaint.apply {
color = textColor
textSize = this@CircleProgressView.textSize
textAlign = Paint.Align.CENTER
typeface = Typeface.DEFAULT_BOLD
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val padding = strokeWidth / 2
rectF.set(
padding,
padding,
w - padding,
h - padding
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制背景圆环
canvas.drawOval(rectF, circlePaint)
// 绘制进度圆弧
val sweepAngle = (currentProgress / maxProgress) * 360f
canvas.drawArc(rectF, -90f, sweepAngle, false, progressPaint)
// 绘制文字
if (showText) {
val text = "${(currentProgress / maxProgress * 100).toInt()}%"
val centerX = width / 2f
val centerY = height / 2f - (textPaint.descent() + textPaint.ascent()) / 2
canvas.drawText(text, centerX, centerY, textPaint)
}
}
// 公共方法
fun setProgress(progress: Float, animated: Boolean = true) {
val newProgress = progress.coerceIn(0f, maxProgress)
if (animated) {
animateProgress(newProgress)
} else {
this.progress = newProgress
currentProgress = newProgress
invalidate()
}
}
private fun animateProgress(targetProgress: Float) {
progressAnimator?.cancel()
progressAnimator = ValueAnimator.ofFloat(currentProgress, targetProgress).apply {
duration = animationDuration.toLong()
interpolator = DecelerateInterpolator()
addUpdateListener { animator ->
currentProgress = animator.animatedValue as Float
invalidate()
}
start()
}
}
// 属性设置方法
fun setCircleColor(color: Int) {
circleColor = color
circlePaint.color = color
invalidate()
}
fun setProgressColor(color: Int) {
progressColor = color
progressPaint.color = color
invalidate()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
progressAnimator?.cancel()
}
}
复杂自定义 View 实现
波浪效果 View
class WaveView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var waveColor = Color.BLUE
private var waveHeight = 50f
private var waveLength = 200f
private var waveSpeed = 2f
private var waveCount = 2
private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val wavePath = Path()
private var waveOffset = 0f
private var animator: ValueAnimator? = null
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.WaveView,
0, 0
).apply {
try {
waveColor = getColor(R.styleable.WaveView_waveColor, Color.BLUE)
waveHeight = getDimension(R.styleable.WaveView_waveHeight, 50f)
waveLength = getDimension(R.styleable.WaveView_waveLength, 200f)
waveSpeed = getFloat(R.styleable.WaveView_waveSpeed, 2f)
waveCount = getInt(R.styleable.WaveView_waveCount, 2)
} finally {
recycle()
}
}
initPaint()
startAnimation()
}
private fun initPaint() {
wavePaint.apply {
color = waveColor
style = Paint.Style.FILL
alpha = 128
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
for (i in 0 until waveCount) {
drawWave(canvas, i)
}
}
private fun drawWave(canvas: Canvas, waveIndex: Int) {
wavePath.reset()
val waveY = height * 0.7f
val phaseShift = waveIndex * Math.PI / 4
wavePath.moveTo(-waveLength, waveY)
var x = -waveLength
while (x <= width + waveLength) {
val y = (waveY + waveHeight * sin(
2 * Math.PI * (x + waveOffset) / waveLength + phaseShift
)).toFloat()
wavePath.lineTo(x, y)
x += 5f
}
wavePath.lineTo(width + waveLength, height.toFloat())
wavePath.lineTo(-waveLength, height.toFloat())
wavePath.close()
// 设置不同波浪的透明度
wavePaint.alpha = (128 - waveIndex * 30).coerceAtLeast(50)
canvas.drawPath(wavePath, wavePaint)
}
private fun startAnimation() {
animator = ValueAnimator.ofFloat(0f, waveLength).apply {
duration = (waveLength / waveSpeed * 10).toLong()
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
addUpdateListener { animator ->
waveOffset = animator.animatedValue as Float
invalidate()
}
start()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
animator?.cancel()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (animator?.isRunning != true) {
startAnimation()
}
}
}
雷达扫描 View
class RadarView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val sweepPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var centerX = 0f
private var centerY = 0f
private var radius = 0f
private var sweepAngle = 0f
private var animator: ValueAnimator? = null
// 雷达点数据
private val radarDots = mutableListOf<RadarDot>()
data class RadarDot(
val x: Float,
val y: Float,
val alpha: Float = 1f,
val size: Float = 8f
)
init {
initPaints()
generateRandomDots()
startSweepAnimation()
}
private fun initPaints() {
circlePaint.apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 2f
alpha = 100
}
linePaint.apply {
color = Color.GREEN
strokeWidth = 1f
alpha = 150
}
sweepPaint.apply {
shader = SweepGradient(
0f, 0f,
intArrayOf(
Color.TRANSPARENT,
Color.argb(100, 0, 255, 0),
Color.argb(200, 0, 255, 0)
),
floatArrayOf(0f, 0.5f, 1f)
)
}
dotPaint.apply {
color = Color.GREEN
style = Paint.Style.FILL
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
centerX = w / 2f
centerY = h / 2f
radius = minOf(w, h) / 2f - 50f
// 更新渐变中心点
sweepPaint.shader = SweepGradient(
centerX, centerY,
intArrayOf(
Color.TRANSPARENT,
Color.argb(100, 0, 255, 0),
Color.argb(200, 0, 255, 0)
),
floatArrayOf(0f, 0.5f, 1f)
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制同心圆
for (i in 1..4) {
val r = radius * i / 4
canvas.drawCircle(centerX, centerY, r, circlePaint)
}
// 绘制十字线
canvas.drawLine(centerX - radius, centerY, centerX + radius, centerY, linePaint)
canvas.drawLine(centerX, centerY - radius, centerX, centerY + radius, linePaint)
// 绘制扫描效果
canvas.save()
canvas.rotate(sweepAngle, centerX, centerY)
canvas.drawCircle(centerX, centerY, radius, sweepPaint)
canvas.restore()
// 绘制雷达点
drawRadarDots(canvas)
}
private fun drawRadarDots(canvas: Canvas) {
radarDots.forEach { dot ->
dotPaint.alpha = (dot.alpha * 255).toInt()
canvas.drawCircle(dot.x, dot.y, dot.size, dotPaint)
}
}
private fun generateRandomDots() {
radarDots.clear()
repeat(15) {
val angle = Math.random() * 2 * Math.PI
val distance = Math.random() * radius * 0.8
val x = (centerX + distance * cos(angle)).toFloat()
val y = (centerY + distance * sin(angle)).toFloat()
radarDots.add(
RadarDot(
x = x,
y = y,
alpha = (0.3 + Math.random() * 0.7).toFloat(),
size = (4 + Math.random() * 8).toFloat()
)
)
}
}
private fun startSweepAnimation() {
animator = ValueAnimator.ofFloat(0f, 360f).apply {
duration = 3000
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
addUpdateListener { animator ->
sweepAngle = animator.animatedValue as Float
invalidate()
}
start()
}
}
fun addRadarDot(x: Float, y: Float) {
// 将屏幕坐标转换为相对于雷达中心的坐标
val distance = sqrt((x - centerX).pow(2) + (y - centerY).pow(2))
if (distance <= radius) {
radarDots.add(RadarDot(x, y))
invalidate()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
animator?.cancel()
}
}
触摸事件处理
可拖拽的自定义 View
class DraggableView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val gestureDetector: GestureDetector
private val scaleDetector: ScaleGestureDetector
// 变换矩阵
private val matrix = Matrix()
private val savedMatrix = Matrix()
// 触摸状态
private enum class Mode {
NONE, DRAG, ZOOM
}
private var mode = Mode.NONE
private var lastTouchX = 0f
private var lastTouchY = 0f
private var startDistance = 0f
private var midPoint = PointF()
// 绘制内容
private var scale = 1f
private var translateX = 0f
private var translateY = 0f
init {
paint.apply {
color = Color.BLUE
style = Paint.Style.FILL
}
gestureDetector = GestureDetector(context, GestureListener())
scaleDetector = ScaleGestureDetector(context, ScaleListener())
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.save()
canvas.concat(matrix)
// 绘制可拖拽的内容
drawContent(canvas)
canvas.restore()
}
private fun drawContent(canvas: Canvas) {
val centerX = width / 2f
val centerY = height / 2f
val radius = 100f
// 绘制主圆
canvas.drawCircle(centerX, centerY, radius, paint)
// 绘制装饰圆
paint.color = Color.RED
canvas.drawCircle(centerX - 50, centerY - 50, 30f, paint)
canvas.drawCircle(centerX + 50, centerY - 50, 30f, paint)
canvas.drawCircle(centerX, centerY + 50, 30f, paint)
paint.color = Color.BLUE
}
override fun onTouchEvent(event: MotionEvent): Boolean {
scaleDetector.onTouchEvent(event)
gestureDetector.onTouchEvent(event)
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
savedMatrix.set(matrix)
lastTouchX = event.x
lastTouchY = event.y
mode = Mode.DRAG
}
MotionEvent.ACTION_POINTER_DOWN -> {
startDistance = getDistance(event)
if (startDistance > 10f) {
savedMatrix.set(matrix)
getMidPoint(midPoint, event)
mode = Mode.ZOOM
}
}
MotionEvent.ACTION_MOVE -> {
when (mode) {
Mode.DRAG -> {
matrix.set(savedMatrix)
val dx = event.x - lastTouchX
val dy = event.y - lastTouchY
matrix.postTranslate(dx, dy)
}
Mode.ZOOM -> {
val newDistance = getDistance(event)
if (newDistance > 10f) {
matrix.set(savedMatrix)
val scale = newDistance / startDistance
matrix.postScale(scale, scale, midPoint.x, midPoint.y)
}
}
else -> {}
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
mode = Mode.NONE
}
}
invalidate()
return true
}
private fun getDistance(event: MotionEvent): Float {
val x = event.getX(0) - event.getX(1)
val y = event.getY(0) - event.getY(1)
return sqrt(x * x + y * y)
}
private fun getMidPoint(point: PointF, event: MotionEvent) {
val x = event.getX(0) + event.getX(1)
val y = event.getY(0) + event.getY(1)
point.set(x / 2, y / 2)
}
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
// 双击重置
matrix.reset()
invalidate()
return true
}
override fun onLongPress(e: MotionEvent) {
// 长按事件
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
// 可以在这里添加长按逻辑
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
// 惯性滑动
startFlingAnimation(velocityX, velocityY)
return true
}
}
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
val scaleFactor = detector.scaleFactor
matrix.postScale(
scaleFactor,
scaleFactor,
detector.focusX,
detector.focusY
)
invalidate()
return true
}
}
private fun startFlingAnimation(velocityX: Float, velocityY: Float) {
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 1000
interpolator = DecelerateInterpolator()
addUpdateListener { animator ->
val fraction = animator.animatedValue as Float
val currentVelocityX = velocityX * (1 - fraction)
val currentVelocityY = velocityY * (1 - fraction)
matrix.postTranslate(
currentVelocityX * 0.01f,
currentVelocityY * 0.01f
)
invalidate()
}
}
animator.start()
}
}
滑动选择器 View
class SliderView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val thumbPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var minValue = 0f
private var maxValue = 100f
private var currentValue = 50f
private var thumbRadius = 30f
private var trackHeight = 8f
private var trackRect = RectF()
private var progressRect = RectF()
private var thumbX = 0f
private var thumbY = 0f
private var isDragging = false
private var onValueChangeListener: ((Float) -> Unit)? = null
init {
initPaints()
}
private fun initPaints() {
trackPaint.apply {
color = Color.LTGRAY
style = Paint.Style.FILL
}
progressPaint.apply {
color = Color.BLUE
style = Paint.Style.FILL
}
thumbPaint.apply {
color = Color.WHITE
style = Paint.Style.FILL
setShadowLayer(8f, 0f, 4f, Color.argb(50, 0, 0, 0))
}
textPaint.apply {
color = Color.BLACK
textSize = 32f
textAlign = Paint.Align.CENTER
}
setLayerType(LAYER_TYPE_SOFTWARE, null) // 启用阴影
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val centerY = h / 2f
val left = thumbRadius + 20f
val right = w - thumbRadius - 20f
trackRect.set(
left,
centerY - trackHeight / 2,
right,
centerY + trackHeight / 2
)
thumbY = centerY
updateThumbPosition()
}
private fun updateThumbPosition() {
val progress = (currentValue - minValue) / (maxValue - minValue)
thumbX = trackRect.left + progress * trackRect.width()
progressRect.set(
trackRect.left,
trackRect.top,
thumbX,
trackRect.bottom
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制轨道
canvas.drawRoundRect(trackRect, trackHeight / 2, trackHeight / 2, trackPaint)
// 绘制进度
canvas.drawRoundRect(progressRect, trackHeight / 2, trackHeight / 2, progressPaint)
// 绘制滑块
canvas.drawCircle(thumbX, thumbY, thumbRadius, thumbPaint)
// 绘制数值文本
val text = currentValue.toInt().toString()
canvas.drawText(text, thumbX, thumbY - thumbRadius - 20f, textPaint)
// 绘制刻度
drawScale(canvas)
}
private fun drawScale(canvas: Canvas) {
val scaleCount = 5
val scalePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.GRAY
strokeWidth = 2f
}
for (i in 0..scaleCount) {
val x = trackRect.left + i * trackRect.width() / scaleCount
val startY = trackRect.bottom + 10f
val endY = startY + 15f
canvas.drawLine(x, startY, x, endY, scalePaint)
// 绘制刻度值
val value = minValue + i * (maxValue - minValue) / scaleCount
val scaleText = value.toInt().toString()
textPaint.textSize = 24f
canvas.drawText(scaleText, x, endY + 30f, textPaint)
textPaint.textSize = 32f
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val distance = sqrt(
(event.x - thumbX).pow(2) + (event.y - thumbY).pow(2)
)
if (distance <= thumbRadius * 1.5f) {
isDragging = true
parent.requestDisallowInterceptTouchEvent(true)
// 添加按下动画
animateThumbScale(1.2f)
return true
}
}
MotionEvent.ACTION_MOVE -> {
if (isDragging) {
val newThumbX = event.x.coerceIn(trackRect.left, trackRect.right)
val progress = (newThumbX - trackRect.left) / trackRect.width()
val newValue = minValue + progress * (maxValue - minValue)
setValue(newValue)
return true
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
if (isDragging) {
isDragging = false
parent.requestDisallowInterceptTouchEvent(false)
// 添加释放动画
animateThumbScale(1f)
return true
}
}
}
return super.onTouchEvent(event)
}
private fun animateThumbScale(targetScale: Float) {
val animator = ValueAnimator.ofFloat(1f, targetScale).apply {
duration = 150
interpolator = OvershootInterpolator()
addUpdateListener {
// 这里可以实现滑块缩放动画
invalidate()
}
}
animator.start()
}
fun setValue(value: Float) {
val newValue = value.coerceIn(minValue, maxValue)
if (newValue != currentValue) {
currentValue = newValue
updateThumbPosition()
onValueChangeListener?.invoke(currentValue)
invalidate()
}
}
fun setValueRange(min: Float, max: Float) {
minValue = min
maxValue = max
currentValue = currentValue.coerceIn(minValue, maxValue)
updateThumbPosition()
invalidate()
}
fun setOnValueChangeListener(listener: (Float) -> Unit) {
onValueChangeListener = listener
}
}
ViewGroup 自定义
流式布局 FlowLayout
class FlowLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
private var horizontalSpacing = 10.dp
private var verticalSpacing = 10.dp
private val childBounds = mutableListOf<Rect>()
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.FlowLayout,
0, 0
).apply {
try {
horizontalSpacing = getDimensionPixelSize(
R.styleable.FlowLayout_horizontalSpacing,
10.dp
)
verticalSpacing = getDimensionPixelSize(
R.styleable.FlowLayout_verticalSpacing,
10.dp
)
} finally {
recycle()
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
// 测量所有子 View
measureChildren(widthMeasureSpec, heightMeasureSpec)
// 计算布局
val result = calculateLayout(widthSize)
val finalWidth = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
else -> result.width
}
val finalHeight = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
else -> result.height
}
setMeasuredDimension(finalWidth, finalHeight)
}
private fun calculateLayout(maxWidth: Int): LayoutResult {
childBounds.clear()
var currentLineWidth = paddingLeft
var currentLineHeight = 0
var totalHeight = paddingTop
var maxLineWidth = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility == GONE) continue
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
// 检查是否需要换行
if (currentLineWidth + childWidth + paddingRight > maxWidth && currentLineWidth > paddingLeft) {
// 换行
maxLineWidth = maxOf(maxLineWidth, currentLineWidth - horizontalSpacing)
totalHeight += currentLineHeight + verticalSpacing
currentLineWidth = paddingLeft
currentLineHeight = 0
}
// 记录子 View 的位置
val left = currentLineWidth
val top = totalHeight
val right = left + childWidth
val bottom = top + childHeight
childBounds.add(Rect(left, top, right, bottom))
currentLineWidth += childWidth + horizontalSpacing
currentLineHeight = maxOf(currentLineHeight, childHeight)
}
// 处理最后一行
maxLineWidth = maxOf(maxLineWidth, currentLineWidth - horizontalSpacing)
totalHeight += currentLineHeight + paddingBottom
return LayoutResult(
width = maxLineWidth + paddingRight,
height = totalHeight
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var childIndex = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility == GONE) continue
if (childIndex < childBounds.size) {
val bounds = childBounds[childIndex]
child.layout(bounds.left, bounds.top, bounds.right, bounds.bottom)
childIndex++
}
}
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
override fun generateDefaultLayoutParams(): LayoutParams {
return MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
}
override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
return MarginLayoutParams(p)
}
override fun checkLayoutParams(p: LayoutParams?): Boolean {
return p is MarginLayoutParams
}
private data class LayoutResult(
val width: Int,
val height: Int
)
private val Int.dp: Int
get() = (this * resources.displayMetrics.density).toInt()
}
性能优化
绘制优化
class OptimizedCustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 缓存绘制对象
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val path = Path()
private val rect = RectF()
// 缓存计算结果
private var cachedWidth = 0
private var cachedHeight = 0
private var cachedBitmap: Bitmap? = null
private var cachedCanvas: Canvas? = null
// 脏区域标记
private var isDirty = true
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (w != cachedWidth || h != cachedHeight) {
cachedWidth = w
cachedHeight = h
// 重新创建缓存位图
cachedBitmap?.recycle()
cachedBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
cachedCanvas = Canvas(cachedBitmap!!)
isDirty = true
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 使用缓存位图
if (isDirty) {
drawToCache()
isDirty = false
}
cachedBitmap?.let { bitmap ->
canvas.drawBitmap(bitmap, 0f, 0f, null)
}
}
private fun drawToCache() {
cachedCanvas?.let { canvas ->
// 清除之前的内容
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
// 绘制复杂内容到缓存
drawComplexContent(canvas)
}
}
private fun drawComplexContent(canvas: Canvas) {
// 复杂的绘制逻辑
paint.color = Color.BLUE
// 避免在 onDraw 中创建对象
rect.set(50f, 50f, width - 50f, height - 50f)
canvas.drawRoundRect(rect, 20f, 20f, paint)
// 使用路径缓存
path.reset()
path.moveTo(width / 2f, 100f)
path.lineTo(width - 100f, height - 100f)
path.lineTo(100f, height - 100f)
path.close()
paint.color = Color.RED
canvas.drawPath(path, paint)
}
// 提供方法标记需要重绘
fun markDirty() {
isDirty = true
invalidate()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
// 释放缓存资源
cachedBitmap?.recycle()
cachedBitmap = null
cachedCanvas = null
}
}
内存优化
class MemoryOptimizedView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
companion object {
// 使用对象池避免频繁创建对象
private val rectPool = Pools.SimplePool<RectF>(10)
private val paintPool = Pools.SimplePool<Paint>(5)
private fun acquireRect(): RectF {
return rectPool.acquire() ?: RectF()
}
private fun releaseRect(rect: RectF) {
rect.setEmpty()
rectPool.release(rect)
}
private fun acquirePaint(): Paint {
return paintPool.acquire() ?: Paint(Paint.ANTI_ALIAS_FLAG)
}
private fun releasePaint(paint: Paint) {
paint.reset()
paintPool.release(paint)
}
}
// 使用弱引用避免内存泄漏
private var listenerRef: WeakReference<OnCustomEventListener>? = null
interface OnCustomEventListener {
fun onCustomEvent()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 使用对象池
val rect = acquireRect()
val paint = acquirePaint()
try {
rect.set(0f, 0f, width.toFloat(), height.toFloat())
paint.color = Color.BLUE
canvas.drawRect(rect, paint)
} finally {
// 确保释放对象
releaseRect(rect)
releasePaint(paint)
}
}
fun setOnCustomEventListener(listener: OnCustomEventListener?) {
listenerRef = listener?.let { WeakReference(it) }
}
private fun notifyCustomEvent() {
listenerRef?.get()?.onCustomEvent()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
// 清理引用
listenerRef?.clear()
listenerRef = null
}
}
总结
自定义 View 开发是 Android 开发中的高级技能,通过本文的深入探讨,我们学习了:
- 基础概念:View 的绘制流程、测量、布局和绘制
- 自定义属性:如何定义和使用自定义属性
- 复杂效果:波浪动画、雷达扫描等高级视觉效果
- 触摸处理:手势识别、拖拽、缩放等交互功能
- ViewGroup:自定义布局管理器的实现
- 性能优化:绘制优化、内存管理等最佳实践
掌握这些技能后,开发者可以创建出功能强大、性能优秀的自定义 UI 组件,为用户提供独特而流畅的交互体验。在实际开发中,应该根据具体需求选择合适的实现方案,并始终关注性能和用户体验。
上一篇 下一篇