一、自定义View基础概念
1. 为什么要自定义View
-
实现特殊UI效果
-
封装复杂交互逻辑
-
提高View的复用性
-
优化性能(针对特定场景)
2. 自定义View的三种主要方式
-
继承现有View(如Button、TextView等)
-
继承ViewGroup(实现特殊布局)
-
直接继承View(完全自定义)
3. 核心方法
方法 | 调用时机 | 作用 |
---|---|---|
onMeasure() | 测量View大小 | 确定View的宽高 |
onLayout() | 布局子View | 对ViewGroup需要布局子View |
onDraw() | 绘制View内容 | 实际绘制View的视觉内容 |
onTouchEvent() | 处理触摸事件 | 实现交互逻辑 |
二、自定义View实现步骤
1. 继承View基类
public class CustomView extends View {
// 在代码中new时调用
public CustomView(Context context) {
super(context);
init();
}
// 在XML布局中使用时调用
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
parseAttributes(context, attrs); // 解析自定义属性
}
// 带样式时调用
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
parseAttributes(context, attrs);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
parseAttributes(context, attrs);
}
private void init() {
// 初始化画笔等对象
}
}
2. 处理自定义属性
(1) 定义属性文件
在res/values/目录下创建attrs.xml文件:
<resources>
<declare-styleable name="CustomView">
<attr name="customColor" format="color|reference" />
<attr name="customText" format="string|reference" />
<attr name="customSize" format="dimension|reference" />
<attr name="customEnable" format="boolean" />
</declare-styleable>
</resources>
(2) 在自定义View中解析属性
private void parseAttributes(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
try {
mCustomColor = typedArray.getColor(R.styleable.CustomView_customColor, Color.BLACK);
mCustomText = typedArray.getString(R.styleable.CustomView_customText);
mCustomSize = typedArray.getDimension(R.styleable.CustomView_customSize, 16);
mCustomEnable = typedArray.getBoolean(R.styleable.CustomView_customEnable, true);
} finally {
typedArray.recycle(); // 必须回收
}
}
(3) 在XML中使用自定义属性
<com.example.myapp.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:customColor="@color/red"
app:customText="Hello Custom View"
app:customSize="20sp"
app:customEnable="true" />
3. 实现onMeasure()方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取测量模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 计算期望大小
int desiredWidth = calculateDesiredWidth();
int desiredHeight = calculateDesiredHeight();
// 处理宽度
int finalWidth;
if (widthMode == MeasureSpec.EXACTLY) {
finalWidth = widthSize; // 父View指定了确切大小
} else if (widthMode == MeasureSpec.AT_MOST) {
finalWidth = Math.min(desiredWidth, widthSize); // 不能超过父View给定大小
} else {
finalWidth = desiredWidth; // UNSPECIFIED,使用期望大小
}
// 处理高度(同理)
int finalHeight;
if (heightMode == MeasureSpec.EXACTLY) {
finalHeight = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
finalHeight = Math.min(desiredHeight, heightSize);
} else {
finalHeight = desiredHeight;
}
// 设置最终测量结果
setMeasuredDimension(finalWidth, finalHeight);
}
private int calculateDesiredWidth() {
// 根据内容计算宽度
return (int) (mText.length() * mTextSize + getPaddingLeft() + getPaddingRight());
}
private int calculateDesiredHeight() {
// 根据内容计算高度
return (int) (mTextSize + getPaddingTop() + getPaddingBottom());
}
4. 实现onDraw()方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 1. 绘制背景
drawBackground(canvas);
// 2. 绘制文本
drawText(canvas);
// 3. 绘制其他自定义内容
drawCustomContent(canvas);
}
private void drawBackground(Canvas canvas) {
Paint bgPaint = new Paint();
bgPaint.setColor(mBgColor);
bgPaint.setStyle(Paint.Style.FILL);
bgPaint.setAntiAlias(true);
// 绘制圆角矩形背景
RectF rect = new RectF(0, 0, getWidth(), getHeight());
canvas.drawRoundRect(rect, mCornerRadius, mCornerRadius, bgPaint);
}
private void drawText(Canvas canvas) {
Paint textPaint = new Paint();
textPaint.setColor(mTextColor);
textPaint.setTextSize(mTextSize);
textPaint.setAntiAlias(true);
textPaint.setTextAlign(Paint.Align.CENTER);
// 计算基线位置(使文本垂直居中)
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float baseline = (getHeight() - fontMetrics.bottom - fontMetrics.top) / 2;
// 绘制文本
canvas.drawText(mText, getWidth() / 2, baseline, textPaint);
}
private void drawCustomContent(Canvas canvas) {
// 绘制其他自定义图形等
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
// 示例:绘制对角线
canvas.drawLine(0, 0, getWidth(), getHeight(), paint);
canvas.drawLine(getWidth(), 0, 0, getHeight(), paint);
}
5. 处理触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 手指按下
handleActionDown(event);
return true;
case MotionEvent.ACTION_MOVE:
// 手指移动
handleActionMove(event);
invalidate(); // 重绘View
return true;
case MotionEvent.ACTION_UP:
// 手指抬起
handleActionUp(event);
performClick(); // 确保点击事件被触发
return true;
case MotionEvent.ACTION_CANCEL:
// 事件取消
handleActionCancel(event);
return true;
}
return super.onTouchEvent(event);
}
// 处理点击事件(兼容无障碍服务)
@Override
public boolean performClick() {
super.performClick();
// 处理点击逻辑
if (mClickListener != null) {
mClickListener.onClick(this);
}
return true;
}
// 设置点击监听
public void setOnCustomClickListener(OnClickListener listener) {
mClickListener = listener;
}
三、自定义ViewGroup
1. 基本实现
public class CustomLayout extends ViewGroup {
public CustomLayout(Context context) {
super(context);
}
// 必须实现的构造方法...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 测量所有子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
// 计算总大小
int width = 0;
int height = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
width = Math.max(width, child.getMeasuredWidth());
height += child.getMeasuredHeight();
}
// 考虑padding
width += getPaddingLeft() + getPaddingRight();
height += getPaddingTop() + getPaddingBottom();
// 考虑父View的限制
width = resolveSize(width, widthMeasureSpec);
height = resolveSize(height, heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int currentTop = getPaddingTop();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 水平居中布局
int childLeft = (getWidth() - childWidth) / 2;
child.layout(
childLeft,
currentTop,
childLeft + childWidth,
currentTop + childHeight
);
currentTop += childHeight;
}
}
}
2. 更复杂的自定义LayoutManager示例
public class FlowLayout extends ViewGroup {
private List<Line> mLines = new ArrayList<>();
private Line mCurrentLine;
private int mHorizontalSpacing = 10;
private int mVerticalSpacing = 10;
// 构造方法...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mLines.clear();
mCurrentLine = new Line();
int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int maxWidth = 0;
// 测量所有子View
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
if (mCurrentLine.getViewCount() == 0) {
// 行中没有View,直接添加
mCurrentLine.addView(child);
} else if (mCurrentLine.getWidth() + mHorizontalSpacing + child.getMeasuredWidth() <= width) {
// 行中已有View,判断是否能继续添加
mCurrentLine.addView(child);
} else {
// 换行
mLines.add(mCurrentLine);
maxWidth = Math.max(maxWidth, mCurrentLine.getWidth());
mCurrentLine = new Line();
mCurrentLine.addView(child);
}
}
// 添加最后一行
if (mCurrentLine.getViewCount() > 0) {
mLines.add(mCurrentLine);
maxWidth = Math.max(maxWidth, mCurrentLine.getWidth());
}
// 计算总高度
int height = getPaddingTop() + getPaddingBottom();
for (int i = 0; i < mLines.size(); i++) {
height += mLines.get(i).getHeight();
}
height += (mLines.size() - 1) * mVerticalSpacing;
// 设置最终测量结果
setMeasuredDimension(
resolveSize(maxWidth + getPaddingLeft() + getPaddingRight(), widthMeasureSpec),
resolveSize(height, heightMeasureSpec)
);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = getPaddingLeft();
int top = getPaddingTop();
for (int i = 0; i < mLines.size(); i++) {
Line line = mLines.get(i);
line.layout(left, top);
top += line.getHeight() + mVerticalSpacing;
}
}
private class Line {
private List<View> mViews = new ArrayList<>();
private int mWidth;
private int mHeight;
public void addView(View view) {
if (mViews.size() > 0) {
mWidth += mHorizontalSpacing;
}
mWidth += view.getMeasuredWidth();
mHeight = Math.max(mHeight, view.getMeasuredHeight());
mViews.add(view);
}
public void layout(int left, int top) {
int currentLeft = left;
// 计算剩余空间,用于水平居中
int extraWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - mWidth;
int space = extraWidth / (mViews.size() + 1);
currentLeft += space;
for (View view : mViews) {
int viewWidth = view.getMeasuredWidth();
int viewHeight = view.getMeasuredHeight();
// 垂直居中
int viewTop = top + (mHeight - viewHeight) / 2;
view.layout(
currentLeft,
viewTop,
currentLeft + viewWidth,
viewTop + viewHeight
);
currentLeft += viewWidth + mHorizontalSpacing + space;
}
}
// getter方法...
}
}
四、自定义View高级技巧
1. 使用Canvas绘制复杂图形
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 1. 绘制渐变背景
Paint bgPaint = new Paint();
LinearGradient gradient = new LinearGradient(
0, 0, getWidth(), getHeight(),
Color.BLUE, Color.GREEN, Shader.TileMode.CLAMP
);
bgPaint.setShader(gradient);
canvas.drawRect(0, 0, getWidth(), getHeight(), bgPaint);
// 2. 绘制路径
Paint pathPaint = new Paint();
pathPaint.setColor(Color.RED);
pathPaint.setStyle(Paint.Style.STROKE);
pathPaint.setStrokeWidth(5);
pathPaint.setAntiAlias(true);
Path path = new Path();
path.moveTo(0, getHeight()/2);
path.cubicTo(
getWidth()/4, getHeight(),
getWidth()*3/4, 0,
getWidth(), getHeight()/2
);
canvas.drawPath(path, pathPaint);
// 3. 绘制位图
if (mBitmap != null) {
Rect src = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
Rect dst = new Rect(50, 50, getWidth()-50, getHeight()-50);
canvas.drawBitmap(mBitmap, src, dst, null);
}
// 4. 绘制文本路径
Paint textPaint = new Paint();
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(50);
textPaint.setAntiAlias(true);
Path circlePath = new Path();
circlePath.addCircle(getWidth()/2, getHeight()/2, 150, Path.Direction.CW);
canvas.drawTextOnPath("这是一个圆形文本路径", circlePath, 0, 0, textPaint);
}
2. 使用PathEffect实现特殊效果
// 在onDraw方法中添加
Paint effectPaint = new Paint();
effectPaint.setColor(Color.YELLOW);
effectPaint.setStyle(Paint.Style.STROKE);
effectPaint.setStrokeWidth(10);
effectPaint.setAntiAlias(true);
// 虚线效果
effectPaint.setPathEffect(new DashPathEffect(new float[]{20, 10}, 0));
canvas.drawCircle(getWidth()/2, getHeight()/2, 200, effectPaint);
// 离散点效果
effectPaint.setPathEffect(new DiscretePathEffect(10, 5));
canvas.drawLine(100, 100, getWidth()-100, getHeight()-100, effectPaint);
// 组合效果
effectPaint.setPathEffect(new ComposePathEffect(
new CornerPathEffect(30),
new DashPathEffect(new float[]{20, 10}, 0)
));
Path path = new Path();
path.moveTo(100, 300);
path.lineTo(300, 500);
path.lineTo(500, 300);
path.lineTo(700, 500);
canvas.drawPath(path, effectPaint);
3. 实现View的保存和恢复状态
// 定义状态常量
private static final String STATE_SUPER = "superState";
private static final String STATE_CUSTOM = "customState";
// 保存状态
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable(STATE_SUPER, super.onSaveInstanceState());
bundle.putInt(STATE_CUSTOM, mCustomState);
return bundle;
}
// 恢复状态
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER));
mCustomState = bundle.getInt(STATE_CUSTOM);
} else {
super.onRestoreInstanceState(state);
}
}
4. 使用硬件加速优化性能
// 在构造方法中
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
setLayerType(LAYER_TYPE_HARDWARE, null);
}
// 或者在需要时动态设置
public void enableHardwareAcceleration(boolean enable) {
setLayerType(enable ? LAYER_TYPE_HARDWARE : LAYER_TYPE_SOFTWARE, null);
}
五、性能优化建议
-
避免在onDraw()中创建对象:如Paint、Path等应在初始化时创建并复用
-
使用clipRect()减少绘制区域:只绘制可见部分
-
合理使用invalidate():避免不必要的重绘
-
考虑使用Canvas的快速绘制方法:如drawBitmap()替代drawPath()
-
对于复杂静态内容:可以考虑使用缓存Bitmap
-
使用View的缓存机制:如setDrawingCacheEnabled(true)
-
减少过度绘制:通过Android开发者选项中的"显示过度绘制"检查
六、自定义View的常见问题
-
wrap_content不生效:需要在onMeasure()中正确处理AT_MOST模式
-
padding不生效:需要在onDraw()和onMeasure()中考虑padding值
-
触摸事件处理不灵敏:检查onTouchEvent()返回值,确保正确消费事件
-
内存泄漏:避免在自定义View中持有Activity的引用
-
XML属性不生效:检查attrs.xml定义和解析代码是否正确