一、定位
SpecMode
模式 | 规则 |
---|---|
EXACTLY | 父容器为 View 制定了精确的大小, View 的最终大小就是 SpecSize 所指定的值,它对应于 match_parent 和具体的数值这两种模式 |
AT_MOST | 父容器指定了一个可用大小即 SpecSize,子View只要不超过这个尺寸即可 ,对应的场景就是wrap_content |
UNSPECIFIED | 不限制模式,父视图对子视图没有任何约束,要多大给多大。 |
onMeasure()决定View的大小
// 所希望的尺寸
val desiredWidth = 100
val desiredHeight = 100
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 获取模式
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
var height: Int;
var width: Int;
if (widthMode == MeasureSpec.EXACTLY) {//固定
width = widthMeasureSpec
} else if (widthMode == MeasureSpec.AT_MOST) {//最大
width = Math.min(widthMeasureSpec, desiredWidth)
} else {//不限制
width = desiredWidth
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightMeasureSpec
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(heightMeasureSpec, desiredHeight)
} else {
height = desiredHeight
}
setMeasuredDimension(height, width)
}
onLayout
onLayout()决定View在ViewGroup中的位置
onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int)四个参数分别为该控件相对于父控件的左(mLeft)上(mTop)右(mRight)下(mBottom)四个位置的坐标(见下图),也可以通过View的getLeft()、getTop()、getRight()、getBottom()获取上述距离。通常当ViewGroup/View有多个子view的时候才会需要重写该方法。
二、绘制流程
AttributeSet
View的构造函数constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {},可以获取到view在xml文件中被设定的属性以及属性值。
第一步:自定义属性:
需要创建attrs.xml文件,为特定的view增加属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="textColor" format="color" />
<declare-styleable name="CustomView">
<attr name="textColor" />
</declare-styleable>
</resources>
第二步:在xml文件中为该自定义的view增加自定义的属性值。
记得要增加xmlns:app这一行。当然你也可以给它改名为xmlns:custom或其他名字。
</…………
xmlns:app="http://schemas.android.com/apk/res-auto"/>
<!-- xmlns:custom="http://schemas.android.com/apk/res-auto"-->
<com.accmobile.kotlintestpro.custom.CustomView
android:id="@+id/c_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:textColor="@color/white" />
<!-- custom:textColor="@color/white"-->
第三步: AttributeSet / TypedArray获取方式:
// 直接从AttributeSet获取
val count = attrs.attributeCount
var i = 0
while (i < count) {
Log.e(TAG, "attrName = " + attrs.getAttributeName(i) + " , attrVal = " + attrs.getAttributeValue(i));
i++
}
// 通过TypedArray获取
val ta: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView)
pointColor = ta.getColor(R.styleable.CustomView_pointColor, Color.Black.toArgb())
ta.recycle()
最后,通过获取到的属性值在代码中进行操作。
onDraw
主要是重写onDraw(canvas: Canvas?)函数。
Canvas类提供了很多的绘制方法,详见参考文章。
比较常用的函数是drawBitmap(Bitmap bitmap,float left, float top, Paint paint):在指定点(x,y)使用指定的画笔paint绘制位图。
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
paint.color = pointColor
if (pointX != 0f) {
canvas!!.drawCircle(pointX, pointY, 50f, paint)
}
}
三、点击事件
通常会定义一个最短移动距离大小,当手指移动距离小于这个数,则认为是点击事件,否则认为是滑动事件:
fun getTouchSlop(context: Context?): Int {
return ViewConfiguration.get(context!!).scaledTouchSlop
}
通常会重写onTouchEvent方法。
我们首先需要了解MotionEvent的常用方法:
getAction():获取事件类型。常用的几种事件类型在下方代码中。
getX():获得触摸点在当前 View 的 X 轴坐标。
getY():获得触摸点在当前 View 的 Y 轴坐标。
getRawX():获得触摸点在整个屏幕的 X 轴坐标。
getRawY():获得触摸点在整个屏幕的 Y 轴坐标。
单点点击:
override fun onTouchEvent(event: MotionEvent?): Boolean {
this.pointX = event!!.x
this.pointY = event.y
Log.d(TAG, "$pointX,$pointY")
when (event.action) {
/******************单点事件******************/
MotionEvent.ACTION_DOWN -> {
Log.d(TAG, "手指按下")
downX = event.x
downY = event.y
}
MotionEvent.ACTION_UP -> {
//手指抬起
val distanceX = abs(event.x - downX)
val distanceY = abs(event.y - downY)
if (distanceX < touchSlop && distanceY < touchSlop) {
Toast.makeText(context, "点击", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "滑动", Toast.LENGTH_SHORT).show()
}
pointX = 0f
}
MotionEvent.ACTION_MOVE -> {
Log.d(TAG, "手指滑动")
}
MotionEvent.ACTION_CANCEL -> {
Log.d(TAG, "事件被拦截")
}
MotionEvent.ACTION_OUTSIDE -> {
Log.d(TAG, "超出区域")
}
}
this.invalidate()// 更新视图
return true
}
多点点击:
详见下文