作为小白的我,一直都觉得自定义View是个很难的课题,现在也是这么觉得,虽然使用起来还是挺方便,不过也是需要加入自己的想象力,无奈我想象力不太丰富,好了开始说下这个应该如何使用的吧
最近也开始使用Xmind去做一个思维导图,我觉得这样也有利于去理解,所以我也附上了这个图
顺便说,如果想更深一层去了解自定View的话,看下这位大佬的文章,分析的很透彻
https://juejin.im/post/6844903607855218702#heading-16
按照这个思维导图的顺序,开始撸代码:
1 自定义属性:
<declare-styleable name="TestView">
<attr name="test_boolean" format="boolean"></attr>
<attr name="test_string" format="string"></attr>
<attr name="test_integer" format="integer"></attr>
<attr name="test_enum" format="enum">
<enum name="top" value="1"></enum>
<enum name="bottom" value="2"></enum>
</attr>
<attr name="test_dimension" format="dimension"></attr>
</declare-styleable>
<declare-styleable name="RoundCircleProgressBar">
<attr name="color" format="color"/>
<attr name="line_width" format="dimension"/>
<attr name="radius" format="dimension"/>
<attr name="android:progress"/>
<attr name="android:textSize"/>
</declare-styleable>
然后是控件的编写,里面写得都比较详细了啦,请慢慢看,我把两个案例都放进去吧
1 TextView
class TextVIew(context: Context, attrs: AttributeSet?) :
View(context, attrs) {
val typedArray=context.obtainStyledAttributes(attrs,R.styleable.TextVIew)
private var mTextColor=0
private var mTextSize=0
private var mTextContent=""
private lateinit var mPaint: Paint
init {
//通过for循环确保用户未输入属性值的时候,能使用默认属性值
for (i in 0..typedArray.indexCount)
{
when(typedArray.getIndex(i)){
R.styleable.TextVIew_text_color->mTextColor=typedArray.getColor(R.styleable.TextVIew_text_color,0xFFFF00)
R.styleable.TextVIew_text_size->mTextSize=typedArray.getInt(R.styleable.TextVIew_text_size,dpTosp(30).toInt())
R.styleable.TextVIew_text_content->mTextContent=
typedArray.getString(R.styleable.TextVIew_text_content).toString()
}
}
typedArray.recycle()
initPaint()
}
private fun initPaint() {
mPaint= Paint()
//STROKE为空心,FILL为实心
mPaint.style=Paint.Style.STROKE
mPaint.color=-0x10000
mPaint.strokeWidth=6f
mPaint.isAntiAlias=true
}
private fun dpTosp(dpValue:Int): Float {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,dpValue.toFloat(),resources.displayMetrics)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。
//MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpec = mode + size。
val widthMode=MeasureSpec.getMode(widthMeasureSpec)
val widthSize=MeasureSpec.getSize(widthMeasureSpec)
var width=0
//EXACTLY:精准模式。父容器已经解决了子元素所需要的精准大小,这时候子 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的 match_parent 和具体的数值
//AT_MOST:最大模式。父最大模式容器指定了一个可用大小即 SpecSize,子元素最大不可以超过指定的这个值。它对应于LayoutParams 中的 wrap_content
//UNSPECIFIED:父容器不对子元素作任何约束,子 View 想要多大就给多大。这种情况一般用在系统内部,表示一种测量的状态用于类型可滑动的布局中,例如ListView之类的
width = if (widthMode==MeasureSpec.EXACTLY){
widthSize
}else {
val needWidth = measuredWidth() + paddingLeft + paddingRight
if (widthMode == MeasureSpec.AT_MOST) {
needWidth.coerceAtMost(widthSize)
} else {
needWidth
}
}
val heightMode=MeasureSpec.getMode(heightMeasureSpec)
val heightSize=MeasureSpec.getSize(heightMeasureSpec)
var height=0
height = if (heightMode==MeasureSpec.EXACTLY){
heightSize
}else{
val needHeight=measuredHeight()+paddingBottom+paddingTop
if (heightMode==MeasureSpec.AT_MOST){
needHeight.coerceAtMost(width)
}else{
needHeight
}
}
//为view传入绘制后的宽高,这个很重要哦,要不然前面的工作就白做了
setMeasuredDimension(width,height)
}
private fun measuredWidth():Int{
return 0
}
private fun measuredHeight():Int{
return 0
}
override fun onDraw(canvas: Canvas?) {
// canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2 - mPaint.getStrokeWidth() / 2, mPaint);
// mPaint.setStrokeWidth(1);
// canvas.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2, mPaint);
// canvas.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight(), mPaint);
mPaint.textSize = 72f
mPaint.style = Paint.Style.FILL
mPaint.strokeWidth = 0f
canvas?.drawText(mTextContent, 0, mTextContent.length, 0f, height.toFloat(), mPaint)
}
//使用状态的存储和复位之后,记得在xml中对应的控件需要添加id,否则控件无法复位
override fun onSaveInstanceState(): Parcelable? {
val bundle=Bundle()
//这一步是为了把当前控件的状态呢进行存储,因为在继承别的View的时候,可能他们自身已经有一套存储的方法,
// 所以为了避免使自身的存储失效,所以要在这里进行状态的存储
bundle.putParcelable(INSTANCE,super.onSaveInstanceState())
bundle.putString(KEY_TEXT,mTextContent)
return bundle
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is Bundle){
val bundle= state as Bundle
val parcelable=bundle.getParcelable<Parcelable>(INSTANCE)
super.onRestoreInstanceState(parcelable)
mTextContent = bundle.getString(KEY_TEXT).toString()
return
}
super.onRestoreInstanceState(state)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
mTextContent="哦豁"
//刷新控件
invalidate()
return true
}
companion object{
val INSTANCE="instance"
val KEY_TEXT="key_text"
}
}
2 圆形进度条
class CircleProgress(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private val mRadius: Int
private val mColor: Int
private val mLineWidth: Int
private val mTextSize: Int
private var mProgress: Int
private var mPaint: Paint? = null
private fun dp2px(dpVal: Int): Float {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dpVal.toFloat(), resources.displayMetrics
)
}
private fun initPaint() {
mPaint = Paint()
mPaint!!.isAntiAlias = true
mPaint!!.color = mColor
}
var progress: Int
get() = mProgress
set(progress) {
mProgress = progress
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
var width = 0
width = if (widthMode == MeasureSpec.EXACTLY) {
widthSize
} else {
val needWidth = measureWidth() + paddingLeft + paddingRight
if (widthMode == MeasureSpec.AT_MOST) {
needWidth.coerceAtMost(widthSize)
} else {
needWidth
}
}
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
var height = 0
height = if (heightMode == MeasureSpec.EXACTLY) {
heightSize
} else {
val needHeight = measureHeight() + paddingTop + paddingBottom
if (heightMode == MeasureSpec.AT_MOST) {
needHeight.coerceAtMost(heightSize)
} else //MeasureSpec.UNSPECIFIED
{
needHeight
}
}
setMeasuredDimension(width, height)
}
private fun measureHeight(): Int {
return mRadius * 2
}
private fun measureWidth(): Int {
return mRadius * 2
}
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
mPaint!!.style = Paint.Style.STROKE
mPaint!!.strokeWidth = mLineWidth * 1.0f / 4
val width = width
val height = height
canvas.drawCircle(
width / 2.toFloat(), height / 2.toFloat(),
width / 2 - paddingLeft - mPaint!!.strokeWidth / 2, mPaint!!
)
mPaint!!.strokeWidth = mLineWidth.toFloat()
canvas.save()
canvas.translate(paddingLeft.toFloat(), paddingTop.toFloat())
val angle = mProgress * 1.0f / 100 * 360
canvas.drawArc(
RectF(
0f,
0f,
(width - paddingLeft * 2).toFloat(),
(height - paddingLeft * 2).toFloat()
),
0f,
angle,
false,
mPaint!!
)
canvas.restore()
val text = "$mProgress%"
// text = "张鸿洋";
mPaint!!.strokeWidth = 0f
mPaint!!.textAlign = Paint.Align.CENTER
mPaint!!.textSize = mTextSize.toFloat()
val y = getHeight() / 2
val bound = Rect()
mPaint!!.getTextBounds(text, 0, text.length, bound)
val textHeight = bound.height()
canvas.drawText(
text,
0,
text.length,
getWidth() / 2.toFloat(),
y + textHeight / 2.toFloat(),
mPaint!!
)
mPaint!!.strokeWidth = 0f
// canvas.drawLine(0, height / 2, width, height / 2, mPaint);
}
override fun onSaveInstanceState(): Parcelable? {
val bundle = Bundle()
bundle.putInt(KEY_PROGRESS, mProgress)
bundle.putParcelable(INSTANCE, super.onSaveInstanceState())
return bundle
}
override fun onRestoreInstanceState(state: Parcelable) {
if (state is Bundle) {
val parcelable =
state.getParcelable<Parcelable>(INSTANCE)
super.onRestoreInstanceState(parcelable)
mProgress = state.getInt(KEY_PROGRESS)
return
}
super.onRestoreInstanceState(state)
}
companion object {
private const val INSTANCE = "instance"
private const val KEY_PROGRESS = "key_progress"
}
init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.RoundCircleProgressBar)
mRadius = ta.getDimension(R.styleable.RoundCircleProgressBar_radius, dp2px(30)).toInt()
mColor = ta.getColor(R.styleable.RoundCircleProgressBar_color, -0x10000)
mLineWidth =
ta.getDimension(R.styleable.RoundCircleProgressBar_line_width, dp2px(3)).toInt()
mTextSize =
ta.getDimension(R.styleable.RoundCircleProgressBar_android_textSize, dp2px(36)).toInt()
mProgress = ta.getInt(R.styleable.RoundCircleProgressBar_android_progress, 30)
ta.recycle()
initPaint()
}
}
emmm剩下就是在xml里面直接使用了啦!!!好了,到此结束,反正你们都不点赞,我给自己看就足够了!!