🪑 自定义 View:木匠的神奇家具工坊 🪑

168 阅读4分钟

从前有个名叫小木的木匠,他生活在一个充满魔法的森林里。小木有一个神奇的工坊,他可以根据居民们的需求,打造出各种会说话、会跳舞、甚至会变魔术的家具。让我们来看看他是如何制作这些神奇家具的吧!

🌳 第一幕:准备木材 (继承 View 类)

小木首先需要选择合适的木材来制作家具:

java

// 神奇衣柜 - 继承自View类
public class MagicWardrobeView extends View {
    
    // 构造函数:用于在Java代码中创建视图
    public MagicWardrobeView(Context context) {
        super(context);
        init();
    }
    
    // 构造函数:用于在XML布局中使用
    public MagicWardrobeView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }
    
    // 初始化方法
    private void init() {
        // 准备基本材料
    }
    
    private void init(AttributeSet attrs) {
        // 从XML属性中获取配置信息
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MagicWardrobeView);
        try {
            // 获取自定义属性值
            int color = a.getColor(R.styleable.MagicWardrobeView_doorColor, Color.BLACK);
            boolean hasMirror = a.getBoolean(R.styleable.MagicWardrobeView_hasMirror, false);
        } finally {
            a.recycle(); // 必须回收TypedArray
        }
    }
}

🪚 第二幕:测量木材 (onMeasure 方法)

小木需要测量木材的尺寸,确保家具能放在合适的位置:

java

@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 = resolveSize(desiredWidth, widthMeasureSpec);
    int finalHeight = resolveSize(desiredHeight, heightMeasureSpec);
    
    // 设置测量结果
    setMeasuredDimension(finalWidth, finalHeight);
}

private int calculateDesiredWidth() {
    // 计算衣柜的理想宽度(例如:基于内部物品的宽度)
    return 300; // 默认300像素
}

🪓 第三幕:切割木材 (onLayout 方法)

如果衣柜里有多个抽屉,小木需要精确安排每个抽屉的位置:

java

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    // 如果这是一个包含子视图的ViewGroup,需要在这里布局子视图
    // 例如:安排衣柜里的抽屉、隔板等
    
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        
        // 计算子视图的位置
        int childLeft = ...;
        int childTop = ...;
        int childRight = childLeft + child.getMeasuredWidth();
        int childBottom = childTop + child.getMeasuredHeight();
        
        // 布局子视图
        child.layout(childLeft, childTop, childRight, childBottom);
    }
}

🎨 第四幕:绘制家具 (onDraw 方法)

小木开始精心绘制衣柜的外观:

java

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    // 绘制衣柜主体
    Paint paint = new Paint();
    paint.setColor(Color.BROWN);
    paint.setStyle(Paint.Style.FILL);
    canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
    
    // 绘制衣柜门
    paint.setColor(mDoorColor); // 从属性中获取的门颜色
    float doorWidth = getWidth() * 0.45f;
    canvas.drawRect(getWidth() * 0.05f, 0, 
                   getWidth() * 0.05f + doorWidth, getHeight(), paint);
    canvas.drawRect(getWidth() * 0.5f, 0, 
                   getWidth() * 0.5f + doorWidth, getHeight(), paint);
    
    // 绘制门把手
    paint.setColor(Color.GOLD);
    canvas.drawCircle(getWidth() * 0.25f, getHeight() * 0.5f, 10, paint);
    canvas.drawCircle(getWidth() * 0.75f, getHeight() * 0.5f, 10, paint);
    
    // 如果有镜子,绘制镜子
    if (mHasMirror) {
        paint.setColor(Color.GRAY);
        canvas.drawRect(getWidth() * 0.1f, getHeight() * 0.1f, 
                       getWidth() * 0.9f, getHeight() * 0.4f, paint);
    }
}

🧙 第五幕:添加魔法 (处理交互)

小木给衣柜添加了一个神奇的功能:点击时会播放音乐:

java

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 手指按下
            mIsPressed = true;
            invalidate(); // 重绘视图,更新状态
            return true;
            
        case MotionEvent.ACTION_UP:
            // 手指抬起
            mIsPressed = false;
            if (isInsideDoor(event.getX(), event.getY())) {
                // 触发魔法:播放音乐
                playMagicMusic();
            }
            invalidate(); // 重绘视图,更新状态
            return true;
    }
    
    return super.onTouchEvent(event);
}

private boolean isInsideDoor(float x, float y) {
    // 判断触摸点是否在门的区域内
    return (x > getWidth() * 0.05f && x < getWidth() * 0.5f - doorWidth/2) ||
           (x > getWidth() * 0.5f && x < getWidth() * 0.95f);
}

📦 第六幕:打包家具 (自定义属性)

小木为了让居民们能定制衣柜,定义了一些可配置的属性:

xml

<!-- res/values/attrs.xml -->
<resources>
    <declare-styleable name="MagicWardrobeView">
        <attr name="doorColor" format="color" />
        <attr name="hasMirror" format="boolean" />
        <attr name="drawerCount" format="integer" />
    </declare-styleable>
</resources>

在布局文件中使用这些属性:

xml

<!-- layout/activity_main.xml -->
<com.example.magicfurniture.MagicWardrobeView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:doorColor="#8B4513"
    app:hasMirror="true"
    app:drawerCount="3" />

🪄 第七幕:高级魔法 (动画与状态管理)

小木还为衣柜添加了一个平滑开门的动画效果:

java

private void openDoor() {
    // 创建属性动画
    ValueAnimator animator = ValueAnimator.ofFloat(mDoorAngle, 90f);
    animator.setDuration(500);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mDoorAngle = (float) animation.getAnimatedValue();
            invalidate(); // 每次角度变化都重绘视图
        }
    });
    animator.start();
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    // ... 之前的绘制代码 ...
    
    // 应用门的旋转效果
    canvas.save();
    canvas.rotate(mDoorAngle, getWidth() * 0.05f, 0);
    canvas.drawRect(getWidth() * 0.05f, 0, 
                   getWidth() * 0.05f + doorWidth, getHeight(), paint);
    canvas.restore();
    
    canvas.save();
    canvas.rotate(-mDoorAngle, getWidth() * 0.95f, 0);
    canvas.drawRect(getWidth() * 0.5f, 0, 
                   getWidth() * 0.5f + doorWidth, getHeight(), paint);
    canvas.restore();
}

🧠 关键概念解析

  1. View 的三大核心方法

    • onMeasure():测量视图大小,确定自己的宽高
    • onLayout():布局子视图(如果是 ViewGroup)
    • onDraw():绘制视图内容
  2. 事件处理

    • 通过onTouchEvent()方法处理用户触摸事件
    • 可以实现点击、滑动等各种交互效果
  3. 自定义属性

    • attrs.xml中定义属性
    • 在构造函数中通过TypedArray获取属性值
  4. 动画效果

    • 使用ValueAnimatorObjectAnimator创建平滑动画
    • 通过invalidate()触发重绘

🌟 总结:自定义 View 的魔法

自定义 View 就像木匠打造神奇家具的过程:先选择木材 (继承 View),再测量尺寸 (onMeasure),然后切割布局 (onLayout),接着绘制外观 (onDraw),最后添加魔法 (交互和动画)。通过理解这些步骤,你也能创造出各种炫酷的自定义视图!