一、实现步骤
1. 继承View类或其子类
2. 复写view中的一些函数
3. 为自定义View类增加属性(两种方式)
4. 绘制控件(导入布局)
5. 响应用户事件
6. 定义回调函数(根据自己需求来选择)
二、哪些方法需要被重写
-
onDraw()
view中onDraw()是个空函数,也就是说具体的视图都要覆写该函数来实现自己的绘制。对于ViewGroup则不需要实现该函数,因为作为容器是“没有内容“的(但必须实现dispatchDraw()函数,告诉子view绘制自己)。
-
onLayout()
主要是为viewGroup类型布局子视图用的,在View中这个函数为空函数。
-
onMeasure()
用于计算视图大小(即长和宽)的方式,并通过setMeasuredDimension (width, height) 保存计算结果。
-
onTouchEvent()
定义触屏事件来响应用户操作。
还有一些不常用的方法:
onKeyDown()当按下某个键盘时
onKeyUp()当松开某个键盘时
onTrackballEvent()当发生轨迹球事件时
onSizeChange()当该组件的大小被改变时
onFinishInflate()回调方法,当应用从XML加载该组件并用它构建界面之后调用的方法
onWindowFocusChanged(boolean)当该组件得到、失去焦点时
onAttachedToWindow()当把该组件放入到某个窗口时
onDetachedFromWindow()当把该组件从某个窗口上分离时触发的方法
onWindowVisibilityChanged(int)当包含该组件的窗口的可见性发生改变时触发的方法
View的绘制流程
二、哪些方法需要被重写
-
onDraw()
view中onDraw()是个空函数,也就是说具体的视图都要覆写该函数来实现自己的绘制。对于ViewGroup则不需要实现该函数,因为作为容器是“没有内容“的(但必须实现dispatchDraw()函数,告诉子view绘制自己)。
-
onLayout()
主要是为viewGroup类型布局子视图用的,在View中这个函数为空函数。
-
onMeasure()
用于计算视图大小(即长和宽)的方式,并通过setMeasuredDimension (width, height) 保存计算结果。
-
onTouchEvent()
定义触屏事件来响应用户操作。
还有一些不常用的方法:
onKeyDown()当按下某个键盘时
onKeyUp()当松开某个键盘时
onTrackballEvent()当发生轨迹球事件时
onSizeChange()当该组件的大小被改变时
onFinishInflate()回调方法,当应用从XML加载该组件并用它构建界面之后调用的方法
onWindowFocusChanged(boolean)当该组件得到、失去焦点时
onAttachedToWindow()当把该组件放入到某个窗口时
onDetachedFromWindow()当把该组件从某个窗口上分离时触发的方法
onWindowVisibilityChanged(int)当包含该组件的窗口的可见性发生改变时触发的方法
View的绘制流程
我们调用requestLayout()的时候,会触发measure 和 layout 过程,调用invalidate,会执行 draw 过程。
三.自定义控件的三种方式
1. 继承已有的控件
当要实现的控件和已有的控件在很多方面比较类似, 通过对已有控件的扩展来满足要求。
2. 继承一个布局文件
一般用于自定义组合控件,在构造函数中通过inflater和addView()方法加载自定义控件的布局文件形成图形界面(不需要onDraw方法)。
3.继承view
通过onDraw方法来绘制出组件界面。
四.自定义属性
attr_zxy.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- 功能:自定义attr -->
<resources>
<declare-styleable name="zxy">
<attr name="zxyTextColor" format="color" />
</declare-styleable>
</resources>
ZxyTextView.kt
/**
* Created by zxy on 2020/4/18 0018 16:02
* ******************************************
* * 简单的自定义View
* ******************************************
*/
class ZxyTextView : androidx.appcompat.widget.AppCompatTextView {
var zxyColor: Int = Color.BLACK
constructor(mContext: Context?, attrs: AttributeSet?) : super(mContext,attrs) {
//取出自定义Attr的属性分组
val array =
context!!.theme.obtainStyledAttributes(attrs, R.styleable.zxy, R.attr.zxyTextColor, 0)
//遍历,这边只有一个自定义attr属性,循环可有可无
for (index in 0..array.indexCount step 1) {
when (array.getIndex(index)) {
R.styleable.zxy_zxyTextColor ->
//得到xml中传入的颜色值
zxyColor = array.getColor(R.styleable.zxy_zxyTextColor, Color.BLACK)
}
}
array.recycle()//回收
init()
}
/**
* 初始化内容
*/
private fun init() {
this.setTextColor(zxyColor)//设置TextView的文字颜色为xml传入的颜色
}
}
布局文件中的app:zxyTextColor="#1425DF" 属性为暴露出去的属性
<com.zxy.zxypermissionsdispatcher.ZxyTextView
android:id="@+id/text9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="TextView9TextView9"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/text8"
app:zxyTextColor="#1425DF"
/>
其他属性示例:
attr定义的属性
<attr name="attr_zxy" format="reference" />
<declare-styleable name="ZXY">
<!-- 颜色 -->
<attr name="zxyTitleTextColor" format="color" />
<attr name="zxyRightTextColor" format="color" />
<attr name="zxyBackgroundColor" format="color" />
<!-- 图片 -->
<attr name="zxyLeftImg" format="reference" />
<attr name="zxyRightImg1" format="reference" />
<attr name="zxyRightImg2" format="reference" />
<!-- 文案 -->
<attr name="zxyTitleText" format="string" />
<attr name="zxyRightText" format="string" />
<!-- 尺寸 -->
<attr name = "zxyTitleSize" format = "dimension" />
<attr name = "zxyRightTitleSize" format = "dimension" />
</declare-styleable>
获取布局的属性
when (array.getIndex(index)) {
R.styleable.ZXY_zxyBackgroundColor ->
backgroundColor = array.getColor(R.styleable.ZXY_zxyBackgroundColor, Color.BLACK)
R.styleable.ZXY_zxyRightTextColor ->
rightTextColor = array.getColor(R.styleable.ZXY_zxyRightTextColor, Color.BLACK)
R.styleable.ZXY_zxyTitleTextColor ->
titleTextColor = array.getColor(R.styleable.ZXY_zxyTitleTextColor, Color.BLACK)
R.styleable.ZXY_zxyRightText ->
rightText = array.getString(R.styleable.ZXY_zxyRightText)
R.styleable.ZXY_zxyTitleText ->
titleText = array.getString(R.styleable.ZXY_zxyTitleText)
R.styleable.ZXY_zxyRightImg1 ->
rightImg1 = array.getResourceId(R.styleable.ZXY_zxyRightImg1,-1)
R.styleable.ZXY_zxyLeftImg ->
leftImg = array.getResourceId(R.styleable.ZXY_zxyLeftImg,-1)
R.styleable.ZXY_zxyRightImg2 ->
rightImg2 = array.getResourceId(R.styleable.ZXY_zxyRightImg2,-1)
R.styleable.ZXY_zxyTitleSize ->
titleSize = array.getDimensionPixelSize(R.styleable.ZXY_zxyTitleSize, 18)
R.styleable.ZXY_zxyRightTitleSize ->
rightTextSize = array.getDimensionPixelOffset(R.styleable.ZXY_zxyRightTitleSize, 18)
}
format 数据类型参考
1. reference:参考某一资源ID。
(1)属性定义:
<declare-styleable name = "名称">
<attr name = "background" format = "reference" />
</declare-styleable>
(2)属性使用:
<ImageView
android:layout_width = "42dip"
android:layout_height = "42dip"
android:background = "@drawable/图片ID"
/>
2. color:颜色值。
(1)属性定义:
<declare-styleable name = "名称">
<attr name = "textColor" format = "color" />
</declare-styleable>
(2)属性使用:
<TextView
android:layout_width = "42dip"
android:layout_height = "42dip"
android:textColor = "#00FF00"
/>
3. boolean:布尔值。
(1)属性定义:
<declare-styleable name = "名称">
<attr name = "focusable" format = "boolean" />
</declare-styleable>
(2)属性使用:
<Button
android:layout_width = "42dip"
android:layout_height = "42dip"
android:focusable = "true"
/>
4. dimension:尺寸值。
(1)属性定义:
<declare-styleable name = "名称">
<attr name = "layout_width" format = "dimension" />
</declare-styleable>
(2)属性使用:
<Button
android:layout_width = "42dip"
android:layout_height = "42dip"
/>
5. float:浮点值。
(1)属性定义:
<declare-styleable name = "AlphaAnimation">
<attr name = "fromAlpha" format = "float" />
<attr name = "toAlpha" format = "float" />
</declare-styleable>
(2)属性使用:
<alpha
android:fromAlpha = "1.0"
android:toAlpha = "0.7"
/>
6. integer:整型值。
(1)属性定义:
<declare-styleable name = "AnimatedRotateDrawable">
<attr name = "visible" />
<attr name = "frameDuration" format="integer" />
<attr name = "framesCount" format="integer" />
<attr name = "pivotX" />
<attr name = "pivotY" />
<attr name = "drawable" />
</declare-styleable>
(2)属性使用:
<animated-rotate
xmlns:android = "http://schemas.android.com/apk/res/android"
android:drawable = "@drawable/图片ID"
android:pivotX = "50%"
android:pivotY = "50%"
android:framesCount = "12"
android:frameDuration = "100"
/>
7. string:字符串。
(1)属性定义:
<declare-styleable name = "MapView">
<attr name = "apiKey" format = "string" />
</declare-styleable>
(2)属性使用:
<com.google.android.maps.MapView
android:layout_width = "fill_parent"
android:layout_height = "fill_parent"
android:apiKey = "0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g"
/>
8. fraction:百分数。
(1)属性定义:
<declare-styleable name="RotateDrawable">
<attr name = "visible" />
<attr name = "fromDegrees" format = "float" />
<attr name = "toDegrees" format = "float" />
<attr name = "pivotX" format = "fraction" />
<attr name = "pivotY" format = "fraction" />
<attr name = "drawable" />
</declare-styleable>
(2)属性使用:
<rotate
xmlns:android = "http://schemas.android.com/apk/res/android"
android:interpolator = "@anim/动画ID"
android:fromDegrees = "0"
android:toDegrees = "360"
android:pivotX = "200%"
android:pivotY = "300%"
android:duration = "5000"
android:repeatMode = "restart"
android:repeatCount = "infinite"
/>
9. enum:枚举值。
(1)属性定义:
<declare-styleable name="名称">
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>
(2)属性使用:
<LinearLayout
xmlns:android = "http://schemas.android.com/apk/res/android"
android:orientation = "vertical"
android:layout_width = "fill_parent"
android:layout_height = "fill_parent"
>
</LinearLayout>
10. flag:位或运算。
(1)属性定义:
<declare-styleable name="名称">
<attr name="windowSoftInputMode">
<flag name = "stateUnspecified" value = "0" />
<flag name = "stateUnchanged" value = "1" />
<flag name = "stateHidden" value = "2" />
<flag name = "stateAlwaysHidden" value = "3" />
<flag name = "stateVisible" value = "4" />
<flag name = "stateAlwaysVisible" value = "5" />
<flag name = "adjustUnspecified" value = "0x00" />
<flag name = "adjustResize" value = "0x10" />
<flag name = "adjustPan" value = "0x20" />
<flag name = "adjustNothing" value = "0x30" />
</attr>
</declare-styleable>
/**
* Created by zxy on 2020/9/16 14:12
* ******************************************
* * 自定义View
* ******************************************
*/
class MyView : View {
private lateinit var linePaint: Paint //默认线条画笔
private lateinit var linePaintSelect: Paint//选中线条画笔
private var startX = 200f//起始位置X
private var startY = 200f//起始位置Y
private var lineStep = 300f//线的步长
private var radius = 40f//圆圈的半径
private var defaultWidth = 0//默认View的宽度
private var defaultHeight = 0//默认View的高度
private val textSize = 40f//字体的大小
private var strokeWidthLine = 4f//画笔的宽度
var textList = mutableListOf<String>()//数据源
var number = 0//进度
var bitmap = BitmapFactory.decodeResource(resources, R.drawable.jingbi)//金币图片
constructor(mContext: Context) : super(mContext) {
init(mContext)
}
constructor(mContext: Context, attrs: AttributeSet) : super(mContext, attrs) {
init(mContext)
}
constructor(mContext: Context, attrs: AttributeSet, defStyleAttr: Int) : super(mContext, attrs, defStyleAttr) {
init(mContext)
}
private fun init(mContext: Context) {
initLinePaint(mContext)
}
private fun initLinePaint(mContext: Context) {
linePaint = Paint()
//设置是否抗锯齿;设置抗锯齿会使图像边缘更清晰一些,锯齿痕迹不会那么明显。
linePaint.isAntiAlias = true
linePaint.style = Paint.Style.FILL
//设置画笔颜色
linePaint.color = 0xffCECECE.toInt()
//设置画笔宽度
linePaint.strokeWidth = ScreenUtil.dp2px(mContext, strokeWidthLine).toFloat()
linePaint.textSize = textSize
linePaintSelect = Paint()
//设置是否抗锯齿;设置抗锯齿会使图像边缘更清晰一些,锯齿痕迹不会那么明显。
linePaintSelect.isAntiAlias = true
linePaintSelect.style = Paint.Style.FILL
//设置画笔颜色
linePaintSelect.color = 0xff3C5EF9.toInt()
//设置画笔宽度
linePaintSelect.strokeWidth = ScreenUtil.dp2px(mContext, strokeWidthLine).toFloat()
linePaintSelect.textSize = textSize
}
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(0xffffffff.toInt())
drawOld(canvas)
drawNew(canvas)
}
private fun drawOld(canvas: Canvas) {
val width = bitmap.width
val height = bitmap.height
for (index in textList.indices) {
if (index == 0) {
canvas.drawCircle(startX + (lineStep * index), startY, radius, linePaint)
canvas.drawText(textList[index], startX + (lineStep * index) - (textSize / 2), startY + radius * 2, linePaint)
canvas.drawBitmap(bitmap, startX - (width / 2) + (lineStep * index), (startY - radius * 2) - height, linePaint)
if (index != textList.size - 1)
canvas.drawLine(startX + (lineStep * index), startY, startX + (lineStep * (index + 1)), startY, linePaint)
} else {
canvas.drawCircle(startX + (lineStep * index) + radius, startY, radius, linePaint)
canvas.drawText(textList[index], startX + (lineStep * index) - (textSize / 2) + radius, startY + radius * 2, linePaint)
canvas.drawBitmap(bitmap, startX + (lineStep * index) + radius - width / 2, (startY - radius * 2) - height, linePaint)
if (index != textList.size - 1)
canvas.drawLine(startX + (lineStep * index) + (radius * 2), startY, startX + (lineStep * (index + 1)), startY, linePaint)
}
}
}
private fun drawNew(canvas: Canvas) {
if (number != 0)
for (index in textList.indices) {
val decade = number / 10 % 10 //取到十位
if (index == 0) {
canvas.drawCircle(startX + (lineStep * index), startY, radius, linePaintSelect)
canvas.drawText(textList[index], startX + (lineStep * index) - (textSize / 2), startY + radius * 2, linePaintSelect)
} else {
canvas.drawCircle(startX + (lineStep * index) + radius, startY, radius, linePaintSelect)
canvas.drawText(textList[index], startX + (lineStep * index) - (textSize / 2) + radius, startY + radius * 2, linePaintSelect)
}
if (decade > index) {
if (index != textList.size - 1)
canvas.drawLine(startX + (lineStep * index), startY, startX + (lineStep * (index + 1)), startY, linePaintSelect)
} else {
if (decade==0 || index != textList.size - 1)
canvas.drawLine(startX + (lineStep * index), startY, startX + (lineStep * index) + (number % 10 * 10 * (lineStep / 100)), startY, linePaintSelect)
break
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, defaultHeight)
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, heightSize)
} else if (heightMode == MeasureSpec.AT_MOST) {
defaultWidth = (lineStep * (textList.size - 1) + (startX * 2)).toInt()//线的宽度
defaultHeight = (bitmap.height + startY + radius * 2).toInt()
setMeasuredDimension(defaultWidth, defaultHeight)
}
}
/**
* 刷新测量View
*/
fun refreshView() {
postInvalidate()
requestLayout()
}
}