从前有个名叫小木的木匠,他生活在一个充满魔法的森林里。小木有一个神奇的工坊,他可以根据居民们的需求,打造出各种会说话、会跳舞、甚至会变魔术的家具。让我们来看看他是如何制作这些神奇家具的吧!
🌳 第一幕:准备木材 (继承 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();
}
🧠 关键概念解析
-
View 的三大核心方法
onMeasure():测量视图大小,确定自己的宽高onLayout():布局子视图(如果是 ViewGroup)onDraw():绘制视图内容
-
事件处理
- 通过
onTouchEvent()方法处理用户触摸事件 - 可以实现点击、滑动等各种交互效果
- 通过
-
自定义属性
- 在
attrs.xml中定义属性 - 在构造函数中通过
TypedArray获取属性值
- 在
-
动画效果
- 使用
ValueAnimator或ObjectAnimator创建平滑动画 - 通过
invalidate()触发重绘
- 使用
🌟 总结:自定义 View 的魔法
自定义 View 就像木匠打造神奇家具的过程:先选择木材 (继承 View),再测量尺寸 (onMeasure),然后切割布局 (onLayout),接着绘制外观 (onDraw),最后添加魔法 (交互和动画)。通过理解这些步骤,你也能创造出各种炫酷的自定义视图!