Java下的框架编程(4)--Annotation vs XML vs Interface 最新一Round

本文探讨了注解(Annotation)与XML在Spring框架中的应用与对比,分析了两者的特点与适用场景,并讨论了它们如何提高开发效率及代码的可读性。此外,还分析了Spring框架如何通过这两种方式实现对POJO的无侵入性。
2005年11月28日 11:23:00
作者: 江南白衣

1.因为两者的用途不同,所以Annotation与XML应该并存

 Anootation vs XML 到了现在,在Spring和EJB3各占一边的持续演示下,各自的用途已清晰。
虽然两者之间完全可以相互替代,没有绝对不能做的事情,但却存在好不好的问题,两者的用途不同,应该并用:

"size=10,db_user=ito"这样的参数,仍然适合放在XML,YAML(ruby),properties,ini文件里。
而Annotation中所放,是真正程序的一部分,是旧编程体系中所缺的那一块东西。
 看看编程语言的发展:
一开始只是函数的封装;
到了OO语言,对象有了自己的属性和方法;
到了框架满天飞的年代,这些属性和方法又有了自己的属性,来告诉框架如何为自己提供服务。比如Spring和EJB3,POJO就用配置信息来告诉框架无侵入的提供服务。整个进化的过程非常自然。

因为这些信息本来就是程序的一部分,所以应该仍然放在Code里头,Spring把它割裂到XML明显影响了代码的阅读。

2.Anotation/XML PK Interface,Spring 无侵入性的基础
切尔斯基的Blog讲了Annotation/XML 对Interface的PK。这次PK,也可以认为是Spring 对 EJB2.1在框架无侵入性上的一次PK。
在EJB2.1时代,POJO必须通过接口来向框架声明一些东西,这就造成了框架的侵入性,强迫POJO实现一堆接口。而Spring,把这些元信息搬到了XML配置文件。
 在Spring里随便就可以举个例子。比如一个POJO,在Spring为它进行依赖注入A和B后,需要根据A和B来装配一个内部属性C,这样就需要再跑一个init()函数。Spring提供两种方式,一种侵入性的,实现InitializingBean接口的afterPropertiesSet()函数供Spring调用。 而另一种是无侵入性的,在Bean的xml节点里面自行定义init函数的名字。

3.Annotation目前的弱点
 一是Hibernate、Struts、三姑妈、四姨婆的annotation如果一股脑儿的全堆在可怜的POJO上很没人道,如果三姑六婆都抢Transation这个Anontation就更惨了。
二是annoation的表达能力有限,不如XML的强。

4.JSR250 - Common Annotations for the Java
Final Draft已经发布,八卦回来一看,也只是多定义了几个Common Annotation而已。
1.Generated 自动生成的代码要用此声明,而且必须说明工具的名称,如@Generated("com.sun.xml.rpc.AProcessor")

2. Resource/Resources 就是EJB3里面用的资源引用。

3.PostConstruct / PreDestroy 声明那些如题的函数。

还有一些安全方面的annotation,不关心。


文章:
Java下的框架编写(1)--序
Java下的框架编程(2)-对泛型的无聊用法和为擦拭法站台
Java下的框架编程(3)--关于反射的碎话
Java下的框架编程(4)--Annotation vs XML vs Interface 最新一Round
Java下的框架编程(5)--cglib应用
Java下的框架编程(6)--asm(待写)




Trackback: http://tb.blog.youkuaiyun.com/TrackBack.aspx?PostId=538306


<think>我们计划开发个自定义View,实现以下功能: 1. 圆弧扫描角度为150度(从-75度到75度,或者从105度到255度,具体看起点位置,这里我们选择从左侧开始,所以起点是-75度,终点75度,总共150度) 2. 圆弧上有个倒三角指示器,可以随着用户拖动而移动,指示当前进度 3. 进度可以拖动,即用户触摸并拖动指示器时,进度随之改变 实现思路: 1. 自定义View,重写onDraw方法绘制圆弧和指示器(倒三角)。 2. 重写onTouchEvent方法,处理触摸事件,根据触摸点位置更新进度。 3. 绘制圆弧:使用drawArc方法绘制背景圆弧和进度圆弧。 4. 绘制指示器:在圆弧上根据当前进度绘制个倒三角形。 5. 计算触摸点是否在指示器附近,并实现拖动。 步骤: 1. 定义必要的属性:圆弧的宽度、颜色,进度颜色,指示器颜色,当前进度,最大进度等。 2. 在构造方法中初始化属性,并获取自定义属性(如果需要)。 3. 在onSizeChanged中计算圆弧的矩形区域(RectF)。 4. 在onDraw中: a. 绘制背景圆弧(灰色) b. 绘制进度圆弧(进度部分,不同颜色) c. 根据当前进度计算指示器在圆弧上的位置,绘制倒三角指示器。 5. 在onTouchEvent中,检测触摸事件,判断触摸点是否在指示器附近,如果是则根据触摸点更新进度(需要将触摸点坐标转换为角度,再转换为进度值)。 注意:由于扫描角度是150度,所以我们需要从-75度开始画到75度(或者从105度到255度,但为了对称,我们选择以顶部为0度,左侧为负,右侧为正,所以从-105度到-105+150=45度?不,通常0度在右侧,但我们的圆弧是对称的,所以我们可以调整起始角度为-105度(即从左上开始),然后扫过150度到45度(右下)?但用户要求是150度,我们选择对称的圆弧,即圆弧的起点在左下方,终点在右下方,中间顶部为最高点?或者我们选择从顶部开始,但扫描150度,那么起点是90+75=165度?为了避免混淆,我们这样定义: 我们将圆弧的起点定义为从水平方向(3点钟方向)为0度,逆时针为正。但我们希望圆弧对称于垂直方向(即顶部为对称轴),那么: 总扫描角度150度,那么从90+75=165度开始,到90-75=15度结束?这样不对,因为165度到15度是逆时针旋转了150度(165到180是15度,0到15是15度,中间180到0是180度,总共15+180+15=210度,不对)。所以我们需要调整坐标系。 另种常见的做法:将圆弧的起始点设为-90度(顶部为0度,顺时针为正,那么-90度在左侧,90度在右侧),但我们希望对称,所以从-75度到75度(相对于顶部对称),那么总扫描角度为150度。但是,在Android中,drawArc方法使用的角度是: 起始角度:0度表示3点钟方向,逆时针为正。所以,如果我们希望圆弧对称于y轴(顶部),那么起始角度应该是90+75=165度(从3点钟方向逆时针165度,即从左上开始),然后顺时针扫描150度(因为第三个参数是扫描角度,我们使用正数表示顺时针,负数表示逆时针)?这样就会从165度顺时针画到315度(即-45度)?不对,这样不对称。 重新调整:我们希望圆弧的对称轴是垂直向上(y轴负方向),那么圆弧的起点和终点应该关于y轴对称。所以,我们选择起始角度为180度+75度=255度(从3点钟方向逆时针255度,即位于左下方),然后顺时针扫描150度到45度(右下方)。这样圆弧就是对称的。但是这样计算复杂。 更简单的方法:我们将圆弧的起始角度设置为120度(从3点钟方向逆时针120度,即左上),然后扫描150度(顺时针150度)到270度(右下)?这样也不对称。 实际上,我们可以这样定义:圆弧的起始角度为-105度(相对于3点钟方向,顺时针105度,即左上),然后扫描150度(顺时针150度)到45度(右下)。这样圆弧关于y轴对称。 但是,在Android中,drawArc的起始角度是0度指向正右方(3点钟方向),正角表示逆时针,负角表示顺时针。而扫描角度可以是正(逆时针)或负(顺时针)。如果我们希望从左上(-105度)开始顺时针画150度到右下(45度),那么: 起始角度:-105度(相当于逆时针旋转105度到左上,但注意负号表示顺时针,所以-105度实际上是顺时针105度,即从0度顺时针转105度,到达255度的位置(即-105度)?不对,0度顺时针105度是255度?不对,0度顺时针90度是270度,105度就是255度?不对,0度顺时针105度:0->360-105=255度?是的,255度。然后扫描150度顺时针(扫描角度为负值-150度)?或者用正角表示:起始角度255度,扫描角度150度顺时针(即用负数表示,-150度)?但drawArc的扫描角度用正数表示逆时针,负数表示顺时针。 所以,我们可以这样: 起始角度:255度(即从0度逆时针转255度,到达255度的位置,也就是左上方向) 扫描角度:-150度(顺时针150度,从255度顺时针150度到达255-150=105度?不对,角度是循环的,255-150=105度,但105度在右下?不对,255度在左上(255度在第三象限),顺时针150度后到达255-150=105度,在第二象限?不对,255度顺时针150度:255-150=105度,105度在第二象限(右上)?但我们希望到达右下(45度)?所以不对。 我们重新计算:我们希望圆弧从左上(大约210度)开始,到右下(大约330度)结束,且对称于y轴。那么对称轴是270度(即正上方12点钟方向是270度?在Android中,0度是3点钟方向,90度是12点钟方向,180度是9点钟方向,270度是6点钟方向)。所以,我们希望圆弧关于90度(即12点钟方向)对称。那么,起始角度应该是90-75=15度(逆时针15度,即右上),然后顺时针扫描150度到90+75=165度(左上)?这样也不对称。 实际上,我们可以将圆弧的起始点设为90+75=165度(左上),然后逆时针扫描150度到15度(右上)?这样圆弧就在上方,但方向是逆时针,从左上到右上,但这样圆弧是向上的,不符合我们的要求(我们希望是向下凸出的圆弧)。所以,我们想要个向下凸出的圆弧,那么应该从左上(165度)顺时针扫描到右上(15度),这样就是300度(360-60)的圆弧?不对,165顺时针到15度:165到360是195度,再加上0到15是15度,总共210度?不对。 为了简化,我们采用以下方案: 我们以View的中心为圆心,绘制个圆弧,起始角度为120度(相对于3点钟方向逆时针120度,即左上),然后扫描角度为300度(顺时针300度)?这样也不对。 实际上,我们常见的圆弧进度条是从-90度(顶部)开始,顺时针180度(到右侧底部)?但我们要150度,所以我们可以从-75度(相对于顶部顺时针75度,即左上)开始,然后顺时针扫描150度到75度(相对于顶部逆时针75度,即右上)?不对,因为-75度(顶部顺时针75度)实际上是15度(从0度顺时针75度是285度?)很乱。 我们换种思路:将圆弧的对称轴设为垂直向上(y轴负方向),那么圆弧的起点和终点分别位于对称轴两侧各75度。所以,起点角度为180度+75度=255度(从0度逆时针255度),终点角度为180度-75度=105度(从0度逆时针105度)?但是这样起点和终点之间的夹角是255-105=150度(逆时针),所以我们可以画个从255度逆时针到105度的圆弧(150度),但这样圆弧是逆时针画的,且位于下方?不对,255度在左下方,105度在右下方,逆时针从255到105需要经过0度,所以实际是150度(255->0是105度,0->105是105度,总共210度?不对)。 为了避免混淆,我们直接定义: 起始角度:从0度逆时针旋转的角度,作为起始角度。扫描角度:顺时针为正,逆时针为负?不,在drawArc中,扫描角度正数为逆时针,负数为顺时针。 我们想要个向下凸出的圆弧,对称轴在y轴负方向(即顶部),那么起始角度应该是:对称轴角度(90度,即12点钟方向)减去75度=15度(逆时针15度,即右上),然后顺时针扫描150度(所以扫描角度为-150度)到165度(逆时针165度,即左上)?不对,15度顺时针150度后到达15+150=165度(顺时针165度,相当于逆时针195度?)位置不对。 实际上,如果我们从15度开始,顺时针扫描300度才能到达165度(因为15到165顺时针需要150度?15到165顺时针:15->0(15度),0->360->165(165度),总共180度?不对,15到165顺时针是150度?15->30->...->165,正好150度?是的。所以,如果我们从15度开始,顺时针扫描150度,到达165度,那么圆弧就是从右上到左上,但这样圆弧是向上的(凸向上方),而我们希望向下凸。 所以,我们应该从165度开始,顺时针扫描150度到15度?这样圆弧就是向下凸的。因为165度在左上,15度在右上,顺时针扫描150度(从左上到右上)经过的是下方。所以: 起始角度:165度(从0度逆时针165度) 扫描角度:150度(顺时针150度)?但drawArc的扫描角度参数如果是正数表示逆时针,负数表示顺时针。所以我们要用-150度表示顺时针扫描150度。 因此,在drawArc中,我们这样绘制: drawArc(rectF, 165, -150, false, paint); 这样画出的圆弧就是从165度(左上)顺时针150度到15度(右上),对称轴在90度(顶部)位置,且向下凸出。 但是,注意:165度在左上,15度在右上,顺时针150度经过的区域是:从165度开始顺时针到165+150=315度(即右下)?不对,165度顺时针150度:165+150=315度(从0度逆时针315度,即右下),但我们要的是15度(右上)。所以不对。 重新理解:drawArc(rect, startAngle, sweepAngle, useCenter, paint) startAngle:起始角度,0度指向3点钟方向,逆时针为正。 sweepAngle:扫描角度,正值表示逆时针扫描,负值表示顺时针扫描。 如果我们设置startAngle=165,sweepAngle=-150: 从165度开始,顺时针扫描150度,到达的角度是165-150=15度(因为顺时针是角度减小)。所以,从165度顺时针150度到达15度,这个圆弧就是向下凸出的圆弧,对称于y轴(90度位置)。所以正确。 因此,我们采用: 起始角度:165度 扫描角度:-150度(顺时针150度) 绘制圆弧时,我们需要个矩形区域(RectF)来定义圆弧的边界。 6. 指示器的位置:根据当前进度计算圆弧上的点坐标。 角度 = 165 + 当前进度百分比 * (-150) (因为从165度顺时针150度,所以角度是递减的) 注意:当前进度百分比 = 当前进度值 / 最大值 角度值(弧度) = Math.toRadians(角度) 但注意,在计算坐标时,三角函数需要弧度。 圆心坐标:(centerX, centerY) 半径:圆弧的半径(RectF的宽度或高度的半减去些padding) 则指示器的位置: x = centerX + radius * Math.cos(angleRad) y = centerY + radius * Math.sin(angleRad) 但是注意:在数学坐标系中,0度是x轴正方向,y轴正方向是90度。而Android的Canvas坐标系是y轴向下为正,所以我们在计算时,角度需要调整:实际上,我们计算的角度是基于数学坐标系的,而Canvas的0度方向也是x轴正方向,但y轴正方向是向下,所以角度方向(逆时针)与数学坐标系致。因此,我们可以直接使用。 7. 绘制指示器(倒三角): 我们将在圆弧上的点(x, y)绘制个倒三角形。倒三角形可以由三个点构成:顶点(在圆弧上,指向圆心),另外两个点分别位于顶点的两侧(垂直于半径方向向外延伸点)。 但是,我们想要个倒三角,即尖角朝外。所以: 顶点:在圆弧上的点(x, y) 另外两个点:在顶点两侧,且与圆弧切线方向垂直(即沿着半径方向向内偏移点,然后向两侧扩展)?不对,倒三角应该是底边在圆弧内,顶点在圆弧上。所以: 我们定义: 顶点:圆弧上的点(x, y) 底边的两个点:从顶点沿着半径方向向内(圆心方向)移动定距离,然后向两侧(垂直于半径方向)扩展定距离。 具体: 半径方向向量:从圆心指向顶点,向量为 (dx = x - centerX, dy = y - centerY) 单位化:len = Math.sqrt(dx*dx+dy*dy), ux = dx/len, uy = dy/len 底边的中点:从顶点向内移动定距离(例如10dp)得到: (x1, y1) = (x - ux * indicatorHeight, y - uy * indicatorHeight) 然后,底边的两个点分别从中点向两侧垂直移动:垂直方向向量(与半径垂直)可以是 (uy, -ux)(-uy, ux) 吗?注意,在二维中,与向量(ux, uy)垂直的向量有两个:(-uy, ux)(uy, -ux)。我们取其中个,然后乘以底边宽度的半。 所以,底边左点:x2 = x1 + (-uy) * (indicatorWidth/2), y2 = y1 + ux * (indicatorWidth/2) 底边右点:x3 = x1 + uy * (indicatorWidth/2), y3 = y1 - ux * (indicatorWidth/2) 然后,连接顶点、左点、右点形成倒三角形。 8. 触摸事件处理: 当用户触摸时,我们需要判断触摸点是否在指示器附近(可以用个圆形区域判断,或者用指示器的区域判断,这里我们用圆形区域简单)。 计算触摸点到圆弧上当前指示器位置的距离,如果小于个阈值(比如指示器的大小),则认为触摸在指示器上。 然后,在拖动时,根据触摸点坐标计算角度,然后转换为进度。 如何根据触摸点计算角度? 圆心已知(centerX, centerY),触摸点(touchX, touchY) 计算向量:dx = touchX - centerX, dy = touchY - centerY 角度(弧度) = Math.atan2(dy, dx) 注意,atan2返回的是从x轴正方向逆时针旋转的角度,范围[-π, π]。 然后转换为角度值:angle = (float) Math.toDegrees(angleRad); 注意:由于角度可能是负值,我们可以转换为0~360度:if (angle < 0) angle += 360; 但是,我们的圆弧范围是从165度顺时针到15度(即角度从165度递减到15度,在0~360度中,15度小于165度,所以当我们得到角度值后,需要判断这个角度是否在圆弧范围内(15度到165度之间)?不对,因为我们是从165度顺时针到15度,所以角度值从165度减小到15度。在0~360度中,15度在165度的顺时针方向(因为15<165,所以顺时针方向角度减小)。所以,我们得到的角度值(0~360)中,在圆弧范围内的角度应该是从165度到360度,然后0度到15度?不对,165顺时针到15度,中间经过0度,所以角度值从165到360(195度)和0到15(15度),总共210度?不对,圆弧只有150度。所以我们需要将角度映射到进度。 我们定义圆弧的起始角度165度,结束角度15度(顺时针),但15度比165度小,所以我们可以这样处理: 将角度值转换为相对于起始角度的顺时针偏移量: 如果当前角度 <= 165度且当前角度>=15度,那么偏移角度 = 165 - 当前角度 (顺时针偏移量) 如果当前角度 > 165度(比如180度),那么顺时针偏移量 = 165 + (360 - 当前角度) 这样不对,因为165顺时针到180只有15度,但180度在圆弧外(因为圆弧只到15度,顺时针150度后是165-150=15度,所以圆弧范围是165度顺时针150度,即165~15度(经过0度))。所以,圆弧上的角度应该是:165~360(195度)和0~15(15度)?总共210度?不对,150度圆弧。 实际上,圆弧覆盖的角度范围是:从165度顺时针到15度,相当于角度值从165度开始,顺时针减少150度,所以角度值范围是[165, 15](但15小于165,所以用区间表示就是[165, 360]和[0,15])。所以,当触摸点角度在[15,165]区间内时,实际上不在圆弧上(因为圆弧在另外两个区间)。因此,我们需要判断: 如果当前角度在[15,165]区间内,则忽略(不在圆弧上)。 否则,如果当前角度在[165,360]区间,那么偏移量 = 当前角度 - 165 然后取反?不对,偏移量应该是从165度顺时针旋转到当前角度的度数,即165-当前角度(因为顺时针角度减小)?不对,比如当前角度是180,那么165顺时针到180是15度,所以偏移量15度(占150度的10%)。 如果当前角度在[0,15]区间,那么偏移量 = (360 - 165) + 当前角度 即195+当前角度?这样不对,因为165顺时针到0度是195度(165到360是195度),然后0度到15度是15度,总共210度?不对,应该是165顺时针到当前角度:165顺时针到0度是195度,然后0度到15度是15度,所以165到15度总共210度?但实际只有150度圆弧。所以我们的映射不对。 重新映射:我们定义圆弧的起点为165度,终点为15度(顺时针150度)。所以,我们将圆弧上的角度映射到[0,150]的进度: 设当前角度为angle(0~360) 如果angle在[165,360]区间,则进度 = (angle - 165) * (150/150) = angle-165 但这样不对,因为165到360有195度,而实际圆弧只有150度。所以我们需要将角度限制在圆弧内。 实际上,我们只需要计算从165度顺时针到当前角度的角度差,但不超过150度。所以: 如果angle >=165,那么偏移量 = angle - 165 但最大为150度(因为圆弧只有150度)?不对,比如angle=360,偏移量=360-165=195>150,所以我们要取最小值150?不对,360度在圆弧上吗?在圆弧的终点(15度)之后?不对,圆弧终点是15度,所以当angle>165+150=315度?不对,165+150=315度(顺时针150度)就是15度(因为165+150=315,而315度相当于-45度,即315度)?不对,165顺时针150度:165-150=15度(角度值),所以角度值15度对应圆弧终点。 所以,圆弧上的角度范围是:从165度(起点)顺时针到15度(终点),中间经过的角度值从165递减到15(但经过0度时重置为360)。所以,实际角度值范围是[15,165]的补集?不对,是[165,360]和[0,15]两个区间。 计算偏移量(圆弧上的位置): 如果angle在[0,15]区间,那么偏移量 = (165 - (angle + 360)) 不对,这样是负的。 我们这样计算:从165度顺时针到angle的角度 = (165 - angle) 但是,如果angle在[0,15]区间,那么165-angle是大于150的(比如angle=0,165-0=165>150),所以不对。 换种:将角度值归化到165度附近: 我们计算从165度顺时针到angle的角度差: 如果angle <= 165,那么角度差 = 165 - angle (因为165顺时针到0度是165度,0度到angle是angle度,总共165+angle度?不对) 实际上,我们考虑圆弧的起点是165度,终点是15度(相当于375度),所以: 总圆弧跨度:150度 当前角度相对于起点的位置:将角度加上360度(如果小于165度)再与起点相减。 具体: 如果angle < 165,则angle2 = angle + 360 否则,angle2 = angle 然后,偏移量 = angle2 - 165 (顺时针偏移量) 但是,这个偏移量可能大于150度(比如angle=0,则angle2=360,偏移量=360-165=195>150),所以我们需要限制偏移量在0到150之间。 所以,我们这样计算: float offset; if (angle < 165) { offset = 165 - angle; // 因为165顺时针到360是195度,然后0度到angle是angle度,所以总共195+angle度?不对,我们想要的是从165顺时针到angle的角度,当angle<165时,实际上已经超过了终点(15度),所以应该算作150度(最大值)? } else { offset = angle - 165; // 这个offset是逆时针的偏移量,但我们需要顺时针偏移量? } 这个逻辑不对。 为了简化,我们直接计算触摸点相对于圆心的角度,然后判断该角度是否在圆弧的范围内(即从165度顺时针150度到15度),然后计算该角度在圆弧上的比例。 更好的方法:将角度转换为圆弧上的比例位置(0~1): 首先,将角度归化到[0,360): angle = angle % 360; if (angle < 0) angle += 360; 然后,计算圆弧的起点角度(165度)和终点角度(15度,但15<165,所以终点角度可以表示为15+360=375度)。 那么,圆弧上的点对应的角度范围是:165度 ~ 375度(即165~360,0~15映射到360~375)?不对,375度就是15度。 所以,当前角度对应的归化角度:如果当前角度<165,则归化角度=angle+360,否则归化角度=angle。 然后,当前角度在圆弧上的位置 = (化角度 - 165) / 150 (150是总扫描角度) 但是,归化角度-165可能大于150(比如angle=0,归化角度=360,则360-165=195>150),所以我们需要限制: position = (normalizedAngle - 165) / 150; if (position > 1) position = 1; if (position < 0) position = 0; // 理论上不会小于0 但是,当触摸点在圆弧外时,我们不应该更新进度。所以,我们只处理归化角度在165到375之间(即归化角度在[165, 375])的触摸点,且归化角度-165不能超过150(即最大为375-165=210,但我们限制为150),所以当超过150时,我们取1(即100%)。 所以,进度百分比 = (normalizedAngle - 165) / 150 然后限制在0~1之间。 但是,这样计算,当归化角度=165时,进度0;归化角度=375(即15度)时,进度=(375-165)/150=210/150=1.4>1,所以我们需要限制: position = Math.min(1, (normalizedAngle - 165) / 150); 这样,当触摸点角度在圆弧终点(15度)之后,进度都取1。 9. 更新进度:根据计算出的position(0~1)更新当前进度,并刷新View。 10. 属性动画:如果支持,可以添加属性动画,当进度改变时平滑移动指示器。 由于代码较长,我们分步骤实现。 下面开始编写代码: 定义属性: private int mArcWidth = 20; // 圆弧宽度,单位dp private int mProgressColor = Color.BLUE; private int mArcColor = Color.GRAY; private int mIndicatorColor = Color.RED; private int mIndicatorHeight = 10; // dp private int mIndicatorWidth = 20; // dp private float mMaxProgress = 100; private float mCurrentProgress = 0; 在构造方法中获取自定义属性(如果有)。 在onSizeChanged中计算圆弧的矩形区域: int padding = getPaddingLeft(); int width = getWidth() - padding * 2; int height = getHeight() - padding * 2; // 取宽高中的最小值作为直径 int min = Math.min(width, height); // 计算圆弧的矩形区域 mRectF = new RectF(padding, padding, padding + min, padding + min); // 圆心 mCenterX = padding + min / 2; mCenterY = padding + min / 2; mRadius = min / 2 - mArcWidthPx / 2; // 减去圆弧宽度的半,让圆弧在矩形内 在onDraw中绘制: // 绘制背景圆弧 paint.setColor(mArcColor); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(mArcWidthPx); canvas.drawArc(mRectF, 165, -150, false, paint); // 绘制进度圆弧 paint.setColor(mProgressColor); float sweep = -150 * (mCurrentProgress / mMaxProgress); // 顺时针扫描的角度(负值) canvas.drawArc(mRectF, 165, sweep, false, paint); // 绘制指示器 // 计算指示器位置:角度 = 165 + sweep (因为sweep是负值,所以165+sweep就是当前进度对应的角度) float angle = 165 + sweep; // 单位:度 // 转换为弧度 double angleRad = Math.toRadians(angle); // 计算圆弧上的点 float x = (float) (mCenterX + mRadius * Math.cos(angleRad)); float y = (float) (mCenterY + mRadius * Math.sin(angleRad)); // 绘制倒三角形 drawIndicator(canvas, x, y, angleRad); 绘制指示器的方法: private void drawIndicator(Canvas canvas, float x, float y, double angleRad) { // 计算半径方向向量(从圆心指向圆弧上的点) float dx = x - mCenterX; float dy = y - mCenterY; float length = (float) Math.sqrt(dx * dx + dy * dy); // 单位向量 float ux = dx / length; float uy = dy / length; // 底边的中点:从顶点向圆心方向移动indicatorHeight float midX = x - ux * mIndicatorHeightPx; float midY = y - uy * mIndicatorHeightPx; // 计算垂直向量(垂直于半径方向):取(uy, -ux)(-uy, ux)都可以,这里取个,比如(-uy, ux)为垂直向量的个方向 float perpX = -uy; float perpY = ux; // 底边的两个点 float leftX = midX + perpX * (mIndicatorWidthPx / 2); float leftY = midY + perpY * (mIndicatorWidthPx / 2); float rightX = midX - perpX * (mIndicatorWidthPx / 2); float rightY = midY - perpY * (mIndicatorWidthPx / 2); // 绘制三角形 Path path = new Path(); path.moveTo(x, y); // 顶点 path.lineTo(leftX, leftY); path.lineTo(rightX, rightY); path.close(); Paint indicatorPaint = new Paint(); indicatorPaint.setColor(mIndicatorColor); indicatorPaint.setStyle(Paint.Style.FILL); canvas.drawPath(path, indicatorPaint); } 注意:上面的垂直向量计算,我们取(-uy, ux)作为垂直向量,然后乘以宽度的半得到两个底边点。但是,这样绘制的三角形方向是否正确?我们可以通过调试或调整方向。 触摸事件处理: @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 检查是否触摸在指示器上 if (isTouchOnIndicator(x, y)) { mIsDragging = true; return true; } break; case MotionEvent.ACTION_MOVE: if (mIsDragging) { updateProgress(x, y); return true; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsDragging = false; invalidate(); break; } return super.onTouchEvent(event); } private boolean isTouchOnIndicator(float touchX, float touchY) { // 计算当前指示器的位置 float sweep = -150 * (mCurrentProgress / mMaxProgress); float angle = 165 + sweep; double angleRad = Math.toRadians(angle); float x = (float) (mCenterX + mRadius * Math.cos(angleRad)); float y = (float) (mCenterY + mRadius * Math.sin(angleRad)); // 计算触摸点与指示器顶点的距离 float distance = (float) Math.sqrt((x - touchX) * (x - touchX) + (y - touchY) * (y - touchY)); return distance < mIndicatorTouchRadiusPx; // 触摸半径,比如20dp } private void updateProgress(float touchX, float touchY) { // 计算触摸点相对于圆心的角度 float dx = touchX - mCenterX; float dy = touchY - mCenterY; double angleRad = Math.atan2(dy, dx); float angle = (float) Math.toDegrees(angleRad); if (angle < 0) { angle += 360; } // 归化角度:如果angle<165,则+360 float normalizedAngle = angle; if (angle < 165) { normalizedAngle += 360; } // 计算进度百分比 float position = (normalizedAngle - 165) / 150; if (position < 0) { position = 0; } else if (position > 1) { position = 1; } mCurrentProgress = position * mMaxProgress; invalidate(); } 注意:在updateProgress中,我们直接更新当前进度,并重绘。 11. 单位转换:在构造方法中将dp转换为px。 以上是核心代码,下面给出完整自定义View的代码框架。 注意:由于代码较长,可能需要根据实际情况调整。 完整代码示例:</think>下面我将详细指导您在Android中实现自定义可拖动进度圆弧控件(扫描角度150度,带倒三角指示器)。以下是完整的实现方案: ### 实现思路 1. 创建自定义`View`继承自`View` 2. 绘制静态圆弧(150度扫描角度) 3. 添加可拖动的倒三角指示器 4. 实现触摸事件处理 5. 添加进度变化回调 ### 完整实现代码(Java) ```java import android.content.Context; import android.graphics.*; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import androidx.annotation.Nullable; public class ArcProgressView extends View { // 尺寸和颜色配置 private static final int DEFAULT_ARC_WIDTH_DP = 15; private static final int DEFAULT_INDICATOR_RADIUS_DP = 15; private static final int DEFAULT_START_ANGLE = 165; // 起始角度(0点在右侧) private static final int SWEEP_ANGLE = 150; // 扫描角度 private Paint arcPaint; private Paint indicatorPaint; private RectF arcRect = new RectF(); private Path indicatorPath = new Path(); private float currentProgress = 0; // 0~100 private float arcWidthPx; private float indicatorRadiusPx; private int centerX, centerY; private float radius; private OnProgressChangeListener listener; public ArcProgressView(Context context) { super(context); init(); } public ArcProgressView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { // 转换dp为px arcWidthPx = dpToPx(DEFAULT_ARC_WIDTH_DP); indicatorRadiusPx = dpToPx(DEFAULT_INDICATOR_RADIUS_DP); // 圆弧画笔配置 arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG); arcPaint.setStyle(Paint.Style.STROKE); arcPaint.setStrokeCap(Paint.Cap.ROUND); arcPaint.setStrokeWidth(arcWidthPx); arcPaint.setColor(Color.parseColor("#3F51B5")); // 进度色 // 指示器画笔配置 indicatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); indicatorPaint.setStyle(Paint.Style.FILL); indicatorPaint.setColor(Color.parseColor("#FF4081")); // 指示器色 } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); centerX = w / 2; centerY = h / 2; // 计算圆弧绘制区域(考虑padding) int size = Math.min(w, h) - getPaddingLeft() - getPaddingRight(); radius = size / 2f - arcWidthPx; arcRect.set( centerX - radius, centerY - radius, centerX + radius, centerY + radius ); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 1. 绘制背景圆弧 arcPaint.setColor(Color.LTGRAY); canvas.drawArc(arcRect, DEFAULT_START_ANGLE, SWEEP_ANGLE, false, arcPaint); // 2. 绘制进度圆弧 arcPaint.setColor(Color.parseColor("#3F51B5")); float sweep = SWEEP_ANGLE * (currentProgress / 100f); canvas.drawArc(arcRect, DEFAULT_START_ANGLE, sweep, false, arcPaint); // 3. 绘制倒三角指示器 drawIndicator(canvas, sweep); } private void drawIndicator(Canvas canvas, float sweepAngle) { // 计算指示器位置(圆弧上的点) float angle = DEFAULT_START_ANGLE + sweepAngle; double angleRad = Math.toRadians(angle); float x = (float) (centerX + radius * Math.cos(angleRad)); float y = (float) (centerY + radius * Math.sin(angleRad)); // 创建倒三角形路径 indicatorPath.reset(); float triangleSize = indicatorRadiusPx * 1.5f; // 计算方向向量(指向圆心) float dx = centerX - x; float dy = centerY - y; float length = (float) Math.sqrt(dx * dx + dy * dy); dx /= length; dy /= length; // 计算垂直向量(用于三角形底边) float perpX = -dy; float perpY = dx; // 构建三角形路径 indicatorPath.moveTo(x, y); // 顶点 indicatorPath.lineTo( x - perpX * triangleSize/2 + dx * triangleSize, y - perpY * triangleSize/2 + dy * triangleSize ); indicatorPath.lineTo( x + perpX * triangleSize/2 + dx * triangleSize, y + perpY * triangleSize/2 + dy * triangleSize ); indicatorPath.close(); canvas.drawPath(indicatorPath, indicatorPaint); } @Override public boolean onTouchEvent(MotionEvent event) { if (!isEnabled()) return false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: updateProgress(event.getX(), event.getY()); return true; } return true; } private void updateProgress(float touchX, float touchY) { // 计算触摸点相对于圆心的角度 float dx = touchX - centerX; float dy = touchY - centerY; double touchAngle = Math.toDegrees(Math.atan2(dy, dx)); // 转换为0-360度 if (touchAngle < 0) touchAngle += 360; // 计算相对于起始角度的进度 float relativeAngle = (float) touchAngle - DEFAULT_START_ANGLE; if (relativeAngle < 0) relativeAngle += 360; // 限制在扫描角度范围内 if (relativeAngle > SWEEP_ANGLE) { relativeAngle = (relativeAngle > 180) ? SWEEP_ANGLE : 0; } // 更新进度 currentProgress = (relativeAngle / SWEEP_ANGLE) * 100; // 刷新视图并回调 invalidate(); if (listener != null) { listener.onProgressChanged(currentProgress); } } public void setProgress(float progress) { currentProgress = Math.max(0, Math.min(progress, 100)); invalidate(); } public void setOnProgressChangeListener(OnProgressChangeListener listener) { this.listener = listener; } public interface OnProgressChangeListener { void onProgressChanged(float progress); } private float dpToPx(float dp) { return dp * getResources().getDisplayMetrics().density; } } ``` ### 使用示例(在Activity中) ```java public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ArcProgressView progressView = findViewById(R.id.arc_progress); progressView.setOnProgressChangeListener(progress -> { Log.d("Progress", "当前进度: " + progress + "%"); }); } } ``` ```xml <!-- activity_main.xml --> <com.example.ArcProgressView android:id="@+id/arc_progress" android:layout_width="300dp" android:layout_height="300dp" android:padding="20dp"/> ``` ### 关键实现细节 1. **150度圆弧实现**: - 使用`drawArc()`方法,起始角度165°,扫描角度150° - 公式:$ \text{角度} = 165^\circ + \frac{\text{进度}}{100} \times 150^\circ $ 2. **倒三角指示器**: - 通过路径(Path)绘制三角形 - 方向计算:使用向量计算指向圆心的方向 $$ \begin{cases} \Delta x = \text{centerX} - x \\ \Delta y = \text{centerY} - y \end{cases} $$ - 垂直向量用于构建三角形底边: $$ \text{垂直向量} = (-\Delta y, \Delta x) $$ 3. **触摸事件处理**: - 使用反正切函数计算触摸点角度: $$ \theta = \tan^{-1}\left(\frac{\Delta y}{\Delta x}\right) $$ - 角度范围映射到0-150度(扫描角度) 4. **性能优化**: - 使用`Paint.ANTI_ALIAS_FLAG`抗锯齿 - 在`onSizeChanged()`中预计算尺寸 - 避免在`onDraw()`中创建新对象 ### 相关问题 1. **如何添加渐变颜色效果到圆弧?** 使用`SweepGradient`着色器: ```java Shader shader = new SweepGradient(centerX, centerY, new int[]{Color.RED, Color.YELLOW, Color.GREEN}, null); arcPaint.setShader(shader); ``` 2. **如何实现平滑的拖动动画?** 使用`ValueAnimator`平滑过渡: ```java ValueAnimator animator = ValueAnimator.ofFloat(oldProgress, newProgress); animator.addUpdateListener(animation -> { setProgress((float) animation.getAnimatedValue()); }); animator.start(); ``` 3. **如何保存/恢复控件状态?** 重写`onSaveInstanceState()`和`onRestoreInstanceState()`: ```java @Override protected Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putFloat("progress", currentProgress); bundle.putParcelable("super", super.onSaveInstanceState()); return bundle; } @Override protected void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; currentProgress = bundle.getFloat("progress"); super.onRestoreInstanceState(bundle.getParcelable("super")); } } ``` 4. **如何添加刻度标记?** 在`onDraw()`中添加: ```java for (int i = 0; i <= 10; i++) { float angle = DEFAULT_START_ANGLE + (SWEEP_ANGLE / 10f) * i; // 计算刻度起止点 canvas.drawLine(/* 坐标计算 */); } ``` 5. **如何适配深色模式?** 使用资源文件定义颜色: ```xml <!-- res/values/colors.xml --> <color name="arc_color">#3F51B5</color> <!-- res/values-night/colors.xml --> <color name="arc_color">#BB86FC</color> ``` 在代码中获取: ```java arcPaint.setColor(ContextCompat.getColor(context, R.color.arc_color)); ``` 这个实现完全符合您的需求,包含150度扫描圆弧、可拖动的倒三角指示器,并提供了进度回调接口。您可以根据需要调整颜色、尺寸等参数[^1]。 [^1]: 参考Android绘图基础:Canvas和Paint的使用,以及自定义View触摸事件处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值