如何自定义VIew
1. 有哪些自定义View的手段
- 继承View重写OnDraw方法
- 继承ViewGroup派生特殊的Layout
- 继承现有特殊的View(例如:TextView)
- 继承现有特殊的ViewGroup(例如:LeanerLayout)
2. 自定义View的难点
-
需要比较清楚的了解View的绘制过程
也就是:onMeasure、onLayout、onDraw 这三个过程。这三个过程涉及东西会比较多,会单独的出一篇文档来讲解这个过程。 -
直接继承View的自定义View的需要重写onMeasure、onDraw方法。
需要考虑 wrap_content、padding 的情况。由于View自身在绘制过程中的默认实现,如果不处理这两种情况,设置的值就不会生效。原因可以看下面View中的 getDefaultSize() 源码:public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
从上面的代码,可以看到设置 wrap_content 的效果和设置了 match_parent的效果是一样的。因为 View 自身默认实现的原因,所以使得 wrap_content 与 match_parent 效果一样。那为啥padding也会有影响呢?
第一,ViewGroup 中 margin 和 padding 对绘制过程的影响。如下 ViewGroup 的 measureChildWithMargins 的过程:
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
从上我们可以知道,View/ViewGroup设置的 padding 得自己处理,而设置的 margin 是由父View来处理的。即,ViewGroup 在计算子View的时候需要把自身的Padding给考虑进去,而子View只有在 onDraw 的时候需要,因为View不需要考虑自身的padding对子View的可用size造成的影响。
其次,在 padding 表示的是内容与边框之间的距离。它也属于 View 自身宽度的一个组成部分。如果在 onDraw 绘制的时候,不针对这部分进行处理,padding也不会生效。
3. 举个栗子
自定义一个可以自定义圆角和边框的FrameLayout
class RoundFrameLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0): FrameLayout(context, attrs, defStyleAttr) {
init {
// 允许重写onDraw
setWillNotDraw(false)
}
var radius = 0f // 自定义传入的角度
var borderColor: Int = Color.WHITE // 边框颜色
/**
* 单位:px
*/
var borderWidth: Int = 0
private var mPath: Path? = null
private val mPaint by lazy {
Paint()
}
override fun onDraw(canvas: Canvas?) {
DebugManager.logError("${canvas == null}, $radius, $borderWidth, $borderColor")
drawRectBorder(canvas)
drawRoundRect(canvas)
super.onDraw(canvas)
}
/**
* 通过剪切,给矩形绘制指定的圆角
*/
private fun drawRoundRect(canvas: Canvas?) {
canvas?.let { cav ->
DebugManager.logError("$radius, $borderWidth, $borderColor")
if (radius != 0f) {
if (mPath == null && radius > 0f) {
// 获取 宽高
val w = this.width
val h = this.height
if (w > 0 && h > 0) {
val radiis = floatArrayOf(radius, radius, radius, radius, radius, radius, radius, radius)
// 创建path
mPath = Path()
mPath?.addRoundRect(RectF(0f + borderWidth, 0f + borderWidth, w.toFloat() - borderWidth, h.toFloat() - borderWidth), radiis, Path.Direction.CW)
}
}
mPath?.let {
cav.clipPath(it)
}
}
}
}
/**
* 绘制边框
*/
private fun drawRectBorder(canvas: Canvas?) {
canvas?.let { cav ->
// 获取 宽高
val w = this.width
val h = this.height
if (borderWidth > 0 && w > 0 && h > 0) {
mPaint.strokeWidth = borderWidth.toFloat()
mPaint.color = borderColor
mPaint.style = Paint.Style.STROKE
val rect = RectF(0f + borderWidth, 0f + borderWidth, w.toFloat() - borderWidth, h.toFloat() - borderWidth)
cav.drawRoundRect(rect, radius, radius, mPaint)
}
}
}
}