【安卓开发】自定义View

一、自定义View基础概念

1. 为什么要自定义View

  • 实现特殊UI效果

  • 封装复杂交互逻辑

  • 提高View的复用性

  • 优化性能(针对特定场景)

2. 自定义View的三种主要方式

  1. 继承现有View(如Button、TextView等)

  2. 继承ViewGroup(实现特殊布局)

  3. 直接继承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);
}

五、性能优化建议

  1. 避免在onDraw()中创建对象:如Paint、Path等应在初始化时创建并复用

  2. 使用clipRect()减少绘制区域:只绘制可见部分

  3. 合理使用invalidate():避免不必要的重绘

  4. 考虑使用Canvas的快速绘制方法:如drawBitmap()替代drawPath()

  5. 对于复杂静态内容:可以考虑使用缓存Bitmap

  6. 使用View的缓存机制:如setDrawingCacheEnabled(true)

  7. 减少过度绘制:通过Android开发者选项中的"显示过度绘制"检查

六、自定义View的常见问题

  1. wrap_content不生效:需要在onMeasure()中正确处理AT_MOST模式

  2. padding不生效:需要在onDraw()和onMeasure()中考虑padding值

  3. 触摸事件处理不灵敏:检查onTouchEvent()返回值,确保正确消费事件

  4. 内存泄漏:避免在自定义View中持有Activity的引用

  5. XML属性不生效:检查attrs.xml定义和解析代码是否正确

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值