Android - 自定义view

news2024/9/23 5:14:38

为什么要自定义view?

在Android开发中有很多业务场景,原生的控件无法满足需求,并且经常也会遇到一个UI在多处重复使用情况,于是可以通过自定义View的方式来实现这些UI效果。

自定义view的分类

f0a77d115fdc4c2aa260972bc5d5c471.png自定义属性

Window

  • window是一个抽象类,它的具体实现是PhoneWindow类。

  • Android中的所有视图都是通过window来实现的。

  • 每一个window都对应着一个 view 和 ViewRootImpl,window和 view 通过ViewRootImpl来建立联系,因此,window并不是实际存在的,view是window存在的实体。

ViewRootImpl

ViewRootImpl 是 ViewSystem 和 SurfaceSystem 的桥梁,负责处理事件分发、布局、绘制等,是连接应用逻辑和系统底层的关键组件。

PhoneWindow 中创建的 DecorView对象会通过 setView 的方式设置给它,因为View 的实现是 ViewTree的形式,所以根据 DecorView遍历到所有的 View list。

ViewRootImpl 内部会获取到 Choreographer 对象,根据 Choreographer 提供的节奏 调用 View 的三大方法

Choreographer 对象:负责协调 UI 刷新和动画的执行时机,提高界面的流畅度。

  • windowManager是外界访问window的入口,windowManager常用的方法有:添加window中的view、更新view、删除view

过程总结:

在创建一个 View 时,首先会通过 WindowManager 来初始化一个窗口中的视图,其中会在WindowManagerGlobal 中创建 ViewRootImpl 。

在 PhoneWindow(窗口实现类)中,将创建一个 DecorView,DecorView 作为顶层视图容器,负责加载和展示活动的布局资源。

852ef4f9eddb478280472f7de6587636.png

DecorView

DecorView 作为顶级view,即 Android 视图树的根节点;同时也是 FrameLayout 的子类。

一般内部会包含一个竖直的LinearLayout,分为2部分:上-标题栏(titlebar)、下-内容栏(content)

作用:

显示 & 加载布局:View层的事件都先经过DecorView,再传递到View。 

在Activity中通过 setContentView()  所设置的布局文件实际是被加到内容栏之中的,成为 id = content 的 FrameLayout的唯一子view。

如何得到content?

ViewGroup content = findViewById(R.android.id.content)

如何得到我们设置的view?

content.getChildAt(0)

ViewGroup content = findViewById(android.R.id.content);
View v  = content.getChildAt(0);

7d7cc6d80c724020a3f1957b38428a4f.png

ViewRoot 

ViewRoot 是window和DectorView的连接器。它对应 ViewRootImpl 类。view的三大流程都是通过ViewRoot来实现的。

view的绘制流程是从ViewRoot的 performTraversals() 方法开始的。

performTraversals 会依次调用 performMeasure performLayout performDraw 三大方法,这三个方法会依次完成顶级view 的measure、layout、draw三大流程。

View的绘制流程从顶级View(DecorView)的ViewGroup开始,一层一层从ViewGroup至子View遍历测绘。

自上而下遍历、由父视图到子视图、每一个 ViewGroup 负责测绘它所有的子视图,而最底层的 View 会负责测绘自身。

c4f66e3f6c044e33b232055c4dd2b05e.png399da250e07041b981be81ef3d327953.png

 

View的三大流程

  • measure:测量view的宽高
  • layout:确定view在父容器中的位置
  • draw:负责将view绘制在屏幕上

measure

自定义view的测量方法,是用于测量 View 大小的重要方法。

在这个方法中,需要根据传入的宽度和高度测量规格,计算并设置 View 的宽度和高度。

ViewGroup.LayoutParams 

布局参数类,用于指定视图View 的高度(height) 和 宽度(width)等布局参数。

  • ViewGroup 的子类(RelativeLayout、LinearLayout)有其对应的 ViewGroup.LayoutParams 子类
  • 如:RelativeLayoutViewGroup.LayoutParams子类
    = RelativeLayoutParams

通过以下参数指定

android:layout_height="wrap_content"   //自适应大小  
android:layout_height="match_parent"   //与父视图等高  
android:layout_height="fill_parent"    //与父视图等高  
android:layout_height="100dip"         //精确设置高度值为 100dip  

MeasureSpec

测量规格(MeasureSpec)是由测量模式(mode)和测量大小(size)组成,共32位(int类型),其中:

  • 测量模式(mode):占测量规格(MeasureSpec)的高2位;
  • 测量大小(size):占测量规格(MeasureSpec)的低30位。

MeasureSpec 类型 

MeasureSpec.AT_MOST

  • 表示 View 的大小可以最大到父 View 允许的尺寸,但不能超过这个尺寸。通常情况下,在使用 View.WRAP_CONTENT 且父 View 设置了最大尺寸时,会使用这种模式

MeasureSpec.EXACTLY

  • View 的大小已经被精确地确定了,通常是父 View 已经为它指定了确切的尺寸。例如,在使用 View.MATCH_PARENT 、设置了精确的尺寸、fill.parent时,会使用这种模式

MeasureSpec.UNSPECIFIED

  • 表示 View 的大小可以任意扩展,不受限制。父视图不约束子视图view
  • 通常情况下,在使用 View.WRAP_CONTENT 时,会使用这种模式,如:ListView、ScrollView

MeasureSpec.getMode(xxx) 获取宽高的模式

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);

计算逻辑

View的 MeasureSpec 值计算取决于两个因素:

  • View自身的布局参数(LayoutParams)
  • 父容器的测量规格(MeasureSpec)

即View的大小是由 自身布局参数(LayoutParams) 和 父容器的测量规格(MeasureSpec) 共同决定的。

MeasureSpec值的具体计算逻辑封装在getChildMeasureSpec()里,具体计算逻辑如下源码所示。

/**
  * 源码分析:getChildMeasureSpec()
  * 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
  * 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
  **/
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
    // 参数说明
    // * @param spec 父view的详细测量值(MeasureSpec) 
    // * @param padding view当前尺寸的的内边距和外边距(padding,margin) 
    // * @param childDimension 子视图的布局参数(宽/高)

    //父view的测量模式
    int specMode = MeasureSpec.getMode(spec);     

    //父view的大小
    int specSize = MeasureSpec.getSize(spec);     
  
    //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
    int size = Math.max(0, specSize - padding);  
  
    //子view想要的实际大小和模式(需要计算)  
    int resultSize = 0;  
    int resultMode = 0;  
  
    //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小  

    // 当父view的模式为EXACITY时,父view强加给子view确切的值
    //一般是父view设置为match_parent或者固定值的ViewGroup 
    switch (specMode) {  
    case MeasureSpec.EXACTLY:  
        // 当子view的LayoutParams>0,即有确切的值  
        if (childDimension >= 0) {  
            //子view大小为子自身所赋的值,模式大小为EXACTLY  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  

        // 当子view的LayoutParams为MATCH_PARENT时(-1)  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            //子view大小为父view大小,模式为EXACTLY  
            resultSize = size;  
            resultMode = MeasureSpec.EXACTLY;  

        // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  
  
    // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)  
    case MeasureSpec.AT_MOST:  
        // 道理同上  
        if (childDimension >= 0) {  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  
  
    // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
    // 多见于ListView、GridView  
    case MeasureSpec.UNSPECIFIED:  
        if (childDimension >= 0) {  
            // 子view大小为子自身所赋的值  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        }  
        break;  
    }  
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
}  

2000fb88c60c473f94ba1588826d7fab.png

注意:

区别于顶级View(即DecorView)的测量规格MeasureSpec,计算逻辑:取决于 自身布局参数 & 窗口尺寸

format,png

掌握 Rect 类

Rect 是 Android 中表示矩形的一个类,它有四个主要的属性:lefttopright 和 bottom。使用 Rect 可以方便地进行碰撞检测和区域计算。

  • Rect 主要用于需要整数值的场合,比如简单矩形区域的定义。
Rect(
    left: Int, 
    top: Int, 
    right: Int, 
    bottom: Int)

使用指定的坐标创建一个新矩形。注意:不执行范围检查,因此调用者必须确保 left <= right 且 top <= Bottom。 

Rect(r: Rect?)

创建一个新矩形,使用指定矩形中的值进行初始化(保持不变)。 

掌握 RectF 类

RectF 保存矩形的四个浮点坐标,这使得 RectF 可以更精确地表示矩形,尤其是在涉及到小数点运算时。

  • 在图形绘制中,尤其是涉及到曲线(如圆弧)时,使用 RectF 可以提供更平滑的效果,因为浮点数能够更好地捕捉精细的变化。

矩形由其 4 个边(左、上、右、下)的坐标表示。这些字段可以直接访问。使用 width() 和 height() 检索矩形的宽度和高度。注意:大多数方法不会检查坐标是否正确排序(即左 <= 右且顶部 <= 底部)。

  • RectF()

创建一个新的空 RectF。

  • RectF(float left, float top, float right, float bottom)

使用指定的坐标创建一个新矩形。

  • RectF(RectF r)

创建一个新矩形,使用指定矩形中的值进行初始化(保持不变)。

measure过程

measure过程 根据View的类型分为2种情况:

 

format,png

单一View

5e0aa55317014d9c85e35ee00036b9c9.png

/**
  * 源码分析:measure()
  * 定义:Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法
  * 作用:基本测量逻辑的判断
  */ 
  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

      // 参数说明:View的宽 / 高测量规格
      ...

      int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
              mMeasureCache.indexOfKey(key);

      if (cacheIndex < 0 || sIgnoreMeasureCache) {
          
          onMeasure(widthMeasureSpec, heightMeasureSpec);
          // 计算视图大小 ->>分析1
      } else {
          ...
  }
/**
  * 分析1:onMeasure()
  * 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
  *      b. 存储测量后的View宽 / 高:setMeasuredDimension()
  */ 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    // 参数说明:View的宽 / 高测量规格
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
    // setMeasuredDimension() :获得View宽/高的测量值 ->>分析2
    // 传入的参数通过getDefaultSize()获得 ->>分析3
}
/**
  * 分析2:setMeasuredDimension()
  * 作用:存储测量后的View宽 / 高
  * 注:该方法即为我们重写onMeasure()所要实现的最终目的
  */
  protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  

    //参数说明:测量后子View的宽 / 高值

    // 将测量后子View的宽 / 高值进行传递
        mMeasuredWidth = measuredWidth;  
        mMeasuredHeight = measuredHeight;  
      
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
    } 
  // 由于setMeasuredDimension()的参数是从getDefaultSize()获得的
  // 下面继续看getDefaultSize()的介绍
/**
  * 分析3:getDefaultSize()
  * 作用:根据View宽/高的测量规格计算View的宽/高值
  */
  public static int getDefaultSize(int size, int measureSpec) {  

    // 参数说明:
    // size:提供的默认大小
    // measureSpec:宽/高的测量规格(含模式 & 测量大小)

    // 设置默认大小
    int result = size; 
        
    // 获取宽/高测量规格的模式 & 测量大小
    int specMode = MeasureSpec.getMode(measureSpec);  
    int specSize = MeasureSpec.getSize(measureSpec);  
  
    switch (specMode) {  
        // 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
        case MeasureSpec.UNSPECIFIED:  
            result = size;  
            break;  

        // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
        case MeasureSpec.AT_MOST:  
        case MeasureSpec.EXACTLY:  
            result = specSize;  
            break;  
    }  

    // 返回View的宽/高值
    return result;  
  }    

ViewGroup 

测量原理

从ViewGroup至子View、自上而下遍历进行(即树形递归),通过计算整个ViewGroup中各个View的属性,从而最终确定整个ViewGroup的属性。即:

  1. 遍历测量所有子View的尺寸(宽/高);
  2. 合并所有子View的尺寸(宽/高),最终得到ViewGroup父视图的测量值。

layout

用于计算视图(View)的位置,即计算View的四个顶点位置:LeftTopRight 和 Bottom。

单一view

/**
  * 源码分析起始点:layout()
  * 作用:确定View本身的位置,即设置View本身的四个顶点位置
  */ 
  public void layout(int l, int t, int r, int b) {  

    // 当前视图的四个顶点
    int oldL = mLeft;  
    int oldT = mTop;  
    int oldB = mBottom;  
    int oldR = mRight;  
      
    // 1. 确定View的位置:setFrame() / setOpticalFrame()
    // 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回 
    // setFrame() ->分析1
    // setOpticalFrame() ->分析2
    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    // 2. 若视图的大小 & 位置发生变化
    // 会重新确定该View所有的子View在父容器的位置:onLayout()
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  

      onLayout(changed, l, t, r, b);  
      // 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现 ->分析3
      // 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需自定义重写实现(下面的章节会详细说明)
}  

/**
  * 分析1:setFrame()
  * 作用:根据传入的4个位置值,设置View本身的四个顶点位置
  * 即:最终确定View本身的位置
  */ 
  protected boolean setFrame(int left, int top, int right, int bottom) {

    // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
    // 从而确定了视图的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;

    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

   }

/**
  * 分析2:setOpticalFrame()
  * 作用:根据传入的4个位置值,设置View本身的四个顶点位置
  * 即:最终确定View本身的位置
  */ 
  private boolean setOpticalFrame(int left, int top, int right, int bottom) {

        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;

        Insets childInsets = getOpticalInsets();

        // 内部实际上是调用setFrame()
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
    // 回到调用原处

/**
  * 分析3:onLayout()
  * 注:对于单一View的laytou过程
  *    1. 由于单一View是没有子View的,故onLayout()是一个空实现
  *    2. 由于在layout()中已经对自身View进行了位置计算:setFrame() / setOpticalFrame()
  *    3. 所以单一View的layout过程在layout()后就已完成了
  */ 
 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

   // 参数说明
   // changed 当前View的大小和位置改变了 
   // left 左部位置
   // top 顶部位置
   // right 右部位置
   // bottom 底部位置
}  

viewGroup

/**
  * 源码分析:layout()
  * 作用:确定View本身的位置,即设置View本身的四个顶点位置
  * 注:与单一View的layout()源码一致
  */ 
  public void layout(int l, int t, int r, int b) {  

    // 当前视图的四个顶点
    int oldL = mLeft;  
    int oldT = mTop;  
    int oldB = mBottom;  
    int oldR = mRight;  
      
    // 1. 确定View的位置:setFrame() / setOpticalFrame()
    // 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回 
    // setFrame() ->分析1
    // setOpticalFrame() ->分析2
    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    // 2. 若视图的大小 & 位置发生变化
    // 会重新确定该View所有的子View在父容器的位置:onLayout()
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  

        onLayout(changed, l, t, r, b);  
        // 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现(上面已分析完毕)
        // 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现 ->分析3
  ...

}  

/**
  * 分析1:setFrame()
  * 作用:确定View本身的位置,即设置View本身的四个顶点位置
  */ 
  protected boolean setFrame(int left, int top, int right, int bottom) {
        ...
    // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
    // 从而确定了视图的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;

    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

    }

/**
  * 分析2:setOpticalFrame()
  * 作用:确定View本身的位置,即设置View本身的四个顶点位置
  */ 
  private boolean setOpticalFrame(int left, int top, int right, int bottom) {

        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;

        Insets childInsets = getOpticalInsets();

        // 内部实际上是调用setFrame()
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
    // 回到调用原处


} 
/**
  * 分析3:onLayout()
  * 作用:计算该ViewGroup包含所有的子View在父容器的位置()
  * 注: 
  *    a. 定义为抽象方法,需重写,因:子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现
  *    b. 在自定义ViewGroup时必须复写onLayout()!!!!!
  *    c. 复写原理:遍历子View 、计算当前子View的四个位置值 & 确定自身子View的位置(调用子View layout())
  */ 
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

     // 参数说明
     // changed 当前View的大小和位置改变了 
     // left 左部位置
     // top 顶部位置
     // right 右部位置
     // bottom 底部位置

     // 1. 遍历子View:循环所有子View
      for (int i=0; i<getChildCount(); i++) {
          View child = getChildAt(i);   

          // 2. 计算当前子View的四个位置值
            // 2.1 位置的计算逻辑
            ...// 需自己实现,也是自定义View的关键

            // 2.2 对计算后的位置值进行赋值
            int mLeft  = Left
            int mTop  = Top
            int mRight = Right
            int mBottom = Bottom

          // 3. 根据上述4个位置的计算值,设置子View的4个顶点:调用子view的layout() & 传递计算过的参数
          // 即确定了子View在父容器的位置
          child.layout(mLeft, mTop, mRight, mBottom);
          // 该过程类似于单一View的layout过程中的layout()和onLayout(),此处不作过多描述
      }
    }

draw

paint 类

Paint 类包含有关如何绘制几何图形、文本和位图的样式和颜色信息。

构造函数

Paint() 使用默认设置创建新油漆。

Paint(int flags) 使用指定的标志创建一个新的油漆。

Paint(Paint paint)

常用方法

setColor(int),设置画笔的颜色

setAlpha(int),设置画笔的透明度

setARGB(int a, int r, int g, int b),设置画笔的颜色,a代表透明度,r,g,b代表颜色值

setAntiAlias(boolean),设置是否使用抗锯齿功能,设置后会平滑一些

setDither(boolean),设定是否使用图像抖动处理,设置后图像更加清晰

setStyle(Style),设置画笔的风格

  • Style.FILL,实心
  • Style.FILL_AND_STROKE,同时显示实心和空心
  • Style.STROKE,空心

setStrokeJoin(Join),设置连接 

getFontMetricsInt 返回给定文本的字体规格值

Paint.FontMetricsInt fontMetricsInt = myPaint.getFontMetricsInt();

Canvas类

Canvas主要用于2D绘图,它提供了很多相应的drawXxx()方法,Canvas的获取方式有三种

  • 重写View的onDraw(Canvas)方法
@Override
protected void onDraw(Canvas canvas) {

}
  • SurfaceView通过lockCanvas()方法获取Canvas
@Override
public void surfaceCreated(SurfaceHolder holder) {
    Canvas canvas = holder.lockCanvas();
      
    holder.unlockCanvasAndPost(canvas);
}

 

  • 通过Canvas(Bitmap)或setBitmap(Bitmap)自定义Canvas
Bitmap bitmap = Bitmap.createBitmap(600, 800, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);

常用方法

  • 绘制背景

// a为透明度,`r`,`g`,`b`代表颜色值
drawARGB(int a, int r, int g, int b)
drawColor(int color)
drawRGB(int r, int g, int b)
  • 绘制点

// 根据给定的坐标,绘制点
drawPoint(float x, float y, Paint)

// 两个一组组成坐标,pts必须是二的倍数
drawPoints(float[] pts, Paint)

// offset是起始,count是数量,必须是二的倍数
drawPoints(float[] pts, int offset, int count, Paint)
 
  • 绘制线

线由两个点连接而成

// 根据给定的坐标,绘制线
drawLine(float, float, float, float, Paint)

// 四个一组组成连线,pts必须是四的倍数
drawLines(float[] pts, Paint)

// offset是起始,count是数量,必须是四的倍数
drawLines(float[] pts, int offset, int count, Paint)
  • 绘制矩形

// 绘制矩形,rect指定左右上下
drawRect(Rect rect, Paint)
drawRect(RectF, Paint)
drawRect(float, float, float, float, Paint)
  • 圆角矩阵

// 绘制圆角矩形,rx是横向,ry是纵向
drawRoundRect(RectF, float rx, float ry, Paint)
drawRoundRect(float, float, float, float, float, float, Paint)
  • 绘制椭圆

// 绘制椭圆,rect指定左右上下
drawOval(RectF rect, Paint)
drawOval(float, float, float, float, Paint)
  • 绘制圆

// cx和cy是圆的中心,radius是圆的半径
drawCircle(float cx, float cy, float radius, Paint)
  • 绘制弧形

// startAngle是圆弧开始角度,sweepAngle是圆弧经过的角度,useCenter设置圆弧是否经过中心
drawArc(RectF, float startAngle, float sweepAngle, boolean useCenter, Paint)
drawArc(float, float, float, float, float, float, boolean, Paint)
  • 绘制文字 

 

baseLine基线计算

单一view

/**
  * 源码分析:draw()
  * 作用:根据给定的 Canvas 自动渲染View包括其所有子 View)。
  * 绘制过程:
  *   1. 绘制view背景
  *   2. 绘制view内容
  *   3. 绘制子View
  *   4. 绘制装饰(渐变框,滑动条等等)
  * 注:
  *    a. 在调用该方法之前必须要完成 layout 过程
  *    b. 所有的视图最终都是调用 View 的 draw()绘制视图( ViewGroup 没有复写此方法)
  *    c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制
  *    d. 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制
  */ 
  public void draw(Canvas canvas) {

    ...// 仅贴出关键代码
  
    int saveCount;

    // 步骤1: 绘制本身View背景
      if (!dirtyOpaque) {
          drawBackground(canvas); 
          // ->分析1
      }

      // 若有必要,则保存图层(还有一个复原图层)
      // 优化技巧:当不需绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
      // 因此在绘制时,节省 layer 可以提高绘制效率
      final int viewFlags = mViewFlags;
      if (!verticalEdges && !horizontalEdges) {

    // 步骤2:绘制本身View内容
      if (!dirtyOpaque) 
          onDraw(canvas);
      // 单一View中:默认为空实现,需复写
      // ViewGroup中:需复写
      // ->分析2

    // 步骤3:绘制子View
      // 由于单一View无子View,故View中:默认为空实现
      // ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写
          dispatchDraw(canvas);
      // ->分析3

    // 步骤4:绘制装饰,如滑动条、前景色等等
      onDrawScrollBars(canvas);
      // ->分析4

    return;
  }
    ...    
}


/**
  * 分析1:drawBackground(canvas)
  * 作用:绘制View本身的背景
  */ 
  private void drawBackground(Canvas canvas) {
        // 获取背景 drawable
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        // 根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界
        setBackgroundBounds();

        ...

        // 获取 mScrollX 和 mScrollY值 
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            // 若 mScrollX 和 mScrollY 有值,则对 canvas 的坐标进行偏移
            canvas.translate(scrollX, scrollY);


            // 调用 Drawable 的 draw 方法绘制背景
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
   } 

/**
  * 分析2:onDraw(canvas)
  * 作用:绘制View本身的内容
  * 注:
  *   a. 由于 View 的内容各不相同,所以该方法是一个空实现
  *   b. 在自定义绘制过程中,需由子类去实现复写该方法,从而绘制自身的内容
  *   c. 谨记:自定义View中 必须且只需复写onDraw()
  */
  protected void onDraw(Canvas canvas) {
      
        ... // 复写从而实现绘制逻辑

  }

/**
  * 分析3: dispatchDraw(canvas)
  * 作用:绘制子View
  * 注:由于单一View中无子View,故为空实现
  */
  protected void dispatchDraw(Canvas canvas) {

        ... // 空实现

  }

/**
  * 分析4: onDrawScrollBars(canvas)
  * 作用:绘制装饰,如滚动指示器、滚动条、和前景等
  */
  public void onDrawForeground(Canvas canvas) {
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);

        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        if (foreground != null) {
            if (mForegroundInfo.mBoundsChanged) {
                mForegroundInfo.mBoundsChanged = false;
                final Rect selfBounds = mForegroundInfo.mSelfBounds;
                final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

                if (mForegroundInfo.mInsidePadding) {
                    selfBounds.set(0, 0, getWidth(), getHeight());
                } else {
                    selfBounds.set(getPaddingLeft(), getPaddingTop(),
                            getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
                }

                final int ld = getLayoutDirection();
                Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                        foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
                foreground.setBounds(overlayBounds);
            }

            foreground.draw(canvas);
        }
    }

viewGroup

/**
  * 源码分析:draw()
  * 与单一View的draw()流程类似
  * 作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)
  * 绘制过程:
  *   1. 绘制view背景
  *   2. 绘制view内容
  *   3. 绘制子View
  *   4. 绘制装饰(渐变框,滑动条等等)
  * 注:
  *    a. 在调用该方法之前必须要完成 layout 过程
  *    b. 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
  *    c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制
  *    d. 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制
  */ 
  public void draw(Canvas canvas) {

    ...// 仅贴出关键代码
  
    int saveCount;

    // 步骤1: 绘制本身View背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

    // 若有必要,则保存图层(还有一个复原图层)
    // 优化技巧:当不需绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
    // 因此在绘制时,节省 layer 可以提高绘制效率
    final int viewFlags = mViewFlags;
    if (!verticalEdges && !horizontalEdges) {

    // 步骤2:绘制本身View内容
        if (!dirtyOpaque) 
            onDraw(canvas);
        // View 中:默认为空实现,需复写
        // ViewGroup中:需复写

    // 步骤3:绘制子View
    // ViewGroup中:系统已复写好对其子视图进行绘制,不需复写
        dispatchDraw(canvas);
        
    // 步骤4:绘制装饰,如滑动条、前景色等等
        onDrawScrollBars(canvas);

        return;
    }
    ...    
}
/**
  * 源码分析:dispatchDraw()
  * 作用:遍历子View & 绘制子View
  * 注:
  *   a. ViewGroup中:由于系统为我们实现了该方法,故不需重写该方法
  *   b. View中默认为空实现(因为没有子View可以去绘制)
  */ 
    protected void dispatchDraw(Canvas canvas) {
        ......

         // 1. 遍历子View
        final int childrenCount = mChildrenCount;
        ......

        for (int i = 0; i < childrenCount; i++) {
                ......
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                  // 2. 绘制子View视图 ->>分析1
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                ....
        }
    }

/**
  * 分析1:drawChild()
  * 作用:绘制子View
  */
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        // 最终还是调用了子 View 的 draw ()进行子View的绘制
        return child.draw(canvas, this, drawingTime);
    }

View的常见回调方法

构造函数

构造函数1:会在代码中创建对象 new 时使用

public Custom_Textview(Context context)

构造函数2:会在layout布局中使用

public Custom_Textview(Context context, @Nullable AttributeSet attrs)

构造函数3:会在layout布局中使用,但是有style

public Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr)

构造函数4:Android 5.0 (API 21) 之后新增的,用于支持更复杂的样式定制。

public Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)

View的弹性滑动

 

View的滑动冲突

MotionEvent

实例

自定义一个view 继承自viewGroup能不能出效果?

不能。默认的viewGroup不会调用onDraw方法。因为它是用了责任链模式。

主要实现绘制功能的是:

自定义TextView

1.创建一个自定义TextView 类继承自 View

package com.example.androidstudiostudy.customview;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;

public class Custom_Textview extends View {

    // 构造函数1:会在代码中创建对象 new 时使用
    public Custom_Textview(Context context) {
        super(context);
        Log.d("自定义textView","new 对象");
    }

    // 构造函数2:会在layout布局中使用
    public Custom_Textview(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        Log.d("自定义textView","在layout布局中使用");
    }

    // 构造函数3:会在layout布局中使用,但是有style
    public Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        Log.d("自定义textView","在layout布局中使用,但是有style");
    }

    // 构造函数4:Android 5.0 (API 21) 之后新增的,用于支持更复杂的样式定制。
    public Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    // 自定义view的测量方法,用于测量 View 大小的重要方法。在这个方法中,你需要根据传入的宽度和高度测量规格,计算并设置 View 的宽度和高度。
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // MeasureSpec.getMode(xxx) 获取宽高的模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        // MeasureSpec.AT_MOST 表示 View 的大小可以最大到父 View 允许的尺寸,但不能超过这个尺寸。通常情况下,在使用 View.WRAP_CONTENT 且父 View 设置了最大尺寸时,会使用这种模式
        // MeasureSpec.EXACTLY  View 的大小已经被精确地确定了,通常是父 View 已经为它指定了确切的尺寸。例如,在使用 View.MATCH_PARENT 或者设置了精确的尺寸时,会使用这种模式
        // MeasureSpec.UNSPECIFIED 表示 View 的大小可以任意扩展,不受限制。通常情况下,在使用 View.WRAP_CONTENT 时,会使用这种模式
        if(widthMode == MeasureSpec.UNSPECIFIED){

        }
    }
}

2.创建自定义属性

在res/values下创建一个名为arrs.xml的资源文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- name:自定义view的名字-->
    <declare-styleable name="CustomTextview">
        <!-- name:属性名称 format:格式-->
        <attr name="custom_text" format="string"/>
        <attr name="custom_textColor" format="color"/>
        <!-- 字体大小格式:dimension-->
        <attr name="custom_textSize" format="dimension"/>
        <!-- background:自定义view都是继承自view 所以不用自定义管理-->
        <attr name="custom_background" format="reference|color"/>
        <attr name="custom_inputType">
            <enum name="number" value="1"/>
            <enum name="string" value="2"/>、
        </attr>
    </declare-styleable>
</resources>

3.获取自定义属性

public class Custom_Textview extends View {

    private String text;
    private int textColor = Color.BLACK;
    private int fontSize;
    private Paint myPaint;

    // 构造函数1:会在代码中创建对象 new 时使用
    public Custom_Textview(Context context) {
        // super(context);
        // 修改构造函数,无论使用哪个构造函授都调用第三个
        this(context, null);
        Log.d("自定义textView", "new 对象");
    }

    // 构造函数2:会在layout布局中使用
    public Custom_Textview(Context context, @Nullable AttributeSet attrs) {
        // super(context, attrs);
        this(context, attrs, 0);
        // h
        Log.d("自定义textView", "在layout布局中使用");
    }

    // 构造函数3:会在layout布局中使用,但是有style
    public Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        Log.d("自定义textView", "在layout布局中使用,但是有style");
        // 获取自定义属性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomTextview);

        text = array.getString(R.styleable.CustomTextview_custom_text);
        textColor = array.getColor(R.styleable.CustomTextview_custom_textColor, textColor);
        fontSize = array.getDimensionPixelSize(R.styleable.CustomTextview_custom_textSize, fontSize);

        // 回收自定义属性
        array.recycle();
    }

    // 构造函数4:Android 5.0 (API 21) 之后新增的,用于支持更复杂的样式定制。
    public Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    // 自定义view的测量方法,用于测量 View 大小的重要方法。在这个方法中,你需要根据传入的宽度和高度测量规格,计算并设置 View 的宽度和高度。
    // int widthMeasureSpec, int heightMeasureSpec 是父类传过来的
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }


}

使用 

<com.example.androidstudiostudy.customview.Custom_Textview
    app:custom_text = "自定义文本"
    app:custom_textColor = "@color/p6"
    app:custom_textSize = "14sp"
    android:background="@color/black"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

3.测量view的宽高

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width,height;

        // MeasureSpec.getMode(xxx) 获取宽高的模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // MeasureSpec.AT_MOST 表示 View 的大小可以最大到父 View 允许的尺寸,但不能超过这个尺寸。通常情况下,在使用 View.WRAP_CONTENT 且父 View 设置了最大尺寸时,会使用这种模式
        // MeasureSpec.EXACTLY  View 的大小已经被精确地确定了,通常是父 View 已经为它指定了确切的尺寸。例如,在使用 View.MATCH_PARENT 或者设置了精确的尺寸时,会使用这种模式
        // MeasureSpec.UNSPECIFIED 表示 View 的大小可以任意扩展,不受限制。通常情况下,在使用 View.WRAP_CONTENT 时,会使用这种模式

        // 计算宽度
        // 1. 确定的值,这时候不需要计算,给多少是多少
        width = MeasureSpec.getSize(widthMeasureSpec);

        // 2.给的是wrap_content 需要计算
        if (widthMode == MeasureSpec.AT_MOST) {
            Log.e("测量模式", "AT_MOST");

            // 计算的宽度与字体长度、字体大小有关 ----- 需要使用画笔来测量
            Rect bounds = new Rect();
            myPaint.getTextBounds(text, 0, text.length(), bounds); // 获取文本的rect
            width = bounds.width();
        } else if (widthMode == MeasureSpec.EXACTLY) {
            Log.e("测量模式", "EXACTLY");
        } else if (widthMode == MeasureSpec.UNSPECIFIED) {
            Log.e("测量模式", "UNSPECIFIED");
        }

        // 计算高度
        // 1. 确定的值,这时候不需要计算,给多少是多少
        height = MeasureSpec.getSize(heightMeasureSpec);

        // 2.给的是wrap_content 需要计算
        if(heightMode == MeasureSpec.AT_MOST){
            Rect bounds = new Rect();
            myPaint.getTextBounds(text, 0, text.length(), bounds); // 获取文本的rect
            height = bounds.height();
        }

        // 设置控件的宽高
        setMeasuredDimension(width,height);

    }

4.绘制view

Carson带你学Android:自定义View 绘制过程(Draw) - 简书 (jianshu.com)

Carson带你学Android:自定义View 布局过程(Layout) - 简书

Carson带你学Android:一文梳理自定义View工作流程 - 简书 (jianshu.com)

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2074615.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图数据库查询语言 cypher 与 memgraph

Cyper 作为声明式查询语言, SQL 在计算机行业无人不晓, 无人不知. 而 Cypher 就是 Graph Database 图数据库的 SQL. Cypher 用"圆括号"来表示节点, 用"方括号,连接线及箭头"表示关系 这样一句话 - "Sally likes Graphs. Sally is friends with John. …

完成控制器方法获取参数-@RequestParam

文章目录 1.将方法的request和response参数封装到参数数组1.SunDispatcherServlet.java1.根据方法信息&#xff0c;返回实参列表2.具体调用 2.测试 2.封装Http请求参数到参数数组1.自定义RequestParam注解2.MonsterController.java 增加参数3.SunDispatcherServlet.java1.resol…

软件架构的发展经历了从单体结构、垂直架构、SOA架构到微服务架构的过程剖析

1.单体架构 特点: 1、所有的功能集成在一个项目工程中。 2、所有的功能打一个war包部署到服务器。 3、应用与数据库分开部署。 4、通过部署应用集群和数据库集群来提高系统的性能。 优点: 1、项目架构简单,前期开发成本低,周期短,小型项目的首选。 缺点: 1、全部…

c++实现mysql关系型数据库连接与增删改查操作

最近老师让我实现这个功能&#xff0c;顺便发个东西&#xff0c;我感觉mysql从入门到精通这本书写的蛮好的&#xff0c;其实连接数据库就是调用mysql-c-api库里面的函数mysql_real_connect,下来的增删改查&#xff0c;也无非就是cmd命令台里面的语句&#xff0c;插入&#xff1…

Javaweb学习之Vue实践小界面(四)

目录 前情回顾 本期介绍 效果图 第一步&#xff1a;前期工作 第二步&#xff1a;建立页眉 效果图 第三步&#xff1a;建立导航栏 效果图 第四步&#xff1a;主要内容放置 效果图 第五步&#xff1a;建立页脚 效果图 综合&#xff1a;将文字和背景更改成自己喜欢的…

PEM燃料电池启停控制策略优化的simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 5.完整工程文件 1.课题概述 PEM燃料电池启停控制策略优化的simulink建模与仿真。 1.燃料电池提供是燃料转换为电能和热能的装置。 2.功率的输出的改变通过很多因素&#xff0c;如温度&#xff0c;压力…

谷歌、火狐及Edge等浏览器如何使用allWebPlugin中间件响应ActiveX插件事件

allWebPlugin简介 allWebPlugin中间件是一款为用户提供安全、可靠、便捷的浏览器插件服务的中间件产品&#xff0c;致力于将浏览器插件重新应用到所有浏览器。它将现有ActiveX控件直接嵌入浏览器&#xff0c;实现插件加载、界面显示、接口调用、事件回调等。支持Chrome、Firefo…

模型 OGSM(战略规划)

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。目标引领&#xff0c;策略驱动&#xff0c;量化衡量。 1 OGSM模型的应用 1.1 电商企业年度增长战略 某电商企业面临激烈的市场竞争&#xff0c;决定运用OGSM模型来规划其年度战略&#xff0c;以实现…

代码随想录Day 25|回溯篇完结,题目:491.递增子序列、46、全排列、47.全排列Ⅱ

提示&#xff1a;DDU&#xff0c;供自己复习使用。欢迎大家前来讨论~ 文章目录 第七章 回溯算法part05一、题目题目一&#xff1a;491.递增子序列解题思路&#xff1a;回溯三部曲优化 题目二&#xff1a;46.全排列[46. 全排列](https://leetcode.cn/problems/permutations/)解…

日撸Java三百行(day34:图的深度优先遍历)

目录 一、深度优先搜索 二、图的深度优先遍历 三、代码实现 总结 一、深度优先搜索 深度优先搜索&#xff08;Depth First Search&#xff1a;DFS&#xff09;是一种用于遍历树或图的算法&#xff0c;具体来说就是从起始节点开始&#xff0c;沿某一分支路径不断深入&#…

Linux内核定时器、阻塞_非阻塞IO

一.内核时间管理 Linux 内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序、对于我们驱动编写者来说最常用的定时器。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率…

吴恩达谈AI未来:Agentic Workflow、推理成本下降与开源的优势

近年来&#xff0c;人工智能&#xff08;AI&#xff09;领域的发展势如破竹&#xff0c;然而随着技术的普及&#xff0c;市场也开始出现对AI泡沫的质疑声。2024年8月&#xff0c;AI领域的权威专家吴恩达&#xff08;Andrew Ng&#xff09;在与ARK Invest的对谈中&#xff0c;分…

利用Matlab求解高阶微分方程(ode45)

1、高阶微分方程的基本概念 二阶以及二阶以上的微分方程称之为高阶微分方程&#xff0c;一般来说&#xff0c;微分方程的阶数越高&#xff0c;求解的难度也就越大。求高阶方程的一个常用方法就是降低阶数。对二阶方程 &#xff0c;如果能用变量代换把它化成一阶方程&#xff0c…

【Tesla FSD V12的前世今生】从模块化设计到端到端自动驾驶技术的跃迁

自动驾驶技术的发展一直是全球汽车行业的焦点&#xff0c;Tesla的Full-Self Driving&#xff08;FSD&#xff09;系统凭借其持续的技术革新和强大的数据支持&#xff0c;在这个领域独占鳌头。本文将深入介绍Tesla FSD V12的演进历史&#xff0c;从自动驾驶的基础概念入手&#…

【XML详解】

XML基本概念 XML&#xff08;全称EXtensible Markup Language&#xff0c;可扩展标记语言&#xff09;&#xff1a;是一种用于存储和传输数据的标记语言&#xff0c;通过标签&#xff08;tags&#xff09;来定义数据的结构和含义。数据格式&#xff1a;XML本质上是一种数据的格…

【异常错误】pycharm可以在terminal中运行,但是无法在run中运行(没有输出错误就停止了)

问题&#xff1a; pycharm的命令可以在terminal中运行&#xff0c;但是复制到无法在run中运行&#xff08;没有输出错误就停止了&#xff09; run中运行后什么错误提示都没有 搞不懂为什么 解决&#xff1a; 降低run中batch-size的大小&#xff0c;即可以运行 我并没有观察到…

视频在线去水印解析相册怎么弄,轻松掌握五大技巧

在当前短视频流行的时代&#xff0c;我们常常需要下载一些短视频来进行剪辑或分享&#xff0c;但视频中的水印却成了一个不小的烦恼。为了帮助大家解决这个问题&#xff0c;本文将介绍五款高效的短视频去水印免费软件&#xff0c;让你轻松告别水印烦恼。 工具一&#xff1a;奈…

在VB.net中,LINQ有什么方法与属性

标题 在VB.net中&#xff0c;LINQ有什么方法与属性 正文 在VB.NET中使用LINQ&#xff08;Language Integrated Query&#xff09;&#xff0c;你可以利用一系列的方法和属性来查询和操作内存中的集合&#xff08;如数组、列表等&#xff09;以及数据库等数据源。LINQ提供了丰富…

Python相关系数导图

&#x1f3af;要点 量化变量和特征关联绘图对比皮尔逊相关系数、斯皮尔曼氏秩和肯德尔秩汽车性价比相关性矩阵热图大流行病与资产波动城镇化模型预测交通量宝可梦类别特征非线性依赖性捕捉向量加权皮尔逊相关系数量化图像相似性 Python皮尔逊-斯皮尔曼-肯德尔 皮尔逊相关系…

QcomboBox 组件

在记事本项目中&#xff0c;有一个问题&#xff0c;字体的编码格式是写死的。我们要建一个 组件提供这样的功能&#xff1a;通过点击&#xff0c;获得 不同的格式编码 定义它的槽 首先 把ui里面的comboBox组件与槽关联起来 connect(ui->comboBox,SIGNAL(currentIndexChange…