目录
1)View是什么?
2)View分类
3)View的知识点
4)View的工作流程是怎么样的?
5)案例:如何自定义View?比如我们要实现一个输入框带有清除按钮的view
6)疑问:宽高怎么样理解,xy这些?
7)如何开发自定义属性?比如X图标,我想更换成其他图标
一、View是什么?
在 Android 中,View 是用户界面基本构建块之一,View类是Android中各种组件的基类,如View是ViewGroup基类。它是一个可见的矩形区域,用于显示应用程序中的内容和与用户进行交互。View 可以包含其他 View 或 ViewGroup,并且可以具有自己的属性、样式和行。
Android中的UI组件都由View、ViewGroup组成。
Android 提供了许多内置的 View 类,如 TextView(文本视图)、Button(按钮)、ImageView视图)等。
二、View分类
单一视图: 即一个View,如TextView, 不包含子View
视图组: 即多个View组成的ViewGroup,如LinearLayout, 包含子View
三、View的知识点
-
交互:触摸(TouchEvent)、动画(Animation)
-
封装:尺寸(measure)、属性(attributes)
-
绘图:Canvas、Paint
-
性能:OnDraw,Sufaceview(副线程绘图)
四、View的工作流程是怎么样的?
- 调用
setContentView()
方法设置布局或在 XML 布局文件中定义视图层次结构。 - 系统会解析布局文件并创建相应的 View 对象,形成一个视图树(View Hierarchy)。
- 在 Activity 或 Fragment 的生命周期方法中调用
onCreate()
、onStart 和
onResume()` 方法后,系统会开始绘制视图。 - 系统从根视图开始遍历整个视图树,并依次调用每个 View 的
draw()
方法进行绘制。 - 在
draw()
方法中,View 会先绘制自身的内容,然后递归地绘制它的子视图。 - 绘制过中,系统会将每个 View 的绘制结果保存到一个称为 Canvas 的画布对象中。
- 最后,系统将 Canvas 中的绘制结果显示在屏幕上。
注意:在绘制过程中,系统还会对视图进行测量(measure)和布局(layout)操作,以确定每个视图的大小和位置。这些操作通常在 onMeasure()
和 onLayout()
方法中完成。
请注意,以上流程仅涵盖了基本的 View 绘制流程,实际情况可能因特定需求而有所不同。
五、如何自定义View?比如我们要实现一个输入框带有清除按钮的view
如下是我们想要实现的一个view
当android内置view无法满足我们的时候,我们需要自定义View。那么我们应该如何自定义?如何绘制呢?接下来,我们会通过这个案例进行讲解。
package com.example.mymediaplayer.myview
import android.content.Context
import android.content.ContextParams
import android.graphics.Rect
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.widget.EditText
import androidx.core.content.ContextCompat
import com.example.mymediaplayer.R
class MyEditText @JvmOverloads constructor(//第一步:继承EditText
context: Context, attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatEditText(context, attrs) {
private val TAG = "MyEditText"
private val iconDrawable = ContextCompat.getDrawable(context, R.drawable.baseline_clear_24)
init {
//直接设置显示出来x。drawableRight
}
override fun onTextChanged(
text: CharSequence?,
start: Int,
lengthBefore: Int,
lengthAfter: Int
) {
super.onTextChanged(text, start, lengthBefore, lengthAfter)
//第二步:当文字发生改变的时候,则调用这个方法,进行图标的显示
toggleCleanIcon()
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
//第四步:触摸的时候,并且坐标是在图标上面,才进行文字的删除。
event?.let {e->
iconDrawable?.let {
if (e.action ==MotionEvent.ACTION_UP
&& e.x>width -it.intrinsicWidth
&& e.x<width
&& e.y >height/2 -it.intrinsicHeight/2
&& e.y <height/2 +it.intrinsicHeight/2){
text?.clear()//直接调用清除了,不需要什么点击事件去监听。使用触摸事件。
}
}
}
return super.onTouchEvent(event)
}
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
super.onFocusChanged(focused, direction, previouslyFocusedRect)
//第三步:获得焦点的时候才需要显示
toggleCleanIcon()
}
//是否显示
private fun toggleCleanIcon(){
val icon = if(isFocused && text?.isNotEmpty()==true)iconDrawable else null
Log.d(TAG, "toggleCleanIcon: "+icon)
//用于设置视图(EditText)的四个方向上的可绘制对象(Drawable)。如下就是通过传入右侧的 Drawable 对象来显示在文本视图的右边。
setCompoundDrawablesRelativeWithIntrinsicBounds(null,null,icon,null)
}
}
代码解释:
1)因为我们做的是一个EditText,所以我们需要继承EditText来自定义一个新的view,那么继承EditText就需要重写他的工作构造方法。
2)当输入框有文字的时候,我们想显示一个清理图标在输入框的右边,所以我们需要重写onTextChanged方法去监听,当有文字的时候则调用setCompoundDrawablesRelativeWithIntrinsicBounds方法去设置一个drawable。因为我们本身是在EditText里面,所以我们可以直接调用setCompoundDrawablesRelativeWithIntrinsicBounds方法。
3)并且我们想,输入框获得焦点的时候才显示清理图标,如果失去焦点,那么就消失。
4)在触摸离开清理图标并且触摸的坐标是在icon的时候,则将文本清空。
疑问:宽高怎么样理解,xy这些?
override fun onTouchEvent(event: MotionEvent?): Boolean {
//第四步:触摸的时候,并且坐标是在图标上面,才进行文字的删除。
event?.let {e->
iconDrawable?.let {
if (e.action ==MotionEvent.ACTION_UP
&& e.x>width -it.intrinsicWidth
&& e.x<width
&& e.y >height/2 -it.intrinsicHeight/2
&& e.y <height/2 +it.intrinsicHeight/2){
text?.clear()//直接调用清除了,不需要什么点击事件去监听。使用触摸事件。
}
}
}
return super.onTouchEvent(event)
}
比如这一段代码,e.x就是起点位置,width是获取控件的宽度,intrinsicWidth则是获取控件的固有宽度,例如,一个ImageView的固有宽度可能是其图片的实际宽度,而一个TextView的固有宽度则可能取决于其文本内容的长度和字体大小。
比如一个输入框的宽度是300,而清除图标的宽度是40,那么e.x>width -it.intrinsicWidth的范围则是图标开始的位置,再加上e.x<width就形成了清除图标的一个宽度。也就是260~300.
e.y >height/2 -it.intrinsicHeight/2,为什么要除以2呢,105除以2=52.5,36除以2=13,52.5-13=39,就在图标的顶部,e.y <height/2 +it.intrinsicHeight/2,就是65这个位置,就是36的一般加上去。
除以2的操作是为了进行中心对齐的考量,确保比较的是中心点而不是边缘或其他位置。
七、如何开发自定义属性?比如X图标,我想更换成其他图标
(1)首先我们要创建一个attrs.xml文件
(2)然后我们为自定义控件创建自定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyEditText">
<attr name="clenIcon" format="reference"/>
</declare-styleable>
</resources>
(3)在自定义控件里面读取这个属性并进行设置。
class MyEditText @JvmOverloads constructor(//实现EditText
context: Context, attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatEditText(context, attrs) {
private val TAG = "MyEditText"
private var iconDrawable = ContextCompat.getDrawable(context, R.drawable.baseline_clear_24)
init {
context.theme.obtainStyledAttributes(attrs,R.styleable.MyEditText,0,0)
.apply {
try {
val iconId = getResourceId(R.styleable.MyEditText_clenIcon,0)
if (iconId!=0) {
iconDrawable = ContextCompat.getDrawable(context,iconId)
}
}finally {
recycle()
}
}
}
...
-
context.theme.obtainStyledAttributes(…):
(1)这行代码通过context(通常是当前视图所在的上下文,如Activity或Fragment)和theme(当前主题的引用)来获取一个TypedArray对象。这个对象允许你访问在XML布局文件中为该视图指定的自定义属性。
(2)attrs参数是一个AttributeSet对象,它包含了视图在XML中定义的所有属性。
(3)R.styleable.MyEditText是一个指向资源文件中定义的自定义属性集的引用。这个资源文件(通常位于res/values/attrs.xml)定义了可以在XML布局文件中使用的自定义属性。
(4)后面的两个0参数分别是主题资源的ID和布尔值,用于指定是否强制使用默认样式。这里都设置为0,表示不使用特定的主题资源,并且不强制使用默认样式。 -
val iconId = getResourceId(R.styleable.MyEditText_clenIcon, 0)尝试从自定义属性集中获取名为clenIcon的属性值
-
finally { recycle() }:无论是否成功读取和设置了图标,finally块中的recycle()方法都会被调用。这是为了释放TypedArray对象占用的资源,避免内存泄漏。recycle()方法是TypedArray类的一部分,用于在不再需要该对象时释放其资源。
(4)使用自定义属性
<com.example.mymediaplayer.myview.MyEditText
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="212dp"
android:hint="请输入"
app:clenIcon="@android:drawable/ic_delete"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />