Android 自定义View(一):View是什么?如何创建自定义view,自定义属性等

目录

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的知识点


  1. 交互:触摸(TouchEvent)、动画(Animation)

  2. 封装:尺寸(measure)、属性(attributes)

  3. 绘图:Canvas、Paint

  4. 性能:OnDraw,Sufaceview(副线程绘图)


四、View的工作流程是怎么样的?


  1. 调用 setContentView() 方法设置布局或在 XML 布局文件中定义视图层次结构。
  2. 系统会解析布局文件并创建相应的 View 对象,形成一个视图树(View Hierarchy)。
  3. 在 Activity 或 Fragment 的生命周期方法中调用 onCreate()onStart 和 onResume()` 方法后,系统会开始绘制视图。
  4. 系统从根视图开始遍历整个视图树,并依次调用每个 View 的 draw() 方法进行绘制。
  5. draw() 方法中,View 会先绘制自身的内容,然后递归地绘制它的子视图。
  6. 绘制过中,系统会将每个 View 的绘制结果保存到一个称为 Canvas 的画布对象中。
  7. 最后,系统将 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()
                }
            }
    }
    ...
  1. 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,表示不使用特定的主题资源,并且不强制使用默认样式。

  2. val iconId = getResourceId(R.styleable.MyEditText_clenIcon, 0)尝试从自定义属性集中获取名为clenIcon的属性值

  3. 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" />

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前期后期

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值