如何为可滑动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,只对背景层单独做动画:
- 创建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>
- 修改布局,移除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" />
- 修改自定义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,按钮本身保持静止:
- 修改布局结构:
<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>
- 在Activity/Fragment中启动背景View的动画:
View animatedBg = findViewById(R.id.animated_circle_bg); Animation animation = AnimationUtils.loadAnimation(this, R.anim.swipable_with_alpha); animatedBg.startAnimation(animation);
- 更新动画文件
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
相关产品推荐
相关产品推荐

