这篇文章实际上主要是介绍如何自定义View的,至于绘制什么图形只是我把以前写的View里带的功能顺便提上来的,不重要。效果图如下,作为百度地图中的覆盖物呈现。圆的大小,扇形的数量、颜色、角度都不是写死固定的,可按需求修改,好像有点扯远了。。。
说说View吧,一般来说自定义View需要重写onMeasure()、onDraw()这两个方法。这两个方法干嘛用的呢?打个比方,你要量身定制一套衣服,那么店家肯定要先帮你测下身高、腰围什么的,其中onMeasure()就是干这事的,他要测量宽高尺寸,xml布局文件里面虽然已经指定好了宽高尺寸,但要知道所谓的指定是wrap_content、match_parent和固定尺寸,就好像有的人报身高的时候会说1米7左右吧,或是跟姚明一样就行了,还有老实报身高的。不过onMeasure的服务也很到位,测量模式也有三种UNSPECIFIED(父容器没有对View有任何限制,当前View可以任意取尺寸)、EXACTLY(当前的尺寸就是View应该取的尺寸)和AT_MOST(合身,取View能取的最大尺寸),不过不好意思,两者并不是对应关系(尴尬)。
match_parent和固定尺寸对应EXACTLY,因为match_parent实际上也是确定的。
wrap_content对应的是AT_MOST,这个也好理解,因为每次设置为这个属性时,view表现出来的总是大小刚刚好。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val measureHeightSize = View.MeasureSpec.getSize(heightMeasureSpec)
val measureWidthtSize = View.MeasureSpec.getSize(widthMeasureSpec)
val measureHeightModel = View.MeasureSpec.getMode(heightMeasureSpec)
val measureWidthtModel = View.MeasureSpec.getMode(widthMeasureSpec)
var defaultHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
30f, context.resources.displayMetrics).toInt()
var defaultWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
30f, context.resources.displayMetrics).toInt()
if (measureHeightModel == View.MeasureSpec.AT_MOST) {
defaultHeight = Math.min(defaultHeight, measureHeightSize)
} else if (measureHeightModel == View.MeasureSpec.EXACTLY) {
defaultHeight = measureHeightSize
}
if (measureWidthtModel == View.MeasureSpec.AT_MOST) {
defaultWidth = Math.min(defaultWidth, measureWidthtSize)
} else if (measureWidthtModel == View.MeasureSpec.EXACTLY) {
defaultWidth = measureWidthtSize
}
setMeasuredDimension(defaultWidth, defaultHeight)
}
val measureHeightSize = View.MeasureSpec.getSize(heightMeasureSpec)
val measureWidthtSize = View.MeasureSpec.getSize(widthMeasureSpec)
取测量尺寸
val measureHeightModel = View.MeasureSpec.getMode(heightMeasureSpec)
val measureWidthtModel = View.MeasureSpec.getMode(widthMeasureSpec)
取测量模式
注意上面得到的测量尺寸是父View提供的参考大小。
我给他设置了默认大小为30dp,当测量模式为UNSPECIFIED时(没指定那就默认吧);当测量模式为AT_MOST时(wrap_content)取两者最小值;当测量模式为EXACTLY(固定大小),那么测量尺寸就是我要的尺寸了。最后记得setMeasureDimension(width, height)。
好了,量完身,方形布料也剪好了,接下来要裁剪布料设计款式,穿针引线,缝点艺术图案上去,比如把衣服做成圆形和扇形,加点花花绿绿的颜色什么的。。。
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
maxWidth = this.measuredWidth
maxHeight = this.measuredHeight
maxWidth = Math.min(maxWidth, maxHeight)
paint.isAntiAlias = true
var more = 0f
if (mSectRoundable) {
paint.color = mSectorRoundColor
paint.style = Paint.Style.STROKE
val rectF = RectF(mCentreX, mCentreY,
maxWidth + mCentreX, maxWidth + mCentreY)
canvas.drawArc(rectF, mStartAngle, mSweepAngle, true, paint)
more = mSectorRoundWidth
}
if (mRoundable) {
more = Math.max(more, mRoundWidth)
}
paint.color = sectorColor
paint.style = Paint.Style.FILL
val rectF1 = RectF(mCentreX + more, mCentreY + more,
maxWidth + mCentreX - more, maxWidth + mCentreY - more)
canvas.drawArc(rectF1, mStartAngle, mSweepAngle, true, paint)
if (mRoundable) {
paint.style = Paint.Style.STROKE
paint.color = mRoundColor
paint.strokeWidth = mRoundWidth
canvas.drawCircle((maxWidth / 2).toFloat(), (maxWidth / 2).toFloat(), (maxWidth - mRoundWidth) / 2, paint)
}
}
作为裁缝,你可以在这块布上做任何事情,包括剪出你想要的形状,底色,前提是你要知道如何使用绘制工具和染色剂。说到这里我有些后悔为什么当初不比喻成裁纸和画画呢。。。
maxWidth = this.measuredWidth 获取画布宽度
canvas.drawArc()绘制扇形,里面有4个参数RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint,第一个参数是RectF,就是一个矩形,RectF(float left, float top, float right, float bottom),矩形的性质大家都知道了,所以用一个左上角和右下角这两个对角线的点就可以画出矩形了,可能有人要问,为什么不是另外两个点呢,嗯,我也不知道。这个矩形是一个正方形,我们要画的圆就画在里面,是矩形里面最大的圆。第二参数startAngle是起始度数。第三个sweepAngle叫角度差值,可不是结束的读书大小,这里呢就是扇形的度数了。useCenter就问你圆是不是要画在矩形中间。paint就是画笔,决定画出来的是什么线(包括颜色、粗细、虚实等等)。
另外有一点要说的,如果要在布局文件上用我们的自定义的属性,记得先在res/values/attr.xml文件下或者styles.xml(如果没有请自己新建,文件名理论上并没有严格规定)声明一个自定义属性。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SectorView">
<attr name="radius" format="integer"/>
<attr name="centreX" format="dimension"/>
<attr name="centreY" format="dimension"/>
<attr name="roundColor" format="color"/>
<attr name="sect_roundWidth" format="dimension"/>
<attr name="sect_roundColor" format="color"/>
<attr name="sectorColor" format="color"/>
<attr name="startAngle" format="float"/>
<attr name="sweepAngle" format="float"/>
<attr name="roundShowable" format="boolean"/>
<attr name="sect_roundShowable" format="boolean"/>
<attr name="roundWidth" format="dimension"/>
</declare-styleable>
</resources>
name建议跟自定义View名取一样的,其他自定义view的自定义属性声明也可以添加进去,只要保证格式一样就行了。在构造函数里获取到属性集合的标签。R.styleable.SectorView就是我们上面声明的。typedArray.getColor里面有两个参数,前面一个表示我们要获取的是哪个属性,后面一个在获取不到指定属性时会赋予mRoundColor,是一个默认值。用完后记得typedArray.recycle回收,要养成勤俭节约的好习惯。
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.SectorView) mRoundColor = typedArray.getColor(R.styleable.SectorView_roundColor, Color.GRAY) typedArray.recycle()
另外布局文件中的最外层的layout一定要加上xmlns:app="http://schemas.android.com/apk/res-auto",冒号后面的app可以改成别的名字,随你喜好,然后引用的时候是以app:属性名称的形式。这样就可以取到布局文件里指定的值了。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.intelligenceSecuritySystem.ui.adjustPlan.AddAjustPlanActivity">
<com.intelligenceSecuritySystem.view.SectorView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:sweepAngle="30"
/>
</android.support.constraint.ConstraintLayout>