<think>我们正在处理一个自定义View(CustomView)的动画崩溃问题。用户提到使用ObjectAnimator实现圆形动画时出现FATAL EXCEPTION: main错误。虽然提供的代码中没有直接包含动画部分,但我们可以根据常见的动画实现错误来排查。
常见崩溃原因:
1. 在非UI线程更新View
2. 动画重复启动未取消
3. 属性值设置错误(如设置不存在的属性)
4. 自定义View的绘制方法(onDraw)中出现了异常
根据用户提供的CustomView代码,我们注意到:
- 在构造方法中,有一个三参构造方法调用了`mPainter.setColor(mColor)`,但是`mPainter`在调用时还未初始化(因为`init()`方法在后面才调用)。这会导致NullPointerException。
具体代码段:
```java
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray t = context.obtainStyledAttributes(attrs,R.styleable.CustomView);
int mColor = t.getColor(R.styleable.CustomView_circle_color,Color.BLUE);
t.recycle();
mPainter.setColor(mColor); // 这里mPainter是null,因为init()还没有调用
init();
}
```
解决方案:将属性解析放在init方法中,或者确保在设置mPainter之前已经初始化。
修改建议:
将属性解析移到init方法中,并在init方法中处理自定义属性。
修改后的代码:
```java
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs); // 将attrs传递给init方法
}
private void init(AttributeSet attrs) {
mPainter = new Paint();
mPainter.setStrokeWidth(5f);
mPainter.setStyle(Paint.Style.FILL);
// 如果有属性集,则解析属性
if (attrs != null) {
TypedArray t = getContext().obtainStyledAttributes(attrs, R.styleable.CustomView);
int mColor = t.getColor(R.styleable.CustomView_circle_color, Color.BLUE);
t.recycle();
mPainter.setColor(mColor);
} else {
mPainter.setColor(Color.RED);
}
}
```
同时,修改其他构造方法:
```java
public CustomView(Context context) {
super(context);
init(null);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
```
另外,注意四参构造方法(API 21+):
```java
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
// 同样需要初始化
init(attrs);
}
```
这样修改后,就不会出现NullPointerException。
关于动画部分:
如果用户使用ObjectAnimator,通常是在自定义View中定义了一个属性,然后通过ObjectAnimator改变该属性。例如,我们想改变圆的半径,可以定义一个属性radius,然后提供setRadius方法。在setRadius方法中改变半径并重绘。
在CustomView中添加:
```java
private int mRadius; // 当前半径
public void setRadius(int radius) {
mRadius = radius;
// 重绘
invalidate();
}
public int getRadius() {
return mRadius;
}
```
然后在onDraw中,使用这个mRadius来画圆:
```java
// 原来:int r = Math.min(width, height)/2;
// 改为使用mRadius,但注意:如果mRadius没有被设置过,我们使用原来的计算方式
int currentRadius = mRadius > 0 ? mRadius : Math.min(width, height)/2;
canvas.drawCircle(paddingLeft+width/2, paddingTop+height/2, currentRadius, mPainter);
```
使用ObjectAnimator:
```java
ObjectAnimator animator = ObjectAnimator.ofInt(customView, "radius", 0, 100);
animator.setDuration(1000);
animator.start();
```
注意:属性动画要求对应的属性必须有setter方法(命名规则为setXxx,且参数类型一致)和可选的getter方法(如果需要插值器工作)。
如果出现错误:“属性radius不存在”,则需要在自定义View中定义属性动画的操作对象:
```java
// 在CustomView类中添加
public static final Property<CustomView, Integer> RADIUS = new IntProperty<CustomView>("radius") {
@Override
public void setValue(CustomView object, int value) {
object.setRadius(value);
}
@Override
public Integer get(CustomView object) {
return object.getRadius();
}
};
```
然后使用:
```java
ObjectAnimator animator = ObjectAnimator.ofInt(customView, RADIUS, 0, 100);
```
或者直接使用字符串"radius",但需要确保setter和getter存在。
另外,注意动画的启动和停止时机,避免在View销毁时动画还在运行。
最后,检查onMeasure方法:
用户当前的onMeasure方法在wrap_content时设置了固定宽高,但可能没有处理其他情况(如match_parent或具体数值)。建议修改为:
```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 = 400;
int desiredHeight = 400;
int width, height;
// 宽度
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize);
} else {
width = desiredWidth;
}
// 高度
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, heightSize);
} else {
height = desiredHeight;
}
setMeasuredDimension(width, height);
}
```
这样能更正确地处理wrap_content和match_parent等情况。
总结:
1. 修复构造函数中的空指针问题(主要崩溃原因)。
2. 添加动画所需的属性(如radius)和对应的setter/getter。
3. 修改onMeasure方法以正确测量。
4. 在onDraw中使用动画改变的属性。
如果用户按照上述修改,应该能解决崩溃问题并实现动画效果。
注意:如果用户在使用动画时出现其他错误,请根据错误日志进一步分析。
下面给出完整的CustomView修改代码:</think>### Android CustomView 使用 ObjectAnimator 实现圆形动画的崩溃问题排查
#### 问题分析
根据提供的崩溃日志和代码,主要问题集中在`CustomView`的构造方法中。关键错误点:
```java
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray t = context.obtainStyledAttributes(attrs,R.styleable.CustomView);
int mColor = t.getColor(R.styleable.CustomView_circle_color,Color.BLUE);
t.recycle();
mPainter.setColor(mColor); // ❌ 此处mPainter尚未初始化
init(); // 初始化在属性设置之后调用
}
```
**崩溃原因**:在调用`mPainter.setColor()`时,`mPainter`对象还未初始化(`init()`方法在属性设置之后才执行),导致`NullPointerException`[^1]。
#### 完整修复方案
```java
public class CustomView extends View {
private Paint mPainter;
private float mRadius = 0f; // 动画属性:半径
// 修复构造方法
public CustomView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs); // 将属性集传递给初始化方法
}
private void init(AttributeSet attrs) {
mPainter = new Paint(Paint.ANTI_ALIAS_FLAG);
mPainter.setStyle(Paint.Style.FILL);
// 安全解析自定义属性
if (attrs != null) {
TypedArray t = getContext().obtainStyledAttributes(attrs, R.styleable.CustomView);
try {
int defaultColor = Color.BLUE;
int circleColor = t.getColor(R.styleable.CustomView_circle_color, defaultColor);
mPainter.setColor(circleColor);
} finally {
t.recycle(); // 确保回收资源
}
} else {
mPainter.setColor(Color.RED); // 默认颜色
}
}
// 添加动画属性setter
public void setRadius(float radius) {
mRadius = radius;
invalidate(); // 触发重绘
}
public float getRadius() {
return mRadius;
}
@Override
protected void onDraw(Canvas canvas) {
// 使用动画值mRadius替代静态计算
canvas.drawCircle(getWidth()/2f, getHeight()/2f, mRadius, mPainter);
}
// 精简onMeasure实现
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int defaultSize = 400;
int width = resolveSize(defaultSize, widthMeasureSpec);
int height = resolveSize(defaultSize, heightMeasureSpec);
setMeasuredDimension(width, height);
}
}
```
#### ObjectAnimator 正确使用示例
```java
// 在Activity/Fragment中启动动画
CustomView customView = findViewById(R.id.custom_view);
ObjectAnimator animator = ObjectAnimator.ofFloat(
customView,
"radius", // 对应setRadius方法
0f, // 起始值
200f // 结束值
);
animator.setDuration(1000);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.start();
```
#### 关键修复点说明
1. **构造方法重构**:
- 将属性解析移到`init()`方法内,确保`mPainter`先初始化再使用
- 使用`try-finally`保证`TypedArray`资源回收[^1]
2. **动画属性支持**:
```java
// 必须提供setter方法
public void setRadius(float radius) {
mRadius = radius;
invalidate(); // 强制重绘
}
```
- ObjectAnimator通过反射调用`setRadius()`方法
- `invalidate()`触发`onDraw()`更新视图
3. **XML属性安全使用**:
```xml
<com.example.CustomView
android:layout_width="200dp"
android:layout_height="200dp"
app:circle_color="@color/red" />
```
- 确保`res/values/attrs.xml`声明属性:
```xml
<declare-styleable name="CustomView">
<attr name="circle_color" format="color" />
</declare-styleable>
```
4. **测量优化**:
```java
int width = resolveSize(defaultSize, widthMeasureSpec);
int height = resolveSize(defaultSize, heightMeasureSpec);
```
- 使用系统工具方法`resolveSize()`简化wrap_content处理
#### 常见崩溃场景预防
1. **属性名称拼写错误**:
- ObjectAnimator的`propertyName`必须与setter方法名匹配(setXxx → "xxx")
2. **线程安全**:
```java
// 在UI线程执行动画
new Handler(Looper.getMainLooper()).post(() -> animator.start());
```
3. **内存泄漏预防**:
```java
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
animator.cancel(); // 及时取消动画
}
```
4. **值范围验证**:
```java
public void setRadius(float radius) {
if (radius < 0) throw new IllegalArgumentException("半径不能为负");
mRadius = radius;
invalidate();
}
```
> **提示**:使用`View#isHardwareAccelerated()`检查硬件加速状态,复杂动画在软件渲染下可能导致性能问题[^2]。
通过以上修复,可解决构造方法导致的崩溃问题,并实现平滑的圆形缩放动画效果。