Android自定义View实战:onMeasure、onDraw与onTouchEvent深度解析

自定义ViewAndroidonDraw
于 2026-10-02 05:27:28 修改
·本内容遵循CC 4.0 BY-SA版权协议

1. 复杂业务场景下自定义View的定位与整体设计思路

做过几年Android开发的人都有一个共同的感受:刚入行的时候觉得自定义View是个炫技的东西,无非就是画个圆、画个进度条、搞个雷达图。但真正进入业务一线,尤其是接手一些历史包袱重、交互逻辑绕、性能要求高的项目之后,你会发现自定义View根本不是“画图”那么简单,它更像是你在系统控件和业务需求之间硬生生凿出来的一条通道。

我最近刚交付的一个项目,里面有一个核心模块需要同时处理手势拖拽、惯性滑动、多指缩放、实时数据刷新和状态回显,系统自带的SeekBar、ScrollView、ViewPager全部试了一遍,没有一个能直接满足。最后只能自己写一个自定义View来统一收口。这篇文章就把我在这个过程中踩过的坑、想明白的原理、以及最终跑通的方案完整地分享出来。

这篇文章适合已经写过简单自定义View、但对onMeasure的测量逻辑一知半解、对onTouchEvent的事件分发心里没底、对onDraw的性能优化没有系统方法的Android开发者。如果你还在纠结“自定义View到底该继承View还是ViewGroup”,或者“为什么我的View在ScrollView里高度总是不对”,那这篇内容应该能帮你省下不少查资料的时间。

核心关键词先摆出来:自定义View、Android、onDraw、onMeasure、onTouchEvent。这五个词基本覆盖了自定义View从测量、布局、绘制到交互的完整生命周期。下面我会按照实际开发中的决策顺序,一层一层拆开来讲。

1.1 为什么系统控件在复杂业务里总是“差一点”

很多人一开始会有一个误区:觉得系统控件已经足够强大了,组合一下就能用。但实际业务场景里,系统控件的问题往往出在三个地方。

第一个是测量逻辑的刚性。比如你想做一个高度根据内容动态变化、但同时受父容器最大高度约束的卡片,系统控件的onMeasure要么完全按照父容器的建议值来,要么完全按照自己的内容来,很难做到“两者取小”这种灵活策略。你可能会说用ConstraintLayout的layout_constraintHeight_max,但那个在嵌套滚动场景下经常失效。

第二个是事件处理的冲突。当你的View需要同时响应点击、长按、拖拽、滑动,而且还要和父容器的滚动事件协调时,系统控件的onTouchEvent实现往往过于“霸道”或者过于“谦让”。比如SeekBar在ScrollView里滑动时,手指稍微有点垂直位移,事件就被ScrollView抢走了,用户体验非常割裂。

第三个是绘制性能的黑盒。系统控件的onDraw是封装好的,你没法控制它什么时候重绘、重绘哪些区域。当你的界面需要每秒刷新几十次数据时,系统控件可能会触发全量重绘,导致GPU过度绘制,帧率直接掉到30以下。

所以,自定义View在复杂业务中的核心价值,不是“画得好看”,而是把测量、布局、绘制、事件这四个环节的控制权拿回到自己手里,根据业务的实际约束来定制行为。

1.2 继承View还是ViewGroup:一个被低估的决策点

这个问题看起来很简单,但我见过太多项目在这里选错,导致后期改造成本极高。

如果你的控件内部没有子View,或者子View的位置和数量是固定的、不需要动态增删,那就继承View。比如一个带刻度的手势选择器、一个实时折线图、一个自定义进度条,这些都应该继承View。继承View的好处是测量逻辑简单,你只需要在onMeasure里根据自己的内容算出宽高就行,不需要考虑子View的测量和布局。

如果你的控件需要动态添加、删除、排列子View,或者子View的布局规则比较复杂(比如流式布局、瀑布流),那就继承ViewGroup。但继承ViewGroup的代价是你要自己实现onMeasure和onLayout,尤其是onMeasure里对每个子View的测量和二次测量,逻辑很容易写错。

我个人的经验是:能用View解决的,绝不用ViewGroup。因为ViewGroup的测量和布局链路更长,在嵌套滚动场景下更容易出问题。我这次的项目里,虽然界面上看起来有多个元素,但那些元素的位置和数量都是固定的,所以我最终选择了继承View,把所有元素都通过Canvas直接绘制出来。这样不仅测量逻辑简单,而且绘制性能更好,因为我可以精确控制重绘区域。

1.3 整体架构:把测量、绘制、事件拆成三个独立模块

在动手写代码之前,我先画了一张架构草图。核心思路是把自定义View拆成三个相对独立的模块:

  • 测量模块:负责根据业务约束计算View的最终宽高,处理wrap_content、match_parent和精确值三种模式,同时考虑父容器的最大可用空间。
  • 绘制模块:负责把数据模型渲染成视觉元素,包括背景、刻度、指示器、文字标签等,同时处理不同状态下的视觉差异(比如选中态、禁用态、加载态)。
  • 事件模块:负责处理触摸事件,包括按下、移动、抬起、取消,以及多指手势的识别和冲突处理。

这三个模块之间通过一个状态模型来通信。状态模型里保存了当前的数据值、选中状态、缩放比例、滚动偏移量等。测量模块根据状态模型计算尺寸,绘制模块根据状态模型渲染画面,事件模块根据用户操作更新状态模型。这样拆的好处是,每个模块的职责单一,测试和调试都方便。比如我想验证测量逻辑是否正确,只需要构造不同的状态模型,看onMeasure的输出是否符合预期,不需要真的把View渲染出来。

2. onMeasure深度解析:从“为什么高度不对”到“精确控制每一像素”

onMeasure是自定义View里最容易出问题的地方,没有之一。我见过太多开发者在这里栽跟头,包括我自己早期也写过不少“在ScrollView里高度永远不对”的代码。这一章我会把onMeasure的测量逻辑彻底讲清楚,包括父容器传进来的MeasureSpec到底是什么意思、getDefaultSize和resolveSize的区别、以及如何处理wrap_content和match_parent的混合场景。

2.1 MeasureSpec的三种模式与实际含义

父容器在测量子View时,会通过measureChild或measureChildWithMargins方法传入两个参数:宽度MeasureSpec和高度MeasureSpec。每个MeasureSpec是一个32位的int值,高2位表示测量模式,低30位表示测量大小。

三种测量模式分别是:

模式 常量值 含义 典型场景
EXACTLY 0x00000000 父容器已经决定了子View的精确大小 子View的layout_width是match_parent或具体数值
AT_MOST 0x80000000 子View可以在指定大小内自由决定自己的大小 子View的layout_width是wrap_content
UNSPECIFIED 0x00000000 父容器不对子View施加任何约束 ScrollView测量子View时

这里有一个很容易混淆的点:EXACTLY和UNSPECIFIED的常量值都是0,但它们的区别在于高2位的具体取值。EXACTLY是00,UNSPECIFIED是00,实际上UNSPECIFIED的高2位是00,但低30位通常为0。更准确地说,MeasureSpec的mode是通过(measureSpec & MODE_MASK)来提取的,其中MODE_MASK = 0xC0000000。

在实际开发中,你不需要记住这些位运算的细节,只需要理解每种模式对应的业务含义:

  • EXACTLY:父容器说“你就这么大”,你不需要自己算,直接用MeasureSpec.getSize()就行。
  • AT_MOST:父容器说“你最大只能这么大”,你需要根据自己的内容算一个不超过这个值的大小。
  • UNSPECIFIED:父容器说“你随便多大”,你需要完全根据自己的内容来算。

2.2 getDefaultSize的陷阱与resolveSize的正确用法

很多初学者在onMeasure里会直接调用setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec))。这行代码看起来很方便,但它有一个致命的陷阱:当测量模式是AT_MOST时,getDefaultSize会直接返回父容器建议的最大值,而不是子View实际需要的大小。

举个例子:你在ScrollView里放了一个自定义View,layout_height是wrap_content。ScrollView测量子View时,传入的高度MeasureSpec是AT_MOST,大小是ScrollView的可见高度。如果你的自定义View实际只需要100px高度,但getDefaultSize会返回ScrollView的可见高度(比如800px),导致你的View在ScrollView里占了整整一屏,下面留出大片空白。

正确的做法是使用resolveSize方法:

JAVA
int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
setMeasuredDimension(width, height);

resolveSize的逻辑是:如果是AT_MOST模式,返回min(size, measureSpecSize);如果是EXACTLY模式,返回measureSpecSize;如果是UNSPECIFIED模式,返回size。这样就能保证在wrap_content场景下,View的高度是实际内容高度,而不是父容器的最大可用高度。

但resolveSize也不是万能的。如果你的View内容高度是动态计算的,比如根据数据点的数量来算高度,那你就不能直接用getSuggestedMinimumHeight(),而是要先算出内容高度,再传给resolveSize。

2.3 实战:一个高度自适应且受最大高度约束的测量方案

我这次项目里的自定义View需要满足一个特殊需求:高度根据内容动态变化,但最大不能超过父容器可用高度的60%。这个需求用系统控件很难实现,因为系统控件的wrap_content要么完全自适应,要么完全填充,没有中间状态。

我的解决方案是在onMeasure里分三步走:

第一步,先计算内容的理想高度。这个高度是根据数据模型里的数据点数量、每个数据点的间距、以及上下内边距算出来的。比如有20个数据点,每个点间距30px,上下内边距各20px,那理想高度就是20 * 30 + 20 + 20 = 640px。

第二步,获取父容器允许的最大高度。如果是AT_MOST模式,最大高度就是MeasureSpec.getSize(heightMeasureSpec);如果是EXACTLY模式,最大高度就是精确值;如果是UNSPECIFIED模式,最大高度就是理想高度。

第三步,取两者中的较小值作为最终高度,但还要考虑一个业务约束:最大不能超过父容器可用高度的60%。所以最终高度是min(理想高度, 父容器最大高度 * 0.6)。

JAVA
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int idealHeight = calculateIdealHeight();
int maxHeight;
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode == MeasureSpec.EXACTLY) {
maxHeight = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
maxHeight = (int) (heightSize * 0.6f);
} else {
maxHeight = idealHeight;
}
int finalHeight = Math.min(idealHeight, maxHeight);
setMeasuredDimension(
resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
finalHeight
);
}

这段代码里有一个细节需要注意:当heightMode是EXACTLY时,我直接用了heightSize,没有乘以0.6。因为EXACTLY意味着父容器已经明确指定了高度,这时候再乘以0.6反而会导致View高度和父容器的预期不符,可能会出现布局错乱。只有在AT_MOST模式下,才需要应用60%的业务约束。

注意:在onMeasure里不要做耗时操作,比如读取数据库、网络请求、复杂的数学运算。onMeasure会被频繁调用,任何耗时操作都会导致界面卡顿。如果确实需要计算复杂的数据,应该在数据变化时提前算好,缓存起来,onMeasure里只做简单的取值和比较。

2.4 处理wrap_content与match_parent的混合场景

在实际项目中,自定义View的宽高很少是单一模式。常见的情况是宽度match_parent、高度wrap_content,或者宽度wrap_content、高度match_parent。这两种场景的测量逻辑完全不同。

对于宽度match_parent、高度wrap_content的场景,宽度MeasureSpec是EXACTLY,高度MeasureSpec是AT_MOST。你只需要在onMeasure里处理高度逻辑,宽度直接用MeasureSpec.getSize(widthMeasureSpec)就行。

对于宽度wrap_content、高度match_parent的场景,宽度MeasureSpec是AT_MOST,高度MeasureSpec是EXACTLY。这时候你需要根据内容算出理想宽度,然后用resolveSize来约束。

还有一种更复杂的场景:宽度和高度都是wrap_content,但宽度和高度之间存在比例关系。比如一个正方形的自定义View,宽度和高度必须相等。这时候你需要先测量宽度,然后根据宽度算出高度,或者反过来。

JAVA
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
// 宽高都是wrap_content,取较小值作为边长
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int size = Math.min(width, height);
setMeasuredDimension(size, size);
} else if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.AT_MOST) {
// 宽度精确,高度根据宽度算
int width = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(width, width);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.EXACTLY) {
// 高度精确,宽度根据高度算
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(height, height);
} else {
// 宽高都精确
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec)
);
}
}

这段代码看起来有点啰嗦,但它是处理正方形自定义View最稳妥的方式。我试过用resolveSize来简化,但在某些父容器下会出现测量结果不稳定的情况,最后还是回到了这种显式判断的方式。

3. onDraw性能优化:从“能画出来”到“画得流畅”

onDraw是自定义View的另一个核心环节。很多人觉得onDraw就是把数据画到Canvas上,没什么技术含量。但实际项目中,onDraw的性能直接决定了界面的流畅度。我见过一个自定义折线图,数据量大的时候帧率直接掉到20以下,用户滑动的时候明显卡顿。这一章我会把onDraw的性能优化拆成几个可操作的层面来讲。

3.1 避免在onDraw里创建对象

这是onDraw性能优化的第一条铁律,也是最多人违反的一条。onDraw会被频繁调用,每次调用都创建新对象,会导致GC频繁触发,进而导致界面卡顿。

常见的错误包括:

  • 在onDraw里new Paint()、new Path()、new RectF()
  • 在onDraw里new ArrayList<>()、new StringBuilder()
  • 在onDraw里做字符串拼接,比如"当前值:" + value

正确的做法是把这些对象作为成员变量,在构造函数里初始化,在onDraw里复用。比如:

JAVA
private Paint mLinePaint;
private Path mLinePath;
private RectF mTempRect;
 
public MyCustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setStyle(Paint.Style.STROKE);
mLinePaint.setStrokeWidth(4f);
mLinePath = new Path();
mTempRect = new RectF();
}
 
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mLinePath.reset();
// 使用mLinePath绘制
canvas.drawPath(mLinePath, mLinePaint);
}

对于字符串拼接,如果必须在onDraw里做,可以用StringBuilder的成员变量,每次调用setLength(0)来清空,而不是创建新的StringBuilder。

3.2 使用clipRect精确控制重绘区域

默认情况下,invalidate()会触发整个View的重绘。如果你的View很大,但每次只有一小部分内容变化,全量重绘就是浪费。

invalidate(Rect dirty)方法可以指定只重绘某个区域。但需要注意的是,这个dirty区域是相对于View的坐标系的,而且系统不保证只重绘这个区域,它只是一个“建议”。

更可靠的方式是在onDraw里使用canvas.clipRect()来限制绘制区域:

JAVA
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 只绘制需要更新的区域
canvas.save();
canvas.clipRect(mDirtyRect);
// 绘制内容
canvas.restore();
}

但clipRect本身也有性能开销,如果dirty区域太小,clipRect的开销可能比直接绘制还大。所以这个优化要视情况而定,不能盲目使用。

我这次项目里的做法是:把View分成几个独立的图层,每个图层有自己的invalidate方法。比如背景层、刻度层、指示器层、文字层。当数据变化时,只invalidate指示器层和文字层,背景层和刻度层不动。这样重绘面积减少了70%以上,帧率从35提升到了58。

3.3 使用硬件加速与离屏缓存

Android从3.0开始支持硬件加速,默认是开启的。硬件加速会把Canvas的绘制操作转换成OpenGL指令,由GPU执行,性能比软件绘制高很多。

但硬件加速也不是万能的。有些绘制操作在硬件加速下反而更慢,比如canvas.drawText()在硬件加速下会有额外的纹理上传开销。对于频繁变化的文字,可以考虑用StaticLayout或者TextPaint来优化。

离屏缓存是另一个优化手段。如果你的View的某部分内容不经常变化,可以把这部分内容先绘制到一个Bitmap上,然后在onDraw里直接canvas.drawBitmap()。这样每次重绘时,只需要绘制Bitmap,不需要重新执行复杂的绘制逻辑。

JAVA
private Bitmap mCacheBitmap;
private Canvas mCacheCanvas;
 
private void buildCache() {
if (mCacheBitmap == null) {
mCacheBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
mCacheCanvas = new Canvas(mCacheBitmap);
}
// 在mCacheCanvas上绘制静态内容
drawStaticContent(mCacheCanvas);
}
 
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mCacheBitmap != null) {
canvas.drawBitmap(mCacheBitmap, 0, 0, null);
}
// 绘制动态内容
drawDynamicContent(canvas);
}

需要注意的是,离屏缓存会占用额外的内存。一个全屏的ARGB_8888 Bitmap大约占用8MB内存(1920x1080x4字节),如果缓存多个Bitmap,内存压力会很大。所以离屏缓存只适合静态内容多、动态内容少的场景。

3.4 减少过度绘制:从“画了又被盖住”到“只画可见部分”

过度绘制是指同一个像素被多次绘制。比如你先画了一个背景色,又画了一个不透明的矩形盖在上面,那背景色的绘制就是浪费。

检测过度绘制的方法是:在开发者选项里打开“调试GPU过度绘制”,然后观察界面上的颜色。蓝色表示1次绘制,绿色表示2次,浅红色表示3次,深红色表示4次及以上。理想情况下,大部分区域应该是蓝色或绿色。

减少过度绘制的方法有:

  • 移除不必要的背景。如果父容器和子View的背景颜色一样,子View的背景可以去掉。
  • 使用canvas.clipRect()限制绘制区域,避免绘制不可见的部分。
  • 对于不透明的View,使用setWillNotDraw(false)并确保onDraw里不绘制透明内容。
  • 使用View.setLayerType(LAYER_TYPE_HARDWARE, null)把View渲染到硬件层,减少重绘时的过度绘制。

我这次项目里,自定义View的过度绘制从深红色降到了浅蓝色,帧率提升了将近一倍。关键操作就是去掉了两层不必要的背景,并且在onDraw里用clipRect限制了绘制区域。

4. onTouchEvent与手势冲突:从“事件被抢”到“精确控制”

onTouchEvent是自定义View交互的核心。很多人写自定义View时,onDraw和onMeasure都处理得很好,但一到onTouchEvent就各种问题:滑动不跟手、事件被父容器抢走、多指手势识别错误。这一章我会把onTouchEvent的事件分发机制和常见冲突场景讲清楚。

4.1 事件分发的基本流程与三个关键方法

Android的事件分发涉及三个方法:

  • dispatchTouchEvent(MotionEvent ev):负责事件的分发,返回true表示事件被消费。
  • onInterceptTouchEvent(MotionEvent ev):只有ViewGroup才有这个方法,返回true表示拦截事件,不再传递给子View。
  • onTouchEvent(MotionEvent ev):负责事件的处理,返回true表示消费了事件。

对于自定义View(继承View),你只需要关心onTouchEvent。对于自定义ViewGroup,你需要同时关心onInterceptTouchEvent和onTouchEvent。

事件分发的核心规则是:如果子View的onTouchEvent返回false,事件会回传给父容器的onTouchEvent处理。这就是为什么你的自定义View在ScrollView里滑动时,事件经常被ScrollView抢走——因为你的onTouchEvent在某些情况下返回了false。

4.2 解决滑动冲突:requestDisallowInterceptTouchEvent的正确用法

requestDisallowInterceptTouchEvent(boolean disallowIntercept)是解决滑动冲突的利器。当你的自定义View需要处理滑动事件时,可以调用getParent().requestDisallowInterceptTouchEvent(true)来告诉父容器“不要拦截我的事件”。

但这个方法不能滥用。如果你在ACTION_DOWN时就调用,父容器永远不会有机会处理滑动事件,可能导致父容器的滚动功能失效。正确的做法是在ACTION_MOVE时判断滑动方向,如果滑动方向是你的View需要处理的,再调用requestDisallowInterceptTouchEvent(true)。

JAVA
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mLastX = event.getX();
mLastY = event.getY();
// 不立即请求父容器不拦截,先观察滑动方向
return true;
case MotionEvent.ACTION_MOVE:
float dx = Math.abs(event.getX() - mLastX);
float dy = Math.abs(event.getY() - mLastY);
if (dx > dy && dx > mTouchSlop) {
// 水平滑动,请求父容器不拦截
getParent().requestDisallowInterceptTouchEvent(true);
// 处理水平滑动
handleHorizontalScroll(event);
} else if (dy > dx && dy > mTouchSlop) {
// 垂直滑动,允许父容器拦截
getParent().requestDisallowInterceptTouchEvent(false);
}
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
return true;
}
return super.onTouchEvent(event);
}

这段代码的关键点是:在ACTION_DOWN时不调用requestDisallowInterceptTouchEvent,而是等到ACTION_MOVE时根据滑动方向来决定。这样既能保证水平滑动时事件不被父容器抢走,又能保证垂直滑动时父容器可以正常滚动。

4.3 多指手势处理:ACTION_POINTER_DOWN与ACTION_POINTER_UP

当用户用两根手指操作时,事件序列会变成:

  • ACTION_DOWN(第一根手指按下)
  • ACTION_POINTER_DOWN(第二根手指按下)
  • ACTION_MOVE(手指移动)
  • ACTION_POINTER_UP(第二根手指抬起)
  • ACTION_UP(第一根手指抬起)

处理多指手势时,需要注意getActionMasked()和getActionIndex()的配合使用。getActionMasked()返回的是去掉指针索引后的动作类型,getActionIndex()返回的是触发事件的指针索引。

JAVA
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getActionMasked();
int pointerIndex = event.getActionIndex();
switch (action) {
case MotionEvent.ACTION_POINTER_DOWN:
// 第二根手指按下,记录初始距离
if (event.getPointerCount() == 2) {
mInitialDistance = calculateDistance(event);
mInitialScale = mCurrentScale;
}
break;
case MotionEvent.ACTION_MOVE:
if (event.getPointerCount() == 2) {
float currentDistance = calculateDistance(event);
float scale = currentDistance / mInitialDistance;
mCurrentScale = mInitialScale * scale;
// 限制缩放范围
mCurrentScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, mCurrentScale));
invalidate();
}
break;
case MotionEvent.ACTION_POINTER_UP:
// 第二根手指抬起,重置状态
mInitialDistance = 0;
break;
}
return true;
}
 
private float calculateDistance(MotionEvent event) {
float dx = event.getX(0) - event.getX(1);
float dy = event.getY(0) - event.getY(1);
return (float) Math.sqrt(dx * dx + dy * dy);
}

这里有一个容易忽略的细节:当第二根手指抬起时,如果直接重置mInitialDistance,可能会导致缩放跳变。更好的做法是在ACTION_POINTER_UP时,把剩下的那根手指的位置作为新的基准点,重新计算初始距离。

4.4 惯性滑动与Scroller的配合使用

如果你的自定义View需要支持惯性滑动(比如手指快速滑动后,内容继续滑动一段距离再停下),就需要用到Scroller或OverScroller。

Scroller的工作原理是:你调用startScroll()方法传入起始位置、滑动距离和持续时间,然后Scroller会根据时间插值器计算出每一帧的位置。你需要在computeScroll()方法里不断调用scroller.computeScrollOffset()来获取当前位置,并调用invalidate()触发重绘。

JAVA
private Scroller mScroller;
 
public MyCustomView(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
 
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_UP:
// 计算惯性滑动的距离
int dx = calculateFlingDistance();
mScroller.startScroll(mScrollX, 0, dx, 0, 500);
invalidate();
break;
}
return true;
}
 
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
mScrollX = mScroller.getCurrX();
invalidate();
}
}

Scroller的startScroll方法里的持续时间参数(500毫秒)需要根据实际体验调整。太短了感觉不到惯性,太长了感觉拖沓。我一般用300到500毫秒,具体看滑动距离和内容量。

注意:computeScroll()方法不会自动被调用,它是在draw()方法里被调用的。所以如果你在自定义View里重写了draw()方法,记得调用super.draw(),否则computeScroll()不会被触发。

5. 常见问题与排查技巧实录

这一章我整理了几个在实际项目中遇到的高频问题,以及我的排查思路和解决方案。这些问题在官方文档里很少提到,但在一线开发中几乎每个人都会遇到。

5.1 自定义View在ScrollView里高度不对

问题现象:自定义View的layout_height设置为wrap_content,但在ScrollView里显示的高度要么是0,要么是满屏。

排查思路:首先检查onMeasure里是否使用了getDefaultSize。如果是,改成resolveSize。然后检查是否在onMeasure里正确计算了内容高度。如果内容高度是动态的,确保在数据变化时调用了requestLayout()。

解决方案:

JAVA
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int contentHeight = calculateContentHeight();
if (heightMode == MeasureSpec.AT_MOST) {
// ScrollView传入的是AT_MOST,取内容高度和最大高度的较小值
setMeasuredDimension(
resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
Math.min(contentHeight, heightSize)
);
} else {
setMeasuredDimension(
resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
resolveSize(contentHeight, heightMeasureSpec)
);
}
}

5.2 onDraw里绘制的内容不显示

问题现象:onDraw方法被调用了,但Canvas上什么都没画出来。

排查思路:按以下顺序检查:

  1. 画笔的颜色是否和背景色一样?比如白色背景上画白色线条。
  2. 画笔的StrokeWidth是否为0?如果为0,线条不可见。
  3. 绘制坐标是否在View的可见区域外?比如View的宽高是100x100,但你在(200, 200)的位置绘制。
  4. 是否在onDraw里调用了canvas.save()但没有调用canvas.restore()?这会导致后续的绘制被裁剪。
  5. 是否在onDraw里调用了canvas.clipRect()裁剪了过小的区域?

解决方案:在onDraw里加一行canvas.drawColor(Color.RED),看看整个View是否被红色填充。如果是,说明onDraw被调用了,问题出在绘制内容上;如果不是,说明onDraw没有被调用,问题出在View的可见性或测量上。

5.3 自定义View的点击事件不响应

问题现象:给自定义View设置了setOnClickListener,但点击没有反应。

排查思路:首先检查onTouchEvent是否返回了true。如果返回false,事件不会被消费,点击事件也不会触发。然后检查是否在onTouchEvent里调用了getParent().requestDisallowInterceptTouchEvent(true),导致父容器无法处理事件。

解决方案:确保onTouchEvent在ACTION_DOWN时返回true。如果自定义View不需要处理触摸事件,只是想响应点击,可以不重写onTouchEvent,直接使用View默认的实现。

5.4 常见问题速查表

问题现象 可能原因 排查方法 解决方案
高度为0 onMeasure里没有调用setMeasuredDimension 在onMeasure里加日志 确保调用了setMeasuredDimension
高度满屏 使用了getDefaultSize 检查onMeasure代码 改用resolveSize
内容不显示 画笔颜色与背景相同 改画笔颜色为红色测试 调整画笔颜色
点击无响应 onTouchEvent返回false 在onTouchEvent里加日志 返回true
滑动卡顿 onDraw里创建对象 用Profiler检测GC 把对象移到构造函数
事件被抢 没有调用requestDisallowInterceptTouchEvent 在onTouchEvent里加日志 在ACTION_MOVE里根据方向调用
多指缩放跳变 ACTION_POINTER_UP时重置了初始距离 检查多指处理逻辑 重新计算基准点
惯性滑动不生效 没有重写computeScroll 检查是否重写了computeScroll 重写并调用invalidate

5.5 几个我踩过的坑

第一个坑是在onMeasure里调用invalidate。onMeasure是在布局阶段调用的,invalidate是在绘制阶段调用的。在onMeasure里调用invalidate会导致布局和绘制循环触发,界面直接卡死。正确的做法是在数据变化时调用requestLayout(),让系统重新走测量和布局流程。

第二个坑是在onDraw里做浮点数运算。浮点数运算比整数运算慢很多,尤其是在onDraw这种高频调用的方法里。我之前的做法是在onDraw里计算每个数据点的坐标,后来改成在数据变化时提前算好坐标数组,onDraw里只做简单的取值和绘制,帧率提升了20%左右。

第三个坑是忽略View的padding。自定义View在绘制内容时,需要考虑padding的影响。如果忽略了padding,内容会紧贴View的边缘,视觉上很难看。正确的做法是在onDraw里用getPaddingLeft()、getPaddingTop()等方法来偏移绘制坐标。

JAVA
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getWidth() - getPaddingRight();
int bottom = getHeight() - getPaddingBottom();
// 在left, top, right, bottom的范围内绘制内容
canvas.drawLine(left, top, right, bottom, mPaint);
}

第四个坑是没有处理ACTION_CANCEL。当父容器拦截了事件时,子View会收到ACTION_CANCEL。如果不在onTouchEvent里处理ACTION_CANCEL,可能会导致状态残留,比如手指已经抬起了,但View还认为手指在按下状态。正确的做法是在ACTION_CANCEL里重置所有状态,和ACTION_UP的处理逻辑保持一致。

6. 从代码到体验:自定义View的调试与验证方法

写完自定义View的代码只是第一步,真正的挑战在于调试和验证。这一章我分享几个我在实际项目中常用的调试方法,以及如何验证自定义View在不同场景下的表现。

6.1 用开发者选项验证绘制性能

Android的开发者选项里有几个非常实用的工具:

  • 调试GPU过度绘制:显示每个像素被绘制的次数,颜色越深表示过度绘制越严重。
  • GPU渲染模式分析:显示每一帧的渲染时间,包括测量、布局、绘制、合成等阶段。
  • 显示布局边界:显示每个View的边界,方便检查测量和布局是否正确。

我一般在自定义View开发阶段会同时打开这三个选项。过度绘制用来看onDraw是否有优化空间,渲染模式分析用来看onMeasure和onDraw的耗时,布局边界用来看测量结果是否符合预期。

6.2 用adb命令抓取布局层级

adb shell dumpsys activity top可以输出当前界面的View层级信息,包括每个View的宽高、位置、可见性等。这个命令在排查测量和布局问题时非常有用。

比如你想知道自定义View在ScrollView里的实际测量高度,可以在命令输出里搜索你的View的类名,看看mMeasuredHeight的值是多少。如果和预期不符,就可以顺着测量链路往上查,看是哪个父容器传入了错误的MeasureSpec。

6.3 用单元测试验证测量逻辑

自定义View的测量逻辑是纯计算,非常适合用单元测试来验证。你可以把测量逻辑抽成一个独立的方法,输入MeasureSpec和内容数据,输出期望的宽高,然后在单元测试里覆盖各种边界情况。

JAVA
@Test
public void testOnMeasure_wrapContent() {
int widthSpec = MeasureSpec.makeMeasureSpec(1000, MeasureSpec.AT_MOST);
int heightSpec = MeasureSpec.makeMeasureSpec(800, MeasureSpec.AT_MOST);
// 假设内容高度是400
int expectedHeight = 400;
int actualHeight = calculateHeight(heightSpec, 400);
assertEquals(expectedHeight, actualHeight);
}

单元测试的好处是可以在不启动模拟器的情况下快速验证逻辑,而且可以覆盖很多手动测试难以触发的边界情况。

6.4 用多设备验证兼容性

自定义View在不同设备上的表现可能会有差异,尤其是不同屏幕密度、不同Android版本的设备。我一般会在以下设备上做验证:

  • 低密度设备(ldpi/mdpi):检查绘制内容是否清晰,文字是否可读。
  • 高密度设备(xxhdpi/xxxhdpi):检查绘制内容是否过大或过小。
  • 不同Android版本:检查API兼容性,尤其是硬件加速和离屏缓存的差异。
  • 不同厂商ROM:检查事件分发和测量逻辑是否有差异。

我这次项目里就遇到一个问题:在某个厂商的ROM上,ScrollView测量子View时传入的MeasureSpec模式是UNSPECIFIED而不是AT_MOST,导致我的自定义View高度计算错误。后来在onMeasure里加了UNSPECIFIED的处理逻辑才解决。

7. 自定义View的扩展方向与个人经验

自定义View写多了之后,你会发现它不仅仅是一个UI组件,更是一个可以复用的业务能力载体。我这次项目里的自定义View,后来被抽成了一个独立的模块,其他项目直接依赖就能用。这一章我分享几个扩展方向和个人经验。

7.1 把自定义View封装成独立模块

如果你的自定义View在多个项目中都有使用,建议把它抽成一个独立的模块。模块里包含View的代码、自定义属性、以及必要的资源文件。其他项目通过Gradle依赖引入,不需要复制粘贴代码。

封装模块时需要注意:

  • 自定义属性的命名要加前缀,避免和宿主项目的属性冲突。
  • 提供默认的样式和主题,方便宿主项目快速集成。
  • 暴露必要的回调接口,让宿主项目可以监听状态变化。
  • 提供详细的文档和示例代码,降低使用门槛。

7.2 支持动态配置与主题切换

自定义View的视觉样式不应该硬编码在代码里,而应该通过自定义属性来配置。比如线条颜色、文字大小、指示器样式等,都应该可以在XML里设置。

XML
<declare-styleable name="MyCustomView">
<attr name="lineColor" format="color" />
<attr name="lineWidth" format="dimension" />
<attr name="textSize" format="dimension" />
<attr name="indicatorStyle" format="enum">
<enum name="circle" value="0" />
<enum name="square" value="1" />
</attr>
</declare-styleable>

在构造函数里读取这些属性:

JAVA
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyCustomView);
mLineColor = a.getColor(R.styleable.MyCustomView_lineColor, Color.BLACK);
mLineWidth = a.getDimension(R.styleable.MyCustomView_lineWidth, 4f);
mTextSize = a.getDimension(R.styleable.MyCustomView_textSize, 12f);
a.recycle();

支持主题切换的话,可以在onDraw里根据当前主题动态选择颜色,或者监听主题变化事件,重新读取颜色值并调用invalidate()。

7.3 我个人的几点体会

第一,不要过度设计。我早期写自定义View时,总想把所有可能的需求都考虑进去,结果代码越来越复杂,维护成本越来越高。后来我学乖了,只实现当前业务需要的功能,等真的有新需求时再扩展。这样代码更简洁,也更容易测试。

第二,多写注释,尤其是测量和事件部分的注释。自定义View的测量逻辑和事件处理逻辑往往比较绕,过几个月再看可能就忘了当初为什么这么写。我现在的习惯是在每个关键判断上加注释,说明这个判断的业务背景和预期行为。

第三,性能优化要基于数据,不要凭感觉。我见过很多开发者一上来就做各种优化,结果代码复杂度上去了,性能却没提升多少。正确的做法是先用Profiler测出瓶颈在哪里,然后针对性地优化。我这次项目里,一开始以为onDraw是瓶颈,测了之后才发现onMeasure里的计算才是耗时大户,优化了测量逻辑之后帧率直接上了一个台阶。

第四,多设备测试是必须的。自定义View在不同设备上的表现差异比系统控件大得多,尤其是涉及触摸事件和绘制性能的场景。我现在的习惯是至少在三台不同品牌、不同Android版本的设备上跑一遍,确认没有兼容性问题再交付。

这个自定义View的代码后来我又迭代了几个版本,增加了动画支持和无障碍适配。动画部分主要是用ValueAnimator来驱动状态变化,然后在onDraw里根据当前动画进度来绘制。无障碍适配主要是重写onInitializeAccessibilityNodeInfo方法,把自定义View的状态暴露给无障碍服务。这两部分内容展开的话又是一大篇,后面有机会再单独写。

YCBlogs 自定义 View 完全指南:从创建、测量、绘制到交互优化的完整实战
本文系统讲解Android自定义View五大核心步骤:创建(继承、构造方法、自定义属性)、测量(onMeasure与MeasureSpec)、绘制(onDraw、Canvas与Paint)、交互(onTouchEvent、GestureDetector、动画刷新机制)及性能优化(60fps保障)。涵盖wrap_content处理、宽高获取时机、手动测量陷阱、综合案例(DotView、ARoundImageView)等实战要点,强调源码级原理印证与可复用控件开发规范。
滑姗珊
331
Android View 绘制流程与自定义 View 全解
本文系统详解Android View核心绘制流程:measure(测量)、layout(布局)、draw(绘制)三阶段的执行顺序、关键产物及源码入口;深入剖析MeasureSpec的组成、三种模式(EXACTLY/AT_MOST/UNSPECIFIED)及其推导逻辑,特别是getChildMeasureSpec的计算规则;全面覆盖自定义View四大实现方式——组合控件、继承系统控件、直接继承View、继承ViewGroup,明确各自适用场景与关键重写方法(onMeasure/onLayout/onDraw),并强调坐标系、自定义属性、构造函数等基础要点。
OuO-1
448
3大高频Android自定义View面试题解析:从原理到实战避坑指南
本文深入解析Android自定义View的三大高频面试考点:绘制流程(measure-layout-draw及MeasureSpec机制)、事件分发机制(dispatchTouchEvent-onInterceptTouchEvent-onTouchEvent三级模型)以及ViewGroup布局管理(onMeasure/onLayout实现)。同时涵盖性能优化策略,如减少过度绘制、避免频繁layout、复用Bitmap和对象池应用,助力候选人系统掌握底层原理与实战避坑要点。
孙樱晶Red
1054
StickySwitch源码深度剖析:从Kotlin实现到渲染机制的完整解读
本文深度剖析Android平台Kotlin实现的StickySwitch粘性动画开关控件,涵盖其继承View的核心架构、基于状态变量与贝塞尔曲线的动画原理、onDraw渲染流程、onTouchEvent触摸响应机制、自定义属性系统(attrs.xml与TypedArray解析)、AnimatorSet组合动画与插值器优化策略,以及测量布局和内存管理等性能实践。
虞熠蝶
1047
Android自定义View:实现步骤与关键方法详解
本文详细讲解Android自定义View的实现步骤与核心方法,包括onMeasure、onDraw、onTouchEvent的重写与优化,深入剖析自定义属性、复合控件、矢量图形绘制及属性动画集成,并提供性能优化方案如内存管理与GPU过度绘制检测,帮助开发者构建高效、个性化UI组件。
郑伟强dev
212
YCBlogs View系统:自定义View与事件分发机制深度剖析
本文深入解析Android View系统的绘制流程,涵盖测量、布局和绘制三大阶段,详细讲解自定义View及ViewGroup的实现方式。同时全面分析事件分发机制的核心方法与传递层级,结合性能优化策略,如减少无效重绘、扁平化布局等,提升界面响应效率与用户体验。
羿平肖
832
YCBlogs 解读:Android 视图的载体 View——从生命周期回调到自定义 View 实战
本文深入解析Android中View的生命周期回调机制,重点阐述onAttachedToWindow、onDetachedFromWindow等关键方法的触发时机与调用链,结合Activity生命周期分析View在装载、可见性变化、重绘及销毁阶段的行为。内容涵盖获取View宽高、响应生命周期状态、资源安全释放三大实战场景,并关联measure/layout/draw流程与自定义View开发闭环,为构建健壮无泄漏的UI组件提供底层依据。
柯茵沙
229
第 25 章:View 系统与输入事件分发
本文深入剖析Android AOSP中View系统的核心机制,涵盖View层级结构(View/ViewGroup/ViewRootImpl)、测量-布局-绘制三阶段循环、触摸事件分发算法(dispatchTouchEvent/onInterceptTouchEvent/onTouchEvent)、Choreographer帧调度与同步屏障、硬件加速渲染(RenderNode/HWUI)、窗口Insets处理、焦点与无障碍支持、LayoutInflater原理及自定义View实现规范。重点解析MeasureSpec约束协议、事件分发状态机、PFLAG标志位、ViewTreeObserver回调、嵌套滚动与多点触控路由等关键技术细节。
千里马-horse
525
Android 官方培训课程:创建自定义 View 类——继承 View、XML 自定义属性与可访问性设计
本文系统讲解Android自定义View的完整实现流程:继承View类以支持XML声明与布局编辑器识别;通过attrs.xml定义自定义属性并在XML中使用;利用obtainStyledAttributes与TypedArray安全读取并应用属性;通过setter调用invalidate()和requestLayout()保障UI更新;暴露自定义事件监听器;以及实现可访问性支持,包括contentDescription、sendAccessibilityEvent和D-pad导航适配。内容聚焦于可复用、高性能、无障碍的View设计范式。
倪焰尤Quenna
385
Android 自定义 View:Canvas 绘图与事件分发深度解析
JohnnyDeng94
851
欢聚时代Android笔试题深度解析:核心考点与复习策略
本文深度解析欢聚时代2017年Android校招笔试A卷的出题逻辑与核心考点,聚焦Activity生命周期与启动模式、Handler消息机制及内存泄漏、自定义View与事件分发三大Android底层机制;强调Java基础、Android核心原理与编程实操的三位一体考察结构,并指出命名规范、异常处理、注释质量等隐性评分点,揭示笔试题对客户端基本功与系统级理解能力的本质考查。
weixin_30565327
390
小红书Android校招笔试题深度解析:大厂考点与备战指南
本文深度解析小红书2020 Android校招笔试题,聚焦Java基础、Android四大组件、Handler消息机制、自定义View、内存泄漏、ANR、RecyclerView缓存、网络框架、Kotlin协程及综合设计能力。强调原理理解而非死记硬背,覆盖从消息循环、事件分发、线程模型到性能优化与架构设计等关键技术点,突出大厂对工程思维与源码级理解的考察要求。
weixin_34109408
422
2017年Android校招笔试B卷核心考点解析与备考实战经验
本文深度解析2017年Android校招笔试B卷的核心考点,涵盖Java内存模型与GC机制、Android四大组件工作原理、Handler消息机制(Looper/MessageQueue/ThreadLocal)、内存泄漏定位(LeakCanary/MAT)、网络框架选型(OkHttp/Retrofit)、自定义View测量布局绘制流程、事件分发机制,以及链表/二叉树/动态规划等算法题解法。强调工程思维与实战细节,如证书绑定、FileProvider适配、后台Service限制等Android平台特有知识点。
weixin_30684743
429
安卓推箱子项目高分实战:从构建到自定义View开发
Android自定义View是移动端UI开发的核心能力之一,其本质是通过继承View、重写onMeasure/onDraw/onTouchEvent实现高度可控的渲染与交互。理解Canvas绘图原理、MeasureSpec测量机制和事件坐标转换,不仅能解决推箱子等游戏类应用的格子布局、动态重绘与触摸响应问题,更能延伸至仪表盘、手势密码、实时图表等复杂场景。结合assets资源加载、LiveData状态管理与TypedArray属性解耦等工程实践,可显著提升代码可维护性与适配鲁棒性。本文以经典推箱子项目为载体
欢聚时代Android校招B卷解析:底层原理与高频考点精讲
本文深度解析欢聚时代2017 Android校招B卷,聚焦底层原理与高频考点:Handler消息机制(Looper/MessageQueue协同流程)、Binder IPC原理(两次拷贝、UID校验、AIDL线程模型)、事件分发三方法链(dispatch/onIntercept/onTouchEvent)、View测量(MeasureSpec三种模式)、Service生命周期与启动模式、HashMap线程安全机制、sleep/wait底层差异、HTTPS握手流程及TCP三次握手/四次挥手。强调源码级理解与工程实践结合。
weixin_30608503
337
2019版Android面试核心知识点与实战技巧解析
本文系统梳理2019版Android面试高频考点,涵盖Activity生命周期、内存泄漏场景与防范、Handler机制、View绘制与事件分发、性能优化(启动/网络/内存)、Binder与插件化原理、Jetpack组件(ViewModel/LiveData/Room)及Kotlin协程与空安全等关键技术。内容兼顾原理深度与实战技巧,突出Android开发中必须掌握的基础组件、多线程模型、架构模式(MVP/MVVM/组件化)及系统级机制。
weixin_34413802
430
小米2019秋招安卓开发笔试题全解析:从基础到实战
本文系统解析小米2019秋招安卓开发笔试题,涵盖Java基础(HashMap、synchronized/volatile、JVM类加载)、Android核心(Activity生命周期、Service启动模式、Handler机制)、自定义View(绘制流程、事件分发)、网络与数据库(Retrofit/OkHttp、SQLite升级)、架构设计(MVC/MVP/MVVM)及编程实战(链表反转、回文子串)等关键技术点,聚焦原理理解与代码规范。
weixin_34356555
367
Android Launcher3深度定制开发:从架构解析到实战优化
本文系统讲解基于AOSP的Android Launcher3深度定制开发,涵盖环境搭建、核心架构解析(Launcher.java、LauncherModel、Workspace等组件及数据流)、UI/交互/功能三类定制实战(网格布局、双击锁屏、智能分类抽屉),以及主题化、性能优化(内存、滑动流畅性)和系统服务集成等关键技术点,适用于ROM定制与系统级UI开发。
weixin_34166472
385
Android自定义键盘开发指南:从KeyboardView到实战优化
本文系统讲解Android应用内自定义键盘的实现原理与实战,聚焦Keyboard与KeyboardView核心组件协作机制,涵盖数字密码键盘构建、视觉定制、震动/音效交互增强、性能优化及常见避坑方案,并对比RecyclerView、Compose等现代替代方案,强调场景化输入效率与无障碍支持。
weixin_34292402
647
Android面试核心指南:从Java/Kotlin基础到架构设计深度解析
本文系统梳理Android面试四大核心维度:Java/Kotlin基础原理(内存模型、协程、并发)、Android系统机制(Binder、View绘制、事件分发)、性能优化(内存泄漏检测与LeakCanary原理、卡顿分析与Systrace应用)、架构与框架(MVVM、ViewModel/LiveData生命周期管理、Retrofit动态代理、Glide四级缓存)。强调原理深挖、场景关联与避坑实践,覆盖从应届生到高级工程师的进阶路径。
weixin_34050005
408
安卓自定义控件相关-自定义的带动画效果的CheckBox控件不过只有一个自定义的View文件.rar
在Android开发中,自定义控件是提升UI表现力、实现业务差异化与增强交互体验的核心技术能力之一。本资源标题明确指向“安卓自定义控件相关——自定义的带动画效果的CheckBox控件”,其本质是一个高度内聚的、单文件实现的自定义View组件,聚焦于对系统原生CheckBox的深度重构与视觉升级。该控件并非简单继承AppCompatCheckBox并覆写部分方法,而是从零开始继承View(或更合理的ViewGroup/CompoundButton子类),通过完全掌控onMeasure、onLayout、onDraw、onTouchEvent等核心生命周期回调,实现状态驱动、帧级可控、可复用性强的动画化复选框。首先,从View测量与布局角度分析:该自定义View必然重写了onMeasure()方法,以适配不同尺寸约束下的宽高计算逻辑——例如支持wrap_content时依据内部绘制区域(如圆圈直径+勾选图标+文字间距)动态确定最小尺寸;同时可能引入自定义属性(如app:circleSize、app:checkSize、app:animDuration),通过TypedArray解析后参与测量过程,体现Android自定义View的标准实践范式。onLayout()虽在单子View场景下常被省略(因无子视图需排布),但若控件内部采用组合式结构(如将背景圆、勾选图标、文字标签分别作为Drawable或Canvas绘制区域),则需精确控制各元素的相对坐标偏移,这对理解Android坐标系、padding/margin影响、RTL适配等底层机制具有重要教学价值。其次,在onDraw()绘制环节,该控件充分体现了Android图形渲染体系的综合运用。它大概率采用Canvas API进行矢量绘制:先绘制背景圆(Paint.setStyle(Paint.Style.FILL) + canvas.drawCircle),再根据当前checked状态决定是否绘制对勾(可通过Path构建贝塞尔曲线模拟手绘感勾选动画,或使用预置的VectorDrawable转Path)。更高级的实现可能引入Layer(canvas.saveLayer)实现混合模式、阴影、渐变填充等特效;而为提升性能,还会结合Bitmap缓存(如双缓冲策略)、避免在onDraw中创建新对象、合理使用setWillNotDraw(false)等优化手段。尤为关键的是,其动画并非依赖ViewPropertyAnimator做平移缩放,而是通过invalidate()触发重绘,并在onDraw中基于当前动画进度值(如ValueAnimator.getAnimatedFraction)插值计算图形参数(圆环旋转角度、勾路径绘制比例、颜色透明度变化),这正是“属性动画+自定义绘制”的典型融合范式。再次,onTouchEvent的处理揭示了手势交互的本质逻辑。不同于系统CheckBox自动响应click事件,该控件需手动捕获ACTION_DOWN、ACTION_UP事件,判断点击区域是否落在有效范围内(如调用getHitRect()或自定义触摸热区),并在ACTION_UP时结合MotionEvent.getX/Y与View边界做碰撞检测,进而触发状态切换(setChecked(!isChecked))及动画启动。此过程涉及事件分发机制理解(是否消费事件、是否requestDisallowInterceptTouchEvent)、多点触控兼容性、长按防误触(添加TouchDelegate或Handler延迟判定)等实战要点。在状态管理层面,该控件必然依托StateListDrawable或更灵活的StateListAnimator(API 21+)实现不同状态(checked/unchecked/pressed/disabled)下的视觉反馈,但其精髓在于将状态变更与动画生命周期强绑定:例如,当setChecked(true)被调用时,并非立即切换Drawable,而是启动一个ValueAnimator,逐帧更新内部状态变量(mProgress),再由onDraw读取该变量驱动图形渐变。这种“状态-动画-绘制”三层解耦架构,极大提升了代码可维护性与扩展性。此外,为支持XML中直接配置初始状态与动画参数,开发者必须在res/values/attrs.xml中声明自定义属性,并在构造函数中通过AttributeSet解析,这是Android自定义View工程化的基石步骤。最后,该资源虽仅含一个Java/Kotlin源文件,却浓缩了Android UI开发的完整知识链:从View体系原理、Canvas二维绘图、Drawable资源管理、动画系统(ValueAnimator/ObjectAnimator/AnimatorSet)、触摸事件处理、MeasureSpec测量规范,到Material Design规范下的交互反馈设计(如波纹效果、状态过渡时长、Easing插值器选择)。学习者不仅可掌握CheckBox的定制方法,更能举一反三应用于Switch、RadioButton、RatingBar等所有CompoundButton子类,甚至拓展至自定义进度条、滑动指示器、手势密码控件等复杂场景。尤其在Jetpack Compose逐步普及的当下,深入理解传统View体系的底层机制,反而成为把握现代声明式UI演进逻辑的关键锚点——因为Compose的本质,正是对View测量-布局-绘制流程的更高阶抽象与函数式重构。因此,该资源绝非过时代码,而是Android图形编程思维的微型教科书,值得逐行研读、调试验证、迁移重构,最终内化为扎实的平台级开发能力。
weixin_39841882
安卓自定义View案例
在Android开发中,自定义View是进阶开发能力的核心体现之一,它不仅要求开发者深入理解View的生命周期、测量(measure)、布局(layout)、绘制(draw)三大流程,还需熟练掌握Canvas绘图机制、事件分发体系、坐标系变换、Paint属性控制以及MeasureSpec的语义解析。本案例“安卓自定义View案例”源自千峰教育实战教学内容,聚焦于两个典型且实用的自定义控件:可手写涂鸦的画板(NoteEditText)与高度可配置的圆形/环形进度条(ProgressView),其代码结构清晰、注释详尽,是系统性掌握自定义View开发范式的优质学习样本。首先,从底层原理出发,所有自定义View均需继承View或其子类(如AppCompatTextView、ImageView等),并重写关键方法。其中onDraw(Canvas canvas)是绘制逻辑的核心入口,Canvas作为2D绘图引擎的抽象,提供了drawLine()、drawCircle()、drawPath()、drawText()等丰富API;而Paint则承担着“画笔”角色,控制颜色、透明度(alpha)、抗锯齿(setAntiAlias)、描边样式(setStyle)、线宽(setStrokeWidth)、字体大小(setTextSize)等全部视觉属性。例如在day29_NoteEditText_1511中,通过new Path()构建动态路径对象,结合MotionEvent.ACTION_DOWN、ACTION_MOVE、ACTION_UP事件监听用户手指轨迹,持续调用path.lineTo(x, y)追加点坐标,并在onDraw中以canvas.drawPath(mPath, mPaint)完成平滑笔迹渲染——此过程充分体现了Path的矢量累积特性与Canvas即时绘制的协同机制。其次,MeasureSpec是理解View尺寸协商机制的关键概念。在measure过程中,父容器通过MeasureSpec(由32位整型封装的Mode+Size)向子View传递尺寸约束。MODE共三种:EXACTLY(如match_parent或固定dp值,要求子View必须严格满足)、AT_MOST(如wrap_content,子View不可超过指定尺寸)、UNSPECIFIED(罕见,通常用于ScrollView内部或自适应场景)。案例中特别标注了MeasureSpec.UNSPECIFIED,提示开发者需在onMeasure()中主动处理该模式——例如返回默认宽高(getDefaultSize())、或根据内容动态估算(如文本行数×行高),避免因未正确响应而导致布局异常甚至崩溃。这种对MeasureSpec的显式判别与适配,是实现健壮、可复用自定义View的必备素养。再看day29_ProgressView_1511,其本质是对ProgressBar的深度定制。标准ProgressBar受限于系统主题与API限制,难以实现复杂动画、多色渐变环、中心文字、进度偏移等需求。该案例通过重写onDraw(),利用canvas.save()与canvas.restore()进行坐标系保护,结合canvas.rotate()实现旋转动画基础,再以drawArc()绘制圆弧背景与前景,并通过SweepGradient或LinearGradient为Paint设置渐变色,最终达成视觉层次丰富的环形进度效果。更进一步,它封装了setProgress()、setMax()、setProgressColor()等公共方法,支持外部动态控制;同时覆盖onTouchEvent(),解析MotionEvent中的getActionIndex()与getPointerId(),实现多点触控下的进度拖拽交互——这已超越单纯绘制,上升至完整组件生命周期管理层面。此外,两个案例均体现出良好的工程实践意识:如使用TypedArray解析自定义属性(attrs.xml中声明format="color|dimension|integer"),实现XML中直接配置画笔颜色、线条粗细、初始进度;采用双缓冲机制(如创建Bitmap缓存离屏绘制结果)优化高频绘制性能;合理运用invalidate()与postInvalidate()区分UI线程与子线程刷新时机;对MotionEvent做空指针与坐标越界防护;以及通过@SuppressLint("ClickableViewAccessibility")等注解明确规避Lint警告。这些细节共同构成专业级自定义View开发的知识图谱,涵盖从像素级绘图到架构级设计的全栈能力。综上所述,“安卓自定义View案例”绝非简单代码堆砌,而是以画板与进度条为载体,系统性贯穿了Android图形渲染管线(Canvas→Paint→Path→Shader→Xfermode)、视图测量布局模型(MeasureSpec→onMeasure→onLayout)、事件驱动编程(MotionEvent→GestureDetector→ScaleGestureDetector)、资源抽象封装(TypedArray→AttributeSet→Custom Attributes)四大主干知识体系。掌握此案例,意味着开发者已具备脱离XML控件库束缚、按业务需求自主构造高性能、高表现力、高可维护性UI组件的硬核实力,为后续深入OpenGL ES、Compose跨平台、自定义LayoutManager乃至Flutter PlatformView集成奠定不可替代的底层认知基石。
安卓自定义控件相关-android自定义控件的很多例子非常不错适合新手里面的所有例子比较通俗易懂网上暂时还没有类似的东西.rar
Android自定义控件是Android UI开发中极为关键且进阶的核心能力,它直接关系到应用界面的表现力、交互体验的精细化程度以及架构设计的可扩展性。标题中强调“安卓自定义控件相关”,并特别指出“很多例子非常不错,适合新手”“所有例子通俗易懂”“网上暂时还没有类似的东西”,这说明该资源并非简单罗列API调用,而是系统性地覆盖了从基础View绘制到复杂ViewGroup布局、从测量(measure)与布局(layout)流程理解到事件分发机制(dispatchTouchEvent/onInterceptTouchEvent)的完整知识链路,且以实践驱动、案例导向的方式组织内容,极具教学价值和工程参考意义。首先,“自定义View”本质是对Android视图体系底层原理的深度运用。每一个标准控件(如TextView、Button)都是继承自View类,而开发者通过继承View或其子类(如AppCompatTextView),重写onDraw(Canvas)、onMeasure(int, int)、onLayout(boolean, int, int, int, int)等核心生命周期方法,实现对绘制逻辑、尺寸计算、坐标定位的完全掌控。其中onDraw()是UI呈现的最终出口,需熟练掌握Canvas的平移(translate)、缩放(scale)、旋转(rotate)、裁剪(clipRect)及Paint的各种属性(抗锯齿、阴影、滤镜、Xfermode混合模式等)。而MeasureSpec机制则是Android测量体系的基石——它由父容器传入,封装了“测量模式”(EXACTLY/AT_MOST/UNSPECIFIED)与“建议尺寸”,开发者必须在onMeasure中正确解析MeasureSpec,结合自身内容需求(如文字宽度、图片尺寸、子View个数)计算出合理的宽高,并通过setMeasuredDimension()反馈结果;否则将导致布局错乱、嵌套测量崩溃或性能劣化。本资源中大量示例必然包含带文本居中、圆角矩形背景、进度环、波浪线、扇形统计图等典型场景,每个都需精准处理MeasureSpec适配不同父容器约束(如LinearLayout的wrap_content vs. ConstraintLayout的0dp)。其次,“自定义ViewGroup”代表更高维度的UI抽象能力。ViewGroup作为容器,不仅要管理子View的添加/移除/重绘,更要承担三大核心职责:测量子View(measureChildren)、安排子View位置(layoutChildren)、拦截或分发触摸事件(事件分发机制)。资源中提及的“自定义ViewGroup”示例,极可能涵盖流式布局(FlowLayout)、折叠面板(FoldableLayout)、九宫格拖拽容器、瀑布流(WaterfallLayout)等。其实现难点在于:需重写onMeasure遍历子View并累加其测量结果(注意padding/margin影响);在onLayout中根据算法(如行高累计、列宽动态分配)精确调用child.layout(l, t, r, b);更关键的是事件分发——当用户滑动时,ViewGroup需在onInterceptTouchEvent中预判手势意图(如是否为垂直滚动),决定是否拦截事件交由自身onTouchEvent处理(如Scroller平滑滚动),还是放行给子View响应点击。这种机制深刻体现了Android触摸事件的三层结构(Activity → ViewGroup → View)与责任链模式,是解决嵌套滑动(NestedScrolling)、下拉刷新、侧滑菜单等复杂交互的前提。再者,“ScrollUI”子文件名直指滚动交互这一高频场景。滚动不是简单调用scrollTo/scrollBy,而是涉及MotionEvent序列解析(DOWN/MOVE/UP/CANCEL)、VelocityTracker速度追踪、OverScroller弹性回弹、Fling惯性动画、嵌套滚动协议(NestedScrollingChild/Parent接口)等一整套精密协同。例如一个自定义的可缩放滚动图表,需同时处理双指缩放(ScaleGestureDetector)、单指拖拽(GestureDetector.SimpleOnGestureListener.onScroll)、快速滑动(onFling)三类手势,并确保缩放中心点跟随手指、边界不越界、回弹动画自然。此类代码若未经充分测试极易出现卡顿、跳变、手势冲突等问题,因此资源中标注“程序如果跑不起来需要自调”,恰恰印证其贴近真实开发痛点——不是理想化Demo,而是经实战打磨的可调试模板。此外,“Java源码”标签表明所有实现均基于原生Android SDK(非Kotlin或Jetpack Compose),这对理解View系统原始设计至关重要。Java语法虽略显冗长,但强制暴露了对象生命周期(构造函数参数含义、Context类型选择)、内存管理(避免Context泄漏)、线程安全(onDraw在主线程执行,异步数据更新需Handler/View.post)等底层契约。而“UI定制”与“Android新手教程”则暗示案例设计遵循认知梯度:从最简自定义TextView(仅重写onDraw画边框)→ 带状态切换的ToggleView(结合invalidate()触发重绘+StateListDrawable)→ 支持触摸反馈的CircleButton(onTouchEvent处理按下态+ValueAnimator渐变)→ 最终进阶至支持多点触控与物理引擎的绘图板(Path + Bezier曲线 + GestureDetector + Scroller)。这种螺旋上升的路径,使新手能逐层解构“为什么需要重写onMeasure”“为何onLayout后子View才可见”“事件为何有时被父容器吞掉”等根本性问题。综上,该资源的价值远超代码片段集合,它是Android UI内功修炼的“武功秘籍”:以数十个可运行、可调试、可拆解的实例为载体,将View绘制原理、MeasureSpec语义、ViewGroup布局算法、事件分发流向、滚动物理模型等抽象概念,具象为每一行可验证的Java代码。学习者唯有亲手修改drawColor、调整MeasureSpec计算逻辑、打断点观察MotionEvent传递路径、对比不同onInterceptTouchEvent返回值的影响,才能真正贯通Android视图体系的任督二脉——而这,正是当前市面上多数泛泛而谈的“自定义控件教程”所严重缺失的深度与实操性。
weixin_39840387
安卓Android源码——MyRatingBar.rar
Android平台中的RatingBar控件是系统原生提供的用于用户评分交互的核心UI组件之一,其本质继承自AbsSeekBar,属于Progress类控件的子类,支持以星形图标(★)为单位进行直观的五级或自定义等级评分。而本压缩包“安卓Android源码——MyRatingBar.rar”所包含的MyRatingBar项目,正是一套典型的、面向工程实践的自定义RatingBar实现方案,具有极高的学习与复用价值。该源码并非简单调用系统RatingBar,而是从View底层原理出发,完整重构了测量(onMeasure)、布局(onLayout)、绘制(onDraw)、触摸事件处理(onTouchEvent)及状态管理等核心生命周期方法,体现了Android自定义View开发的完整技术闭环。首先,在架构设计层面,MyRatingBar继承自android.view.View而非AppCompatRatingBar或RatingBar,表明其实现完全脱离系统控件依赖,具备高度可移植性与可控性。开发者通过重写onMeasure方法,动态适配不同数量的星星(如3星、5星、10星)、单颗星星尺寸(width/height)、间距(paddingBetweenStars)、是否支持半星评分等参数,支持MeasureSpec.EXACTLY、AT_MOST与UNSPECIFIED三种模式下的精准尺寸计算;在onLayout中不涉及子View排列(因其为纯绘制型控件),但需精确计算每颗星的绘制坐标原点,尤其在支持RTL(Right-to-Left)布局时需结合View.getLayoutDirection()做镜像适配。onDraw方法是其核心逻辑所在:采用Canvas.drawBitmap或Canvas.drawRect+Paint Shader方式逐星渲染,区分正常态(normal)、按下态(pressed)、只读禁用态(disabled)及半星过渡态(half),并引入PorterDuffXfermode实现遮罩式半星裁剪(如左半星填充、右半星留空),确保视觉精度达像素级。更进一步,源码中往往封装了StarDrawable抽象类或StateListDrawable资源映射机制,使图标支持.9.png、矢量SVG或动态着色(ColorFilter),极大提升UI一致性与主题适配能力。在交互逻辑上,MyRatingBar深度定制了onTouchEvent响应链:不仅捕获ACTION_DOWN、ACTION_MOVE、ACTION_UP事件,还通过MotionEvent.getX()与单星宽度比值精确判定当前滑动落入第几颗星区域,并支持“拖拽连续评分”(即手指横向滑动时实时更新评分值,而非仅点击跳变),该功能需结合VelocityTracker计算滑动速率、设置防抖阈值(如mTouchSlop)、维护mLastTouchedStar索引及插值动画中间态。同时,为兼容无障碍服务(AccessibilityService),必须重写onInitializeAccessibilityEvent与onPopulateAccessibilityEvent方法,向TalkBack等读屏软件准确播报当前评分、最大星级、是否可操作等语义信息;为支持键盘导航(TV端或辅助设备),还需覆写onKeyDown/onKeyUp并实现焦点移动逻辑。数据模型方面,MyRatingBar通常定义float类型mRating成员变量(突破整数限制,支持4.3分等精细评分),配套提供setRating(float rating, boolean isFromUser)、getRating()、setMax(int max)、setStepSize(float step)等API,并通过PropertyAnimator或ValueAnimator驱动评分变化过程中的平滑过渡动画(如星星逐个点亮/熄灭)。更重要的是,它必然实现Parcelable接口以支持跨Activity/Fragment的序列化传递,并通过自定义AttributeSet解析attrs.xml中声明的自定义属性(如app:starCount="5"、app:stepSize="0.5"、app:isIndicator="false"),使得XML布局中可直接以形式声明使用,真正达成“声明即使用”的工程友好性。此外,该源码往往集成完整的测试验证体系:包含Instrumented Test验证真实设备上的触摸行为、Robolectric Unit Test校验逻辑分支覆盖、以及Lottie或AnimatedVectorDrawable实现的加载态/反馈态动效增强。在性能优化层面,会采用Bitmap复用(BitmapFactory.Options.inMutable)、避免onDraw中创建对象(防止GC抖动)、使用static final常量替代重复计算、启用硬件加速(setLayerType(LAYER_TYPE_HARDWARE, null))提升复杂绘制帧率。综上所述,MyRatingBar不仅是单一控件的实现,更是Android UI体系中View测量-布局-绘制三阶段原理、触摸事件分发机制、状态管理范式、资源抽象策略、无障碍合规要求及性能调优方法论的集大成体现,是深入理解Android SDK底层视图机制不可多得的实战范本,对构建高可用、高性能、高可访问性的现代Android应用具有深远指导意义。
易小侠
完整的安卓画板源代码 Android小画板
安卓画板应用是Android平台典型的图形交互类项目,其核心价值不仅在于实现基础的绘图功能,更在于综合运用了Android UI开发中多个关键知识点,构成了一套完整的自定义绘图技术体系。该“完整的安卓画板源代码”本质上是一个基于View层级深度定制的绘图系统,其技术架构围绕Canvas(画布)、Paint(画笔)、Path(路径)、MotionEvent(触摸事件)、GestureDetector(手势识别器)以及ColorPicker(颜色选择器)等核心组件展开,体现了Android原生绘图能力的典型实践范式。首先,Canvas与Paint是整个绘图逻辑的基石。Canvas作为Android 2D绘图的抽象接口,提供drawLine()、drawPath()、drawCircle()、drawRect()等系列方法,而Paint则封装了绘制样式:包括strokeWidth(线宽)、strokeCap(线帽类型:BUTT/SQUARE/ROUND)、strokeJoin(拐角连接方式)、color(颜色)、alpha(透明度)、style(填充/描边模式)等。在画板中,用户每一次落笔到抬笔的过程,均需通过记录MotionEvent.ACTION_DOWN → ACTION_MOVE → ACTION_UP事件序列,将连续坐标点构造成Path对象,并利用Canvas.drawPath(path, paint)完成高效渲染;为提升流畅度,通常采用双缓冲机制——在内存Bitmap上创建Canvas进行离屏绘制,再将Bitmap整体draw到View的onDraw()中,避免频繁重绘导致的闪烁与卡顿。其次,自定义View是本项目的灵魂所在。开发者需继承View类并重写onDraw(Canvas)、onTouchEvent(MotionEvent)、onMeasure(int, int)等关键方法。其中onDraw()负责最终呈现,必须严格遵循“先清空再绘制”的原则(如调用canvas.drawColor(Color.WHITE)或canvas.drawBitmap(mBitmap, 0, 0, mPaint)),而onTouchEvent()则承担手势解析职责:通过判断ACTION_DOWN获取起始点,ACTION_MOVE持续追加坐标至Path,ACTION_UP触发一次完整笔画提交并重置Path。更高级实现还会引入Undo/Redo栈,将每次笔画封装为DrawableCommand对象存入ArrayList,配合invalidate()触发局部刷新。第三,手势识别模块远超简单触控响应。除基础拖拽外,画板常集成缩放(ScaleGestureDetector)、旋转(RotationGestureDetector)、平移(ViewPager式惯性滑动)及双击重置画布等交互。例如,通过重写SimpleOnGestureListener的onDoubleTap()可实现快速清屏;利用onScroll()配合Matrix对底层Bitmap做位移变换,支持大画布漫游;结合VelocityTracker分析滑动速率,实现自然阻尼效果。这些均要求开发者深入理解Android事件分发机制(dispatchTouchEvent → onInterceptTouchEvent → onTouchEvent)及ViewGroup嵌套场景下的事件拦截逻辑。第四,颜色选择器与笔触调节构成用户个性化配置体系。颜色选择器并非仅限于预设色块点击,高质量实现往往集成HSV色彩空间拾取器(通过SeekBar调节Hue/Saturation/Value三通道)、渐变色条、历史颜色缓存、透明度滑块(Alpha Picker)等功能;而笔触调节则涉及动态更新Paint对象属性:线宽通过SeekBar联动paint.setStrokeWidth()实时生效;笔尖类型(圆头/方头/三角形)依赖setStrokeCap()与PathEffect(如CornerPathEffect、DiscretePathEffect)组合实现艺术化效果;甚至可接入PressureSensor(压感笔支持)读取getPressure()值实现压力感应粗细变化。此外,工程层面还涵盖Bitmap内存管理(避免OOM:使用inSampleSize缩放加载、及时recycle()、LruCache缓存历史画布)、文件持久化(将Bitmap压缩为PNG/JPEG存入Internal Storage或MediaStore,支持SD卡导出)、多点触控冲突处理(filterTouchEvents防止误触)、硬件加速适配(setLayerType(LAYER_TYPE_HARDWARE, null)优化复杂Path渲染)、以及Material Design规范下的UI组件整合(如BottomSheetDialogFragment封装工具栏、FloatingActionButton快捷操作)。整个项目虽体量精巧,却浓缩了Android图形编程从底层API调用、内存安全、事件驱动模型到用户体验细节的全链路知识体系,是学习Android自定义控件开发不可多得的实战范例。
安卓自定义控件相关-androd自定义有进度的圆形进度条.zip
在Android开发中,自定义控件是构建高性能、高一致性、高可复用性UI体系的核心能力之一。本资源标题《安卓自定义控件相关—Android自定义有进度的圆形进度条》精准指向一个典型且高频使用的UI场景:实现一个支持动态进度更新、视觉美观、可高度定制的圆形进度指示器(Circular Progress Bar)。该控件并非直接调用系统ProgressBar(其默认仅提供水平条形样式),而是通过继承View类,完全重写onDraw()方法,结合Canvas、Paint、Matrix、Path等底层绘图API,手动绘制圆弧路径、背景圆环、进度弧线、中心文字(如“50%”)、阴影/渐变效果甚至带动画过渡的进度变化,从而突破系统控件的样式与行为限制。从技术实现维度看,该自定义圆形进度条涉及多个Android UI底层机制的深度协同。首先是View的生命周期与测量布局流程:需重写onMeasure()方法,正确处理MeasureSpec.EXACTLY、AT_MOST与UNSPECIFIED三种模式,根据父容器约束及自定义属性(如圆环宽度、内外半径、是否显示文本等)计算出合理的宽高尺寸,确保在ConstraintLayout、LinearLayout等不同布局中均能自适应;其次是onDraw()中的Canvas绘图逻辑——核心在于使用drawArc()或Path.addArc()绘制两个同心圆弧:一个为灰色背景圆环(表示总进度范围),另一个为彩色(如蓝色)进度弧线,其起始角度固定(通常为-90°以适配数学坐标系与屏幕坐标系差异),扫过角度由当前进度值(0~360°)线性映射决定;同时需考虑抗锯齿(setAntiAlias(true))、颜色混合(PorterDuff.Mode.SRC_OVER)、描边样式(Paint.Style.STROKE)、线帽(Paint.Cap.ROUND)等细节以提升视觉质感。进一步地,该控件必然封装了完整的状态管理机制:包括当前进度值(mProgress)、最大值(mMax,默认100)、是否启用动画(mIsAnimated)、动画持续时长(mDuration)、插值器(TimeInterpolator)等成员变量,并提供setProgress(int)方法——此方法不应直接赋值,而应触发ValueAnimator执行从旧值到新值的平滑过渡,期间不断调用invalidate()触发重绘,在onDraw()中依据动画实时插值获取当前进度并重绘,从而实现丝滑的进度增长/回退效果。此外,为支持XML属性配置(如app:circleColor="@color/blue"、app:max="200"),需在res/values/attrs.xml中声明TypedArray属性集合,并在构造函数中通过obtainStyledAttributes()解析,赋予控件良好的可配置性与工程可维护性。再深入一层,该控件还隐含对触摸交互、无障碍支持(AccessibilityNodeInfo)、深色模式适配、DP/SP单位转换、内存泄漏防护(避免在动画中持有Activity强引用)等高级实践要求。例如,若需支持用户拖动调整进度,则必须重写onTouchEvent(),将MotionEvent.ACTION_MOVE的坐标映射为角度,再反推进度值;若要兼容Material You动态色彩,应监听Configuration变化并动态更新Paint颜色;而子文件名“MyDrawCircleProgress”表明其实现类很可能采用组合式设计:内部可能封装了一个独立的CircleProgressDrawable作为可绘制对象,实现draw()与setLevel()接口,从而复用Drawable生命周期管理机制。综上,该资源不仅是一个进度条实现,更是Android自定义View开发的微型教科书——涵盖测量、布局、绘制、事件、动画、属性、主题、适配等全链路知识,是掌握Android UI底层原理、突破系统控件桎梏、打造品牌化设计语言不可或缺的实战范例。其代码虽需开发者自行调试验证,但所承载的设计思想、架构分层与API运用逻辑,具有极高的学习迁移价值与工程指导意义。
weixin_39840387
Android实例源码-自定义控件类安卓源代码(20例).zip
Android自定义控件是Android开发中极为关键且高频使用的高级技术模块,其核心价值在于突破系统原生控件(如TextView、Button、ImageView等)在UI表现力、交互逻辑、业务适配性及性能优化等方面的固有局限。本压缩包“Android实例源码-自定义控件类安卓源代码(20例)”并非简单罗列代码片段,而是一套体系化、工程化、场景驱动的实战知识库,覆盖从基础继承View/ViewGroup的轻量级定制,到深度重写onMeasure/onLayout/onDraw实现像素级渲染控制;从事件分发机制(dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent)的精细干预,到属性动画(Property Animation)、硬件加速、Canvas图层管理、SurfaceView/GLSurfaceView协同渲染等底层机制的综合运用。其中,“androd自定义有进度的圆形进度条.zip”与“ProgressWheel 支持进度显示的圆形ProgressBar.zip”代表了自定义ProgressBar的典型范式:通过继承View类,重写onDraw()方法,利用Canvas.drawArc()绘制圆弧路径,结合Paint.setShader()实现渐变色填充,配合ValueAnimator驱动进度值实时更新,并通过自定义AttributeSet解析XML中声明的自定义属性(如app:progressColor、app:backgroundColor、app:strokeWidth),实现高度可配置化;更进一步,该类通常集成invalidate()节流策略与postInvalidateDelayed()异步刷新机制,避免主线程卡顿,同时支持硬件加速兼容性判断(setLayerType(LAYER_TYPE_HARDWARE, null)),确保在不同Android版本(尤其是4.0+)上稳定渲染。而“AndroidWheel Android Wheel支持城市、多种日期时间、密码、图片.zip”与“WheelView.zip”“Android滑动选择控件WheelView,双级联动,有图有真相.rar”则集中体现了复杂手势交互控件的设计精髓:基于VelocityTracker追踪滑动速率,结合Scroller或OverScroller实现惯性滚动与回弹效果;通过onTouchEvent中对ACTION_MOVE事件的持续采样,动态计算偏移量并映射至数据索引;双级联动(如省→市)则需建立数据依赖关系模型,监听上级WheelView的滚动结束事件(OnItemSelectedListener),触发下级数据集重载与重绘,整个过程必须严格遵循View的测量-布局-绘制三阶段生命周期,并在onLayout()中精确计算每个item的Y坐标与视觉层级(Z-order)。Crouton丰富样式的Toast.zip与“五种不同的Toast效果.rar”“五种效果的Toast.rar”则颠覆了传统Toast仅限于短暂文本提示的认知边界:Crouton本质是基于WindowManager的悬浮View管理框架,它绕过Activity Context限制,可在Service、BroadcastReceiver甚至锁屏状态下安全显示;其核心是构建DecorView子视图,通过LayoutParams.type设置为TYPE_APPLICATION_OVERLAY(Android 8.0+需动态申请权限)或TYPE_SYSTEM_ALERT(旧版),并封装了自定义动画(入场/出场Alpha+Scale+Translate组合)、背景圆角Drawable、多状态图标(Success/Warning/Error)、可点击区域扩展及自动生命周期绑定(Activity销毁时自动remove),远超Toast.makeText()的简陋能力。“Android Form EditText 验证输入合法性的编辑框.zip”展示了表单验证控件的工业级实现:不仅封装正则校验、长度限制、空值检查等基础规则,更引入观察者模式(TextWatcher)实时响应输入变化,结合TextInputLayout实现Material Design风格的浮动标签(Floating Label)与错误提示动画;高级版本还集成Retrofit+LiveData实现远程唯一性校验(如用户名是否已注册),通过自定义BindingAdapter与DataBinding深度耦合,实现MVVM架构下的双向验证反馈。“android-gif-drawable 支持gif显示的view.zip”与“ImageViewEx 支持Gif显示的ImageView.zip”则直击Android原生ImageView不支持GIF动画的根本缺陷:前者基于NDK编译libgif.so,通过JNI调用libgif库逐帧解码,配合HandlerThread管理解码线程,SurfaceView渲染帧序列;后者则采用纯Java方案,继承ImageView后重写onDraw(),利用GifDecoder解析GIF元数据(LZW解压、调色板映射、帧延迟计时),通过Choreographer.postFrameCallback实现60fps精准帧同步,同时内置内存缓存(LruCache)与磁盘缓存策略,防止OOM。“HeaderLayout.zip”与“Android 自定义头部控件,简单易用.rar”代表了模块化UI组件设计思想:将Toolbar、StatusBar适配、沉浸式状态栏、多Tab导航、搜索框、返回按钮等高复用元素封装为独立ViewGroup,支持通过自定义属性(app:headerTitle、app:showBack、app:searchEnabled)声明式配置,并提供setOnTitleClickListener()、setOnSearchListener()等回调接口,彻底解耦业务逻辑与UI结构。“ViewFlipper水平滑动.rar”则深入ViewFlipper源码级改造:突破其仅支持简单View切换的限制,集成ViewPager2的PageTransformer实现景深、折叠、旋转等高级转场动画,并注入RecyclerView作为内容容器,实现无限轮播与懒加载。“不规则点击区域 三角形点击区域.zip”揭示了触摸事件坐标系转换的底层原理:重写onTouchEvent时,将MotionEvent.getRawX()/getRawY()转换为View局部坐标,再通过叉积法(Cross Product)或重心坐标法(Barycentric Coordinates)判断点是否位于预设三角形顶点构成的几何区域内,从而实现非矩形热区的精准响应。所有这些案例共同构成Android UI架构能力的完整拼图——它们不仅是代码样本,更是Android图形系统(Skia渲染引擎)、输入子系统(InputManagerService事件分发链)、窗口管理(WindowManagerService)、资源加载(AssetManager)、内存管理(Bitmap内存模型)等多维度知识的交汇点,是进阶Android架构师不可逾越的技术基石。
探索者我有我路向
安卓APP——五子棋源代码
安卓APP——五子棋源代码是一个极具教学价值与工程实践意义的Android平台单机游戏项目,它完整覆盖了Android应用开发中多个核心知识点,尤其在UI定制化、用户交互响应、图形绘制逻辑与算法设计层面具有典型代表性。首先,该项目以“自定义View”为核心技术支点,这是Android高级UI开发的关键能力之一。不同于使用系统原生控件(如TextView、ImageView)进行界面搭建,自定义View要求开发者继承View或SurfaceView类,重写onDraw()、onMeasure()、onLayout()等生命周期方法,从而实现完全可控的视觉呈现。在本五子棋项目中,开发者通过Canvas对象在onDraw()中动态绘制15×15标准棋盘网格线,并基于触摸坐标计算格点位置,进而绘制黑白交替的圆形棋子;同时,在onMeasure()中精准处理MeasureSpec,结合屏幕密度(Density)、宽高比与适配需求,完成棋盘单元格尺寸(如cellSize = Math.min(width, height) / 15)的动态计算与布局约束,确保在不同分辨率(如720p、1080p、全面屏、折叠屏)设备上均能保持比例协调、无拉伸失真,体现了Android多屏幕适配的底层原理与实战技巧。其次,用户交互逻辑高度依赖于对触摸事件的精细化处理,项目明确指出“棋子是与用户交互之后才绘制上去的”,这直接指向Android事件分发机制中的onTouchEvent()重写。开发者需在该回调中捕获MotionEvent.ACTION_DOWN事件,解析getX()/getY()获取原始触摸坐标,再通过反向映射(如xIndex = (int)((x - paddingLeft) / cellSize + 0.5f))将其转换为逻辑棋盘坐标(i,j),并校验该位置是否为空位;随后触发invalidate()请求重绘,在onDraw()中依据二维状态数组(如int[][] board,-1表黑子、1表白子、0表空)绘制对应棋子。此过程不仅涉及事件坐标系与像素坐标的转换、防误触的点击区域容错处理(如设置最小移动阈值避免滑动误判),还隐含了View事件消费机制的理解——若onTouchEvent返回true,则事件被当前View消费,后续ACTION_MOVE/ACTION_UP将持续派发至此;若返回false,则事件可能向上层ViewGroup回传,影响整体交互连贯性。更为关键的是“五连珠判断算法”的设计与实现,这是整个游戏逻辑正确性的基石。项目强调覆盖横、竖、左斜(\)、右斜(/)四种方向,其本质是二维数组上的模式匹配问题。实际实现中需以落子点(i,j)为中心,沿四个方向分别延伸搜索:例如横向需检查(i,j-4)至(i,j+4)区间内是否存在连续5个相同非零值;但更优策略是采用“中心扩展法”——从落子点出发,向正负两个方向各延伸最多4格,统计同色连续长度,四方向总和加1即为当前连珠数。该算法时间复杂度为O(1)(因最大扫描长度固定为9),空间复杂度O(1),无需额外存储结构,体现算法设计的简洁性与高效性。此外,还需处理边界越界防护(如j-4<0时截断)、提前终止优化(某方向已满5则立即返回胜利判定)、以及平局检测(board满且无五连时触发)。这些细节直指编程基本功:边界条件意识、循环控制粒度、短路逻辑运用及状态机思维。从工程架构看,该项目虽为单机轻量级应用,但仍体现出清晰的MVC或简易MVP分层思想:View层负责绘制与事件接收,Model层(如GameEngine类)封装棋盘状态、落子规则、胜负判定等纯逻辑,Controller层(Activity)协调二者交互。代码中必然包含棋盘初始化(new int[15][15])、状态持久化( onSaveInstanceState保存当前棋局应对内存回收)、以及可能的悔棋功能(维护操作栈)。技术栈上以Java语言为主,深度调用Android SDK中的Paint(设置抗锯齿、阴影、颜色)、Path(可选绘制带纹理棋子)、Matrix(缩放旋转适配)等API,同时遵循Material Design规范,在按钮、提示对话框、音效反馈等细节处体现用户体验意识。综上,该五子棋源码绝非简单玩具,而是融合Android图形系统、事件机制、算法工程、适配策略与软件工程思想的综合训练载体,对掌握移动端交互式图形应用开发具有不可替代的示范价值。
王胖胖要加油鸭
安卓app开发项目-包含多种特效的demo,实现多种动画效果,抽屉效果、多种自定义的view、还有输入法(源码).zip
该安卓App开发项目是一个高度综合性、实战导向的UI/UX开发学习资源,全面覆盖Android平台现代应用开发中多个核心且进阶的技术模块。首先,从“多种动画效果”这一关键点切入,项目深度实践了Android动画体系的三大技术栈:View Animation(补间动画)、Property Animation(属性动画)以及Transition Framework(场景转换框架)。其中,属性动画(ObjectAnimator、ValueAnimator、AnimatorSet)被广泛用于实现按钮点击涟漪反馈、卡片翻转、进度环形增长、视图缩放位移组合动效等;而Transition框架则支撑Fragment切换时的共享元素过渡(如图片缩放过渡、文本淡入淡出)、Activity场景切换的Slide/Explode/Fade效果,契合Material Design设计规范中对流畅性与状态可视化的严格要求。此外,项目还很可能整合了Lottie动画库,通过解析JSON格式的AE导出动画,实现高保真、轻量级的矢量交互动画(如加载指示器、空状态提示、操作成功徽章),极大提升用户体验的专业感与细腻度。“抽屉效果”并非仅指简单的DrawerLayout基础用法,而是体现了对Material Design导航模式的深度工程化实现。项目不仅集成NavigationView实现标准侧滑菜单,更可能包含自定义DrawerLayout子类以支持多层级嵌套抽屉、手势驱动的非线性滑动阻尼控制、抽屉开启时背景模糊/变暗/视差位移等视觉增强效果;同时结合BottomNavigationView与Navigation Component实现底部标签栏与抽屉导航的协同联动,并通过SafeArgs安全传递参数,在Fragment间实现类型安全的导航跳转与状态恢复,体现Jetpack架构组件的现代化开发范式。“多种自定义View”是本项目技术深度的重要标志。其涵盖从基础继承View重写onDraw()实现Canvas绘图(如自定义圆形进度条、波浪加载动画、手写签名板),到继承ViewGroup进行复杂布局逻辑封装(如流式标签云FlowLayout、瀑布流StaggeredGridLayout、可拖拽排序的DragSortListView),再到利用RenderScript或OpenGL ES实现高性能图形渲染(如实时滤镜预览、3D翻转卡片)。特别值得注意的是,项目中自定义View很可能遵循Android 12+推荐的Compose混合开发路径,即在传统View体系中嵌入ComposeView,实现声明式UI与命令式UI的无缝桥接,兼顾性能与开发效率。所有自定义控件均严格遵循MeasureSpec处理、onMeasure()精准测量、onLayout()合理排布、onTouchEvent()手势识别(支持多点触控、惯性滑动、长按拖拽)、状态保存与恢复(onSaveInstanceState/onRestoreInstanceState),并提供丰富自定义属性(attrs.xml定义TypedArray读取)、主题适配(支持light/dark mode)、无障碍支持(setAccessibilityDelegate)等工业级质量保障。“输入法”模块远超简单调用InputMethodManager.showSoftInput()的初级用法,而是系统性解决Android软键盘全生命周期管理难题:包括Activity启动时自动弹出/隐藏键盘策略配置(android:windowSoftInputMode)、EditText焦点与键盘联动逻辑(requestFocus() + showSoftInput()防失效)、键盘遮挡输入框的适配方案(adjustResize/adjustPan动态响应)、键盘高度动态监听(通过GlobalLayoutListener监听DecorView全局布局变化反推键盘高度)、沉浸式状态栏下键盘与状态栏的协调避让、第三方输入法兼容性测试(如讯飞、搜狗的特殊行为处理)、软键盘收起后焦点残留问题修复、以及自定义软键盘(如数字密码键盘、带功能键的富文本编辑键盘)的完整实现链路——从KeyboardView继承、Key类建模、OnKeyboardActionListener事件分发,到按键音效、长按重复、Shift/CapsLock状态同步等细节打磨。综上,该项目绝非零散Demo拼凑,而是一套贯穿Android UI开发全栈能力的知识图谱:从底层Canvas绘图原理、硬件加速渲染机制(LayerType、RenderThread)、View绘制流程(measure→layout→draw三阶段及Choreographer垂直同步)、事件分发机制(dispatchTouchEvent→onInterceptTouchEvent→onTouchEvent三级拦截),到上层架构设计(MVVM+LiveData+ViewModel状态驱动UI)、性能优化(过度绘制检测、内存泄漏排查、GPU渲染分析)、无障碍适配、国际化支持、深色主题适配、大屏折叠屏适配(Configuration变更处理)等企业级开发必备素养。每一个子模块均具备可复用、可扩展、可测试的工程化特征,是深入理解Android UI内核机制、构建高质量商业级应用不可或缺的实战范本。
职场程序猿
安卓IOS风格相关-仿iOS的分段单选按钮效果Android版.rar
在移动应用开发领域,UI组件的一致性与平台适配性始终是开发者关注的核心议题之一。本资源标题“安卓IOS风格相关-仿iOS的分段单选按钮效果Android版”精准指向一个典型且高频的跨平台UI适配需求:在Android平台上复现iOS系统原生的Segmented Control(分段控件)视觉与交互行为。该控件在iOS中被称为UISegmentedControl,是一种水平排列、互斥选择的多选项容器,常用于切换视图模式(如“列表/网格”、“今日/本周/本月”)、筛选维度(如“全部/进行中/已完成”)或导航层级(如Tab式轻量切换),其核心特征包括圆角矩形分割、平滑过渡动画、高亮状态反馈、禁用态灰度处理、以及严格单选逻辑(即同一时间仅允许一个segment处于选中状态)。而Android原生SDK并未提供功能对等的标准控件——虽有RadioGroup+RadioButton组合可实现逻辑单选,但其默认样式为垂直/水平堆叠的圆形按钮,缺乏iOS分段控件特有的无缝拼接边框、统一背景色块、文字居中对齐、点击缩放反馈及左右边缘圆角一体化设计;Material Design规范中的ToggleButtonGroup虽支持多按钮单选,但默认样式仍偏向卡片化阴影与内边距,视觉语言与iOS存在显著差异。因此,“仿iOS分段单选按钮”的实现本质上是一次深度自定义View(Custom View)工程实践。开发者需继承ViewGroup(如LinearLayout或FrameLayout)或直接继承View类,通过重写onMeasure()、onLayout()、onDraw()三大核心生命周期方法,精确控制子元素尺寸测量逻辑、布局坐标分配策略与Canvas绘制流程。具体而言:首先,在构造函数中解析自定义属性(如segmentCount、defaultSelectedIndex、textColor、selectedColor、unselectedColor、cornerRadius、dividerWidth、animationDuration等),借助TypedArray从attrs.xml中提取配置;其次,在onMeasure中根据父容器约束与内部文本宽度动态计算每个segment的宽度(支持等分或按文字长度自适应),并设定整体控件高度;再次,在onLayout中遍历所有TextView或自定义SegmentItem子视图,调用child.layout()逐个定位,确保相邻segment无缝衔接、无像素间隙;最关键的是onDraw环节——需使用Paint对象绘制统一背景色块(含左右圆角Path)、分隔线(可能为1px透明灰竖线)、文字(调用canvas.drawText()并配合TextPaint实现居中对齐与抗锯齿),同时结合StateListDrawable或LayerDrawable管理不同状态(normal/pressed/selected/disabled)下的颜色与透明度变化。此外,交互逻辑必须封装完整的事件分发机制:覆盖onTouchEvent(),识别ACTION_DOWN位置以确定点击segment索引,触发performClick()并回调自定义OnSegmentClickListener;为提升体验,还需集成属性动画(ValueAnimator)实现选中项平滑位移(通过translationX模拟iOS式的滑块跟随效果)或背景色渐变过渡;更高级实现甚至引入MotionLayout或TransitionManager实现复杂转场。该资源所附Java代码必然涉及大量Android UI底层机制:包括但不限于MeasureSpec的UNSPECIFIED/EXACTLY/AT_MOST三种模式解析、Canvas的save()/restore()坐标系保护、Path的addRoundRect()构建圆角背景、RectF的复用避免GC压力、TypedArray的recycle()内存优化、以及与Activity/Fragment生命周期联动的状态持久化(如onSaveInstanceState()保存当前选中索引)。值得注意的是,“部分代码功能进行参考学习”这一描述暗示其实现可能未完全遵循现代Android最佳实践——例如未采用ViewBinding替代findViewById、未使用Kotlin协程处理异步动画、未集成Jetpack Compose兼容层、亦或未适配深色模式(Dark Theme)下的动态色彩资源引用。然而,其价值恰恰在于揭示传统自定义View的完整技术栈:从XML属性定义(res/values/attrs.xml中声明declare-styleable)、到自定义绘制算法、再到触摸事件拦截与状态同步,构成一条贯穿Android图形渲染管线(Skia引擎)、输入事件分发(InputManagerService→ViewRootImpl→View)、以及UI线程更新(Choreographer帧同步)的完整知识链路。对于理解Android UI架构本质、掌握跨平台视觉还原方法论、以及构建企业级可复用UI组件库而言,此类仿iOS分段控件项目堪称不可多得的实战范本,其技术深度远超表面样式模仿,实为Android图形编程能力的综合试金石。
weixin_39841882