Android开发—动画中遇到的坑:圆角和背景不能同时绘制

105 阅读4分钟

文章中标出ERROR的地方就是问题点 解决方案:自定义使用自定义View的裁剪+绘制实现这个效果

MainActivity代码

import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private Button animatedButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        animatedButton = findViewById(R.id.animatedButton);
        final GradientDrawable buttonBackground = new GradientDrawable();
        buttonBackground.setColor(0xFFFFFFFF); // 初始颜色为白色
        buttonBackground.setCornerRadius(65f); // 初始圆角度数为65
        animatedButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startAnimation();
            }
        });
    }

    private void startAnimation() {
        // Scale animation
        ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1.0f, 0.5f, 1.0f);
        scaleAnimator.setDuration(1000);
        scaleAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        scaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float scale = (float) animation.getAnimatedValue();
                animatedButton.setScaleX(scale);
                animatedButton.setScaleY(scale);
            }
        });

        // Color animation
        ValueAnimator colorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), 0xFFFFFFFF, 0xFFFF0000);
        colorAnimator.setDuration(1000);
        colorAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        colorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
                int animatedValue = (int) animator.getAnimatedValue();
                animatedButton.setBackgroundColor(animatedValue);
            }
        });

        // Corner radius animation
        ValueAnimator cornerAnimator = ValueAnimator.ofFloat(65f, 10f);
        cornerAnimator.setDuration(1000);
        cornerAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        // 初始化背景为GradientDrawable,以便于动态改变圆角
        GradientDrawable drawable = new GradientDrawable();
        cornerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float radius = (float) animation.getAnimatedValue();
                drawable.setShape(GradientDrawable.RECTANGLE);
                drawable.setCornerRadius(radius);
                animatedButton.setBackground(drawable);
            }
        });

        // Start animations simultaneously
        scaleAnimator.start();
        colorAnimator.start();//ERROR:设置了背景,角度不生效。
        cornerAnimator.start();//ERROR:设置了角度,背景不生效
    }
}

xml代码 :ERROE:如果这里设置了圆角角度,同时动态代码中设置了背景色,会导致两个绘制都失效!!! android:background="@drawable/button_background"

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">
    <Button
        android:id="@+id/animatedButton"
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:text="Animate me!"
        android:textColor="#000" />
</RelativeLayout>

drawable代码

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#FFFFFF"/>
    <corners android:radius="60dp"/>
</shape>

解决方案: 自定义使用自定义View的裁剪+绘制实现这个效果

自定义View

AnimatedButtonView
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

import androidx.annotation.Nullable;

public class AnimatedButtonView extends View {

    private Paint paint;
    private int currentColor;
    private float currentRadius;
    private float currentScale;

    private static final int START_COLOR = 0xFF00FFFF;
    private static final int END_COLOR = 0xFFFFFF00;
    private static final float START_RADIUS = 85f;
    private static final float END_RADIUS = 30f;
    private static final long ANIMATION_DURATION = 1000;
    private RectF rectF;
    private Path path;

    public AnimatedButtonView(Context context) {
        super(context);
        init();
    }

    public AnimatedButtonView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public AnimatedButtonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        rectF = new RectF();
        path = new Path();
        currentColor = START_COLOR;
        currentRadius = START_RADIUS;
        currentScale = 1.0f;

        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                startAnimation();
            }
        });
    }

    private void startAnimation() {
        // Color animation
        ValueAnimator colorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), START_COLOR, END_COLOR);
        colorAnimator.setDuration(ANIMATION_DURATION);
        colorAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        colorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentColor = (int) animation.getAnimatedValue();
                invalidate();
            }
        });

        // Radius animation
        ValueAnimator radiusAnimator = ValueAnimator.ofFloat(START_RADIUS, END_RADIUS);
        radiusAnimator.setDuration(ANIMATION_DURATION);
        radiusAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        radiusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentRadius = (float) animation.getAnimatedValue();
                invalidate();
            }
        });

        // Scale animation
        ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1.0f, 0.5f, 1.0f);
        scaleAnimator.setDuration(ANIMATION_DURATION);
        scaleAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        scaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentScale = (float) animation.getAnimatedValue();
                invalidate();
            }
        });

        // Start all animations
        colorAnimator.start();
        radiusAnimator.start();
        scaleAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Determine the scaled size
        float width = getWidth() * currentScale;
        float height = getHeight() * currentScale;

        // Check for division by zero
        int viewWidth = getWidth();
        int viewHeight = getHeight();
        if (viewWidth == 0 || viewHeight == 0) {
            return; // Avoid division by zero
        }

        // Calculate the dimensions for the rectangle to draw
        float left = (viewWidth - width) / 2;
        float top = (viewHeight - height) / 2;
        float right = left + width;
        float bottom = top + height;

        // Set the dimensions of the RectF
        rectF.set(left, top, right, bottom);

        // Ensure paint is initialized
        if (paint == null) {
            throw new NullPointerException("Paint object is not initialized");
        }

        paint.setColor(currentColor);
        path.reset(); // Reset the path before adding new geometry
        path.addRoundRect(rectF, currentRadius, currentRadius, Path.Direction.CW);
        canvas.drawPath(path, paint);
    }
}

xml代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">

    <com.example.myandroid.AnimatedButtonView
        android:layout_width="200dp"
        android:layout_height="100dp"/>
</RelativeLayout>

然后再MainActivity中加载出来就行

AnimatedButtonView优化自定义控件

import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

import androidx.annotation.Nullable;

/**
 * 优化的问题点:
 * 减少内存泄漏风险 使用局部变量而非匿名内部类来定义动画更新监听器,并确保在动画结束后释放这些监听器的引用。
 * 提高线程安全性 考虑将动画更新逻辑封装成单独的方法,并通过适当的调度机制减少不必要的重绘操作。
 * 加强初始化检查 在init()方法中增加对paint对象的初始化检查,并在其他相关方法中添加必要的异常处理逻辑。
 * 完善异常处理 在关键方法中增加日志记录或异常抛出,以便于调试和维护。
 * 主要优化点:
 * 匿名内部类替换为局部类:避免内存泄漏,监听器实现为独立类。
 * 线程安全性:triggerRedraw方法使用postInvalidate确保从主线程调用绘制操作。
 * 初始化检查:init方法中确保paint正确初始化,并抛出异常。
 * 异常处理:使用Log.e记录重要的异常信息,以协助调试。
 */
public class AnimatedButtonView extends View {

    private Paint paint;
    private int currentColor;
    private float currentRadius;
    private float currentScale;

    private static final int START_COLOR = 0xFF00FFFF;
    private static final int END_COLOR = 0xFFFFFF00;
    private static final float START_RADIUS = 85f;
    private static final float END_RADIUS = 30f;
    private static final long ANIMATION_DURATION = 1000;
    private final RectF rectF;
    private final Path path;

    public AnimatedButtonView(Context context) {
        this(context, null);
    }

    public AnimatedButtonView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public AnimatedButtonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        rectF = new RectF();
        path = new Path();

        try {
            init();
        } catch (Exception e) {
            Log.e("AnimatedButtonView", "Initialization failed: " + e.getMessage());
        }
    }

    private void init() {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        if (paint == null) {
            throw new IllegalStateException("Failed to initialize paint");
        }
        currentColor = START_COLOR;
        currentRadius = START_RADIUS;
        currentScale = 1.0f;

        setOnClickListener(v -> startAnimations());
    }

    private void startAnimations() {
        ValueAnimator colorAnimator = createColorAnimator();
        ValueAnimator radiusAnimator = createRadiusAnimator();
        ValueAnimator scaleAnimator = createScaleAnimator();

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(ANIMATION_DURATION);
        animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
        animatorSet.playTogether(colorAnimator, radiusAnimator, scaleAnimator);
        animatorSet.start();
    }

    private ValueAnimator createColorAnimator() {
        ValueAnimator animator = ValueAnimator.ofObject(new ArgbEvaluator(), START_COLOR, END_COLOR);
        animator.addUpdateListener(new ColorUpdateListener());
        return animator;
    }

    private class ColorUpdateListener implements ValueAnimator.AnimatorUpdateListener {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            currentColor = (int) animation.getAnimatedValue();
            triggerRedraw();
        }
    }

    private ValueAnimator createRadiusAnimator() {
        ValueAnimator animator = ValueAnimator.ofFloat(START_RADIUS, END_RADIUS);
        animator.addUpdateListener(new RadiusUpdateListener());
        return animator;
    }

    private class RadiusUpdateListener implements ValueAnimator.AnimatorUpdateListener {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            currentRadius = (float) animation.getAnimatedValue();
            triggerRedraw();
        }
    }

    private ValueAnimator createScaleAnimator() {
        ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0.5f, 1.0f);
        animator.addUpdateListener(new ScaleUpdateListener());
        return animator;
    }

    private class ScaleUpdateListener implements ValueAnimator.AnimatorUpdateListener {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            currentScale = (float) animation.getAnimatedValue();
            triggerRedraw();
        }
    }

    private void triggerRedraw() {
        postInvalidate(); // Ensure invalidate is called on the UI thread
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        float width = getWidth() * currentScale;
        float height = getHeight() * currentScale;
        float left = (getWidth() - width) / 2;
        float top = (getHeight() - height) / 2;
        float right = left + width;
        float bottom = top + height;

        rectF.set(left, top, right, bottom);

        try {
            paint.setColor(currentColor);
            path.reset();
            path.addRoundRect(rectF, currentRadius, currentRadius, Path.Direction.CW);
            canvas.drawPath(path, paint);
        } catch (Exception e) {
            Log.e("AnimatedButtonView", "Drawing failed: " + e.getMessage());
        }
    }
}