【Android】自定义View组件,并实现在 Compose、Kotlin、Xml 中调用

从事 Android 开发以来,很少有过自定义 View 的相关开发需求,大部分 UI 都是可以集成某些官方组件,在组件的基础上完成能够大大缩短开发时间。但今天我要讲的是:如何使用 Android 开发一个Compose、Xml都可以调用的组件?接下来请跟随我的脚步一起去学习 View 的自定义组件开发吧。

目录

  • Android 屏幕坐标
  • 自定义 View 的方式
  • 自定义 View
    • 初始化
      • 重写构造函数
      • 自定义 XML 属性
    • 测量大小 onMeasure
    • 确定大小 onSizeChanged
    • 确定子布局位置 onLayout
    • 绘制 onDraw
  • 在Xml中引用
  • 在Activity中引用
  • 在Compose中引用
  • 最终效果

Android 屏幕坐标

自定义 View 之前,需要先了解 View 的坐标,知道哪里是起点,哪里是终点,才能更好的展开工作。

经历过九年义务教育的朋友们,相信大家都见过下面的这幅图 👇

平面直角坐标系
很眼熟吧?这张图片的东西被称之为 👉 平面直角坐标系。

在 Android 系统上,也是用的 平面直角坐标系 来确定 View 的方向、大小,只不过它是“倒”过来的平面直角坐标系,如下图👇

在这里插入图片描述
不明白?我们再把这个图片代入到 Android 屏幕上来

在这里插入图片描述
看懂了吧?在 Android 系统上,直角坐标系的原点就是屏幕的左上角,往右是 x 轴,往下是 y 轴,整个 Android 的屏幕就是处于 平面直角坐标系 的第四象限上,其坐标的单位则使用的是像素(px) 来表示。如果你的手机是 1080*1920 像素,则意味着,以原点为起点,至屏幕的右侧,共有 1080 像素 (px) ,以原点为起点,至屏幕的底部,共有 1920 个像素(px)。

自定义 View 的方式

在 Android 中,自定义 View 一般可分为两种方式:继承 ViewGroup 或 View 实现自定义。

  • ViewGroup
    自定义 ViewGroup 一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自 ViewGroup 或各种 Layout ,包含子 View。
  • View
    在没有现成的View,需要自己实现的时候,就是用自定义 View,一般继承自 View、SurfaceView 或其它的 View。

本文只讲解通过继承 View 来实现自定义 View,通过继承 ViewGroup 实现自定义 View 的文章可参考往期文章👉【Android】实现自定义标题栏

自定义 View

View 完成自定义的过程需要经历:初始化 → onMeasure → onSizeChanged → onLayout → onDraw 五个阶段。

初始化

重写构造函数

View 的初始化方式可通过四种构造函数进行初始化,如下:

constructor(context: Context?) : this(context, null)constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : this(context, attrs, defStyleAttr, 0)constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {}
构造函数使用场景
public View(Context context)一般在 Activity、Fragment 中使用(本文使用该构造函数):
View view = new View(context);
public View(Context context, AttributeSet attrs)当从XML文件构造视图,提供XML文件中指定的属性时,会调用此函数(本文使用该构造函数):
< View android:layout_width=“wrap_content” android:layout_height=“wrap_content”/>
public View(Context context, AttributeSet attrs, int defStyleAttr)从XML执行膨胀,并从主题属性应用特定于类的基本样式。View的这个构造函数允许子类在膨胀时使用自己的基本样式。
public View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)从XML执行膨胀,并从主题属性或样式资源应用特定于类的基本样式。View的这个构造函数允许子类在膨胀时使用自己的基本样式。(本文使用该构造函数)

注意:重写构造方法时,拥有四个参数的那个构造函数必须使用super用于访问父类的构造方法,另外三个构造方法,则需要使用this指引下一个构造方法。这样,当调用第一、二、三个构造方法时,就会执行第四个构造方法,使用该方式才能使UI渲染上,否则会出现实例化 View 无效的情况出现。具体参考上方的四个构造方法。

自定义 XML 属性

如果自定义的 View 在 XML 布局上用的到,自定义属性这一步则少不了,届时需要在 res/values/arrts.xml 文件夹添加 XML 的属性。若没有 arrts.xml 文件则需手动创建。创建完成后在其中编写的代码如下:

<resources><declare-styleable name="StepView"><attr name="type"><!--      添加枚举,在布局样式使用type属性时可直接使用以下的选项      --><enum name="start" value="0" /><enum name="middle" value="1" /><enum name="stop" value="2" /></attr><attr name="text" format="string" localization="suggested" /><attr name="textSize" format="dimension" /><attr name="style" format="integer" ><!--      添加枚举,在布局样式使用style属性时可直接使用以下的选项      --><enum name="selected" value="10"/><enum name="not_selected" value="11"/></attr></declare-styleable>
</resources>

编写了名为StepView的属性样式,需要在自定义 View 类里的构造函数中使用 Context.obtainStyledAttributes函数引用,通过循环遍历的方式找到属性赋值给对应的值,这时,就完成了 XML 布局属性的自定义。

constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int, ) : super(context, attrs, defStyleAttr, defStyleRes) {//获取定义的一些属性val styleAttrs = context!!.obtainStyledAttributes(attrs, R.styleable.StepView, defStyleAttr, 0)//数一数有多少个属性呢val indexCount = styleAttrs.indexCount// 循环遍历的方式,找到我们所定义的一些属性for (i in 0..indexCount) {//根据索引值给java代码中的成员变量赋值when (val index = styleAttrs.getIndex(i)) {R.styleable.StepView_text -> text = styleAttrs.getString(index).toString() R.styleable.StepView_textSize -> textSize = styleAttrs.getDimension(index, 2f)R.styleable.StepView_type -> type = styleAttrs.getInt(index, type)R.styleable.StepView_style -> style = styleAttrs.getInteger(index, STYLE_NOT_SELECTED)}}//资源文件中的属性回收styleAttrs.recycle()
}

测量大小 onMeasure

为什么要测量 View 大小?

View 的大小不仅由自身大小所决定,同时也受到父控件的影响,为了我们的控件能更好的适应各种情况,一般需要自己进行测量。

测量 View 大小使用的是 View 的 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法进行测量,为了更好的适配各种分辨率,自定义 View 的过程中,必须由重写该方法。

重写方法,还需要使用setMeasuredDimension(int measuredWidth, int measuredHeight)方法使测量好的宽高产生效果,如下:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)// 获取宽高的测量模式val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)// 获取宽高的测量大小val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)val heithtSpecSize = MeasureSpec.getSize(heightMeasureSpec)if (layoutParams.width == ViewGroup.LayoutParams.WRAP_CONTENT && layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {val fl = mWidthTotal.toFloat() / 1.5fsetMeasuredDimension(fl.toInt() + indentAndBulge.toInt(), mHeight.toInt())} else if (layoutParams.width == ViewGroup.LayoutParams.WRAP_CONTENT) {// 宽 / 高任意一个布局参数为= wrap_content时,都设置默认值val fl = mWidthTotal.toFloat() / 1.5fsetMeasuredDimension(fl.toInt() + indentAndBulge.toInt(), heithtSpecSize)} else if (layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {// 宽 / 高任意一个布局参数为= wrap_content时,都设置默认值setMeasuredDimension(widthSpecSize, mHeight.toInt())}
}

注意:若是没有重写onMeasure方法并完成测量,在给该组件定义宽高为wrap_content时,组件的宽、高度会默认跟随父类。具体原因请查看👉Android 自定义View:为什么你设置的wrap_content不起作用?

确定大小 onSizeChanged

在测量完 View 并使用 setMeasuredDimension(int measuredWidth, int measuredHeight) 函数之后,View 的大小基本上已经确定,那为什么还需要再次确定 View 的大小呢?

这是因为 View 的大小不仅由 View 本身控制,而且受父控件的影响,所以我们在确定 View 大小的时候最好使用系统提供的 onSizeChanged(int w, int h, int oldw, int oldh) 回调函数。

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)Log.e("onSizeChanged", "View的宽度:$w,高度$h")
}

onSizeChanged 方法的四个参数分别为:

  • w – Current width of this view.
  • h – Current height of this view.
  • oldw – Old width of this view.
  • oldh – Old height of this view.

此函数相对简单,我们只需要关注其宽度(w)、高度(h) 即可,这两个参数就是 View 的最终大小。该函数只会在 View 的大小发生改变时自动触发,例如:初始化View、界面横竖屏的切换等。

确定子布局位置 onLayout

确定子布局的函数是 onLayout(boolean changed, int left, int top, int right, int bottom) ,它用于确定子 View 在父 View 的位置。
当此视图应为其每个子级分配大小和位置时,从布局调用。具有子级的派生类应重写此方法,并在其每个子级上调用布局。

/*** 如果自定义的 View 有子组件时,必须重写该方法,用以确定子组件在 View 中的位置.* 如果有需要自定义有子 View 的组件时,应该继承 ViewGroup 而不是 View,具体看情况自己分析*/
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {super.onLayout(changed, left, top, right, bottom)Log.e("onLayout","$left,$top,$right,$bottom")
}

绘制 onDraw

扯了这么多,终于来到自定义 View 的最后一步了!

在上文我们说到 Android 的 平面直角坐标系 以及 Android 屏幕在 平面直角坐标系 中的位置是位于第四象限。那接下来我们如何在 Canvas 画布上绘制出自己想要的样式呢?

Android 中,提供了 Canvas 作为画画的载体,所绘制的东西最终呈现在 Canvas 上,因此也可以理解为 Canvas 是一张纸,Paint 则是五颜六色的笔。Canvas 作为画布(白纸),提供了多个在画布上画的函数给我们调用:

Canvas FunctionDescribe
drawARGB使用 srcover porterduff 模式,用指定的 ARGB 颜色填充整个画布的位图(仅限于当前剪辑)。
drawArc绘制指定的圆弧,该圆弧将缩放以适合指定的椭圆形。
drawBitmap使用指定的矩阵绘制位图。
drawBitmapMesh通过网格绘制位图,其中网格顶点均匀分布在位图上。
drawCircle使用指定的颜料绘制指定的圆。
drawColor使用指定的颜色和混合模式填充整个画布的位图(仅限于当前剪辑)。
drawDoubleRoundRect使用指定的 paint 绘制双圆角矩形。
drawGlyphs使用指定字体绘制字形数组。
drawLine使用指定的绘图绘制具有指定起点和终点 x,y 坐标的线段。
drawMesh将网格对象绘制到屏幕上。
drawOval使用指定的颜料绘制指定的椭圆形。椭圆形将根据绘画中的样式进行填充或加框。
drawPaint用指定的绘画填充整个画布的位图(仅限于当前剪辑)。
drawPatch将指定的位图绘制为 N 面片(最常见的是 9 面片)。
drawPath使用指定的油漆绘制指定的路径。路径将根据绘画中的样式进行填充或加框。
drawPicture绘制图片,拉伸以适合 dst 矩形。
drawPoint用于绘制单个点的 drawPoints() 的帮助程序。
drawRGB使用 srcover porterduff 模式,用指定的 RGB 颜色填充整个画布的位图(仅限于当前剪辑)。
drawRect使用指定的绘制绘制指定的矩形,矩形将根据绘画中的样式进行填充或加框。
drawRenderNode绘制给定的 RenderNode。
drawRoundRect使用指定的油漆绘制指定的圆角矩形,圆角矩形将根据绘画中的样式进行填充或加框。
drawText在指定的 Paint 中绘制指定范围的文本,由开始/结束指定,其原点位于 (x,y)。
drawTextOnPath使用指定的绘画,沿着指定的路径绘制文本,原点位于 (x,y)。
drawTextRun绘制一系列文本,全部在一个方向上,并带有用于复杂文本形状的可选上下文。
drawVertices绘制顶点数组,解释为三角形(基于模式)。

此处展示的函数并非全部,详情请参考👉Canvas

本文自定义 View 绘制的 UI 只需要使用到 drawTextdrawPath 两个 Canvas 函数进行绘制。drawPath 在这里用来绘制背景,drawText绘制文字。

接下来绘制的图形:

在这里插入图片描述
使用 drawPath 进行绘制前,需要先使用 Path 定义好 drawPathcanvas 经过的几个点,这里画个图,以便参考:

在这里插入图片描述
接着写代码实现:

override fun onDraw(canvas: Canvas) {super.onDraw(canvas)// 新建绘制的路径val path = Path()// 第一步,定原点path.moveTo(0f, 0f)// 第二步path.lineTo(totalWidth, 0f)// 第三步val rightBulge = totalWidth + indentAndBulgepath.lineTo(rightBulge, halfHeight)// 第四步path.lineTo(totalWidth, mHeight)// 第五步path.lineTo(0f, mHeight)// 回到原点,闭合图形path.lineTo(0f, 0f)path.close()// 将图形绘制出来canvas.drawPath(path, mPaint)
}

完成了背景的绘制,接下来绘制 Text

mPaint.color = if (style == STYLE_NOT_SELECTED) context.getColor(R.color.font_black) else context.getColor(R.color.white)
canvas.drawText(text, startPointText, mHeight / 1.5f, mPaint)

至此,完成了自定义的全部过程。

在Xml中引用

在 xml 中引用和官方的 View 组件引用没有太大的差别,如下:

<com.miyue.stepdemo.StepViewandroid:id="@+id/step1"android:layout_width="wrap_content"android:layout_height="wrap_content"/>

我们自定义开始时,在attrs.xml文件里自定义了一些组件的属性,添加后如下:

<com.miyue.stepdemo.StepViewandroid:id="@+id/step1"android:layout_width="wrap_content"android:layout_height="wrap_content"app:style="selected"app:text="第一步"app:type="start"/>

注意:在 xml 中初始化的组件,是无法使用Debug断点调试的,但可通过Logcat查看日志信息。

在Activity中引用

val step = StepView(this)
step.setStyle(StepView.STYLE_SELECTED)
step.setType(StepView.TYPE_START)
step.setText("第一步")
step.setTextSize(30f)
val linearLayout = findViewById<LinearLayout>(R.id.ll_step2)
linearLayout.addView(step)

在Compose中引用

如果你的 Android 项目是 Java 项目,建议你创建一个 Kotlin 项目或者 Compose 项目。如果你的项目是 Kotlin 项目,则可以通过以下 的方式创建一个 Compose 界面:鼠标右键包名 → New → Compose → Empty Activity。

在这里插入图片描述
完成上述步骤即可获得一个 Compose 的 Activity,通过以下代码即可完成调用继承自 View 的组件调用。

@Composable
fun CustomView() {AndroidView(modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI treefactory = { context ->// Creates viewStepView(context).apply {setText("第一步")setTextSize(30f)setStyle(StepView.STYLE_SELECTED)setType(StepView.TYPE_START)}},update = { view ->// 视图已膨胀或此块中读取的状态已更新// 如有必要,在此处添加逻辑// 由于selectedItem在此处阅读,AndroidView将重新组合// 每当状态发生变化时// 撰写示例->查看通信// 更新样式view.setText("第二步")})
}

最终效果

在这里插入图片描述

点击前往下载代码👉StepDemo

总结

能找美工处理的就找美工处理,能不碰 View 自定义就不要碰 View 自定义,一旦开始自定义,意味着需要花很长的时间去处理自定义产生的各种适配问题,投入的时间用与产生的收益不成正比。

参考文档

1、【扔物线】UI-1 Drawing
2、HenCoder Android 开发进阶: 自定义 View 1-1 绘制基础
3、HenCoder Android 开发进阶: 自定义 View 1-2 Paint 详解
4、【Android Developer】在 Compose 中使用 View

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

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

相关文章

JavaScript异常处理详解

​​​​​​​一、异常的捕获 1.1 基本的try…catch语句 ES3开始引入了 try-catch 语句&#xff0c;是 JavaScript 中处理异常的标准方式。 语法&#xff1a; try{ //可能发生异常的代码 }catch(error){ //发生错误执行的代码 } 看下面的代码&#xff1a; <script>tr…

记一次canal除坑记录

记一次canal除坑记录 错误信息 Caused by :com.alibaba.otter.canal.parse.exception.CanalParseException: column size is not match for table 问题处理 今天对Canal相关程序进行升级&#xff0c;原监听的表及业务都正常&#xff1b;遇到新增加的表时总是不走&#xff1b;…

202402读书笔记|《当你老了》——灰蒙曙光比爱情温柔,清晨露珠比希望更可爱

202402读书笔记|《当你老了》——灰蒙曙光比爱情温柔&#xff0c;清晨露珠比希望更可爱 《当你老了》作者叶芝&#xff0c;断断续续碎片时间读完的一本书&#xff0c;不是很惊艳&#xff0c;但值得一读。就因为很喜欢当你老了&#xff0c;所以拾起的这本书。读完知道了原来叶芝…

什么是软件安全性测试?如何进行安全测试?

一、什么是软件安全性测试&#xff1f; 软件安全性测试是指对软件系统中的安全漏洞进行检测和评估的过程。其目的是为了确保软件系统在面对各种安全威胁时能够保持其功能的完整性、可用性和机密性。 二、软件安全性测试可以通过以下几个步骤来进行&#xff1a; 1. 需求分析&a…

51单片机四位数码管计算器 Proteus仿真程序

目录 概要 仿真图 部分代码 资料下载地址&#xff1a;51单片机四位数码管计算器 Proteus仿真程序 概要 1.系统通过4x4的矩阵键盘输入数字及运算符。 2.可以进行4位十进制数以内的加法运算&#xff0c;如果计算结果超过4位十进制数&#xff0c;则屏幕显示E 3.可以进行加法以外…

LowB三人组(冒泡排序,插入排序,选择排序)(数据结构课设篇1,python版)(排序综合)

本章博客主要详细讲解一下LowB三人组排序&#xff0c;为什么叫LowB三人组呢&#xff1f;因为他们的时间复杂度都为O&#xff08;n^2&#xff09;。下篇博客会再讲解NB三人组&#xff08;堆排序&#xff0c;归并排序和快速排序&#xff09;&#xff0c;第三篇博客会讲解其他排序…

算法每日一题:队列中可以看到的人数 | 单调栈

大家好&#xff0c;我是星恒 今天是一道困难题&#xff0c;他的题解比较好理解&#xff0c;但是不好想出来&#xff0c;接下来就让我带大家来捋一捋这道题的思路&#xff0c;以及他有什么特征 题目&#xff1a;leetcode 1944有 n 个人排成一个队列&#xff0c;从左到右 编号为 …

Spring国际化的应用及原理详解

1. 简介 Spring国际化&#xff08;Spring Internationalization&#xff0c;简称i18n&#xff09;是Spring框架提供的一种机制&#xff0c;用于支持多语言的应用程序。它使得开发者能够轻松地在应用程序中实现不同语言的支持&#xff0c;从而满足全球化的需求。通过Spring国际…

面试必问究极重点之HashMap的底层原理

1.底层数据结构 JDK版本不同的数据结构 1.7 数组 链表 1.8 数组 &#xff08;链表 | 红黑树&#xff09; 2.添加数据put 在添加一个值的时候&#xff0c;首先会计算他的hash码&#xff0c;然后进行二次hash&#xff0c;在对当前长度取模得到在底层数组中的索引位置当取模完…

electron——查看electron的版本(代码片段)

electron——查看electron的版本(代码片段)1.使用命令行&#xff1a; npm ls electron 操作如下&#xff1a; 2.在软件内使用代码&#xff0c;如下&#xff1a; console.log(process) console.log(process.versions.electron) process 里包含很多信息&#xff1a; process详…

【Linux】——基本指令(二)

&#x1f497;个人主页&#x1f497; ⭐个人专栏——数据结构学习⭐ &#x1f4ab;点击关注&#x1f929;一起学习C语言&#x1f4af;&#x1f4ab; 目录 导读&#xff1a;1. vim 指令2. head指令3. tail指令4. tree指令5. 输出重定向6. echo指令7. wc指令8. | 字符9. date指令…

PCIe 6.0生态业内进展分析总结

上一篇&#xff0c;我们针对PCIe 6.0的功能更新与实现挑战做了简单的分析与总结。更多详细内容可以参考&#xff1a; 扩展阅读&#xff1a;浅析PCIe 6.0功能更新与实现的挑战 那么&#xff0c;PCIe 6.0已经发布了一段时间了&#xff0c;业内硬件支持PCIe 6.0目前有哪些进展呢…

面试算法93:最长斐波那契数列

题目 输入一个没有重复数字的单调递增的数组&#xff0c;数组中至少有3个数字&#xff0c;请问数组中最长的斐波那契数列的长度是多少&#xff1f;例如&#xff0c;如果输入的数组是[1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;6&#xff0c;7&#x…

外包干了5个月,技术明显退步了...

先说一下自己的情况&#xff0c;本科生&#xff0c;19年通过校招进入湖南某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年12月份&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测…

周鸿祎分享大模型十大趋势:2024将出现杀手级应用

1月5日&#xff0c;“2023年风马牛年终秀”上&#xff0c;三六零&#xff08;601360.SH&#xff0c;下称“360”&#xff09;集团创始人周鸿祎分享了对2024年大模型发展趋势的十大预测&#xff0c;呼吁企业树立AI信仰&#xff0c;All in AI。他认为&#xff0c;创新才能破局&am…

ctfshow——信息搜集

文章目录 web 1web 2web 3web 4web 5web 6web 7web 8web 9web 10web 11web 12web 13web 14web 15web 16web 17web 18web 19web 20 web 1 题目提示开发注释未及时删除。 直接右键查看源代码。 web 2 在这关我们会发现&#xff1a;1&#xff09;无法使用右键查看源代码&…

Linux服务器的几种类型

Linux是一个开源操作系统内核&#xff0c;用作各种Linux发行版&#xff08;也称为“distros”&#xff09;的核心组件。由Linus Torvalds于1991年开发&#xff0c;Linux基于Unix操作系统。它以其稳定性、安全性和多功能性而闻名。 Linux的关键特点&#xff1a; 开源性质&#…

OpenCASCADE MFC例子

OpenCASCADE MFC例子 说明 一直对OpenCASCADE一直都比较感兴趣&#xff0c;这个例子是我参考这位大神C幼儿园中班小朋友的专栏做出来的OpenCASCADE_C幼儿园中班小朋友的博客-CSDN博客 不过我用的是vcpkg的方式安装OpenCASCADE&#xff0c;这个需要注意一下&#xff0c;可能需…

[蓝桥杯学习] 树链剖分

定义 将树分割成若干条链&#xff0c;以维护树上的信息&#xff0c;若无特殊需求&#xff0c;一般是重链剖分。 重链剖分 如何重链剖分 两个dfs 第一个dfs是预处理各个结点的基本信息&#xff0c;第二个dfs是利用信息进行剖分&#xff08;dfs序&#xff09; 操作步骤 第一…

git在本地创建dev分支并和远程的dev分支关联起来

文章目录 git在本地创建dev分支并和远程的dev分支关联起来1. 使用git命令2. 使用idea2.1 先删除上面建的本地分支dev2.2 通过idea建dev分支并和远程dev分支关联 3. 查看本地分支和远程分支的关系 git在本地创建dev分支并和远程的dev分支关联起来 1. 使用git命令 git checkout…