You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为可滑动ImageView实现类似三星来电按钮的边框动画

问题描述

我需要实现一个可滑动按钮,动画效果要和三星原生手机来电时的按钮动画一致——就是围绕按钮的圆形向外扩散并逐渐消失的提示动画。目前我给ImageView加了圆形背景drawable,然后做X/Y轴缩放动画,但整个ImageView都被缩放了,我只想让周围的圆形动,按钮本身保持不动,圆形背景的方案可以调整。

当前代码如下:

布局文件

<de.utils.customViews.SwipableImageView
    android:id="@+id/accept_call_button"
    android:layout_width="@dimen/accept_hangup_call_button"
    android:layout_height="@dimen/accept_hangup_call_button"
    android:layout_margin="@dimen/activity_vertical_margin"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    android:contentDescription="@string/accept_call"
    android:src="@drawable/ic_accept_call"
    android:background="@drawable/swipe_circle" />

自定义ImageView类

public class SwipableImageView extends androidx.appcompat.widget.AppCompatImageView {

    public SwipableImageView(@NonNull Context context) {
        super(context);
        init();
    }

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

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

    public void init() {
        Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.swipable);
        startAnimation(animation);
    }
}

动画文件 res/anim/swipable.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="false">
    <scale
        android:duration="1000"
        android:fromXScale="1.0"
        android:fromYScale="1.0"
        android:toXScale="2.0"
        android:toYScale="2.0"
        android:repeatCount="infinite"
        android:interpolator="@android:anim/linear_interpolator" />
</set>
解决方案

方法1:用LayerDrawable分离图标和动画背景

把按钮图标和圆形背景拆成LayerDrawable,只对背景层单独做动画:

  1. 创建LayerDrawable文件 res/drawable/button_with_animated_bg.xml:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 底层:可动画的圆形背景 -->
    <item android:id="@+id/animated_circle_layer">
        <shape android:shape="oval">
            <solid android:color="#4000ff00" /> <!-- 半透明绿色,匹配三星来电按钮风格 -->
        </shape>
    </item>
    <!-- 上层:按钮图标 -->
    <item android:drawable="@drawable/ic_accept_call" />
</layer-list>
  1. 修改布局,移除ImageView的src属性,将LayerDrawable设为背景:
<de.utils.customViews.SwipableImageView
    android:id="@+id/accept_call_button"
    android:layout_width="@dimen/accept_hangup_call_button"
    android:layout_height="@dimen/accept_hangup_call_button"
    android:layout_margin="@dimen/activity_vertical_margin"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    android:contentDescription="@string/accept_call"
    android:background="@drawable/button_with_animated_bg" />
  1. 修改自定义ImageView类,仅对背景层执行动画:
public class SwipableImageView extends androidx.appcompat.widget.AppCompatImageView {

    public SwipableImageView(@NonNull Context context) {
        super(context);
        init();
    }

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

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

    public void init() {
        Drawable background = getBackground();
        if (background instanceof LayerDrawable) {
            LayerDrawable layerDrawable = (LayerDrawable) background;
            Drawable circleLayer = layerDrawable.findDrawableByLayerId(R.id.animated_circle_layer);
            
            // 组合缩放+透明度动画
            AnimationSet animationSet = new AnimationSet(true);
            
            ScaleAnimation scaleAnim = new ScaleAnimation(1f, 2f, 1f, 2f,
                    Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
            scaleAnim.setDuration(1000);
            scaleAnim.setRepeatCount(Animation.INFINITE);
            scaleAnim.setInterpolator(new LinearInterpolator());
            
            AlphaAnimation alphaAnim = new AlphaAnimation(1f, 0f);
            alphaAnim.setDuration(1000);
            alphaAnim.setRepeatCount(Animation.INFINITE);
            alphaAnim.setInterpolator(new LinearInterpolator());
            
            animationSet.addAnimation(scaleAnim);
            animationSet.addAnimation(alphaAnim);
            
            circleLayer.setAnimation(animationSet);
            animationSet.start();
        }
    }
}

方法2:添加独立View作为动画背景

在布局中给ImageView套一个父容器,在ImageView下方放一个专门用于动画的View,按钮本身保持静止:

  1. 修改布局结构:
<FrameLayout
    android:layout_width="@dimen/accept_hangup_call_button"
    android:layout_height="@dimen/accept_hangup_call_button"
    android:layout_margin="@dimen/activity_vertical_margin"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent">

    <!-- 仅用于动画的背景View -->
    <View
        android:id="@+id/animated_circle_bg"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/swipe_circle"
        android:layout_gravity="center" />

    <!-- 核心按钮ImageView -->
    <ImageView
        android:id="@+id/accept_call_button"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:contentDescription="@string/accept_call"
        android:src="@drawable/ic_accept_call" />
</FrameLayout>
  1. 在Activity/Fragment中启动背景View的动画:
View animatedBg = findViewById(R.id.animated_circle_bg);
Animation animation = AnimationUtils.loadAnimation(this, R.anim.swipable_with_alpha);
animatedBg.startAnimation(animation);
  1. 更新动画文件 res/anim/swipable_with_alpha.xml,增加透明度渐变:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="false">
    <scale
        android:duration="1000"
        android:fromXScale="1.0"
        android:fromYScale="1.0"
        android:toXScale="2.0"
        android:toYScale="2.0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:repeatCount="infinite"
        android:interpolator="@android:anim/linear_interpolator" />
    <alpha
        android:duration="1000"
        android:fromAlpha="1.0"
        android:toAlpha="0.0"
        android:repeatCount="infinite"
        android:interpolator="@android:anim/linear_interpolator" />
</set>

方法3:自定义Drawable实现扩散动画

如果需要更灵活的控制逻辑,可以自定义Drawable绘制循环扩散的圆形:

public class PulseDrawable extends Drawable {
    private Paint mPaint;
    private float mRadius;
    private float mMaxRadius;
    private ValueAnimator mAnimator;

    public PulseDrawable(int color) {
        mPaint = new Paint();
        mPaint.setColor(color);
        mPaint.setStyle(Paint.Style.FILL);
        
        // 初始化扩散动画
        mAnimator = ValueAnimator.ofFloat(0f, 1f);
        mAnimator.setDuration(1000);
        mAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mAnimator.addUpdateListener(animation -> {
            float fraction = (float) animation.getAnimatedValue();
            mRadius = mMaxRadius * fraction;
            invalidateSelf();
        });
        mAnimator.start();
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        // 最大半径取控件宽高中最小值的一半
        mMaxRadius = Math.min(bounds.width(), bounds.height()) / 2f;
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        Rect bounds = getBounds();
        float centerX = bounds.centerX();
        float centerY = bounds.centerY();
        // 随半径增大降低透明度
        mPaint.setAlpha((int) (255 * (1 - mRadius / mMaxRadius)));
        canvas.drawCircle(centerX, centerY, mRadius, mPaint);
    }

    @Override
    public void setAlpha(int alpha) {}

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {}

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    // 停止动画用
    public void stop() {
        mAnimator.cancel();
    }
}

然后在自定义ImageView的init方法中设置该Drawable为背景:

public void init() {
    PulseDrawable pulseDrawable = new PulseDrawable(ContextCompat.getColor(getContext(), R.color.call_accept_color));
    setBackground(pulseDrawable);
}

内容的提问来源于stack exchange,提问作者Sarah Multitasker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 09:08:16