Android自定义View之继承扩展(仿网易云音乐听歌识曲)
前言
上篇文章说到了自定义View的组合实战,链接:Android自定义View之组合实战(以支付宝页面为例) ,感兴趣的同学可以看看。今天要分享的是一个模仿网易云音乐听歌识曲界面的自定义View,实现了一个波纹涟漪效果。
正文
废话不多说,惯例是上图,最终效果如下:
ripple.gif
分析:
首先拿到网易云音乐的听歌识曲效果,可以将其动画细分为两个,其一是缩放;其二是渐变,多个圆环同一时间执行动画集合,构成了最终效果!这样一来思路就有了,那么实现还有很大难度吗?
源码配合注释:
这个自定义View分为两个类实现,自定义属性我就不重复写,可以参考上一篇文章的内容:
RippleAnimationView.java
/**
* 波纹动画效果
* Created by fdm on 2017/7/10.
*/
public class RippleAnimationView extends RelativeLayout {
private int rippleType;
private int rippleColor;
private int rippleAmount;
private float rippleScale;
private float rippleRadius;
private int rippleDuration;
public Paint paint;
public float rippleStrokeWidth;
private TypedArray typedArray;
private AnimatorSet animatorSet;
private boolean animationRunning = false;
private ArrayList<RippleCircleView> rippleViewList = new ArrayList<>();
//默认实心圆圈
private static final int DEFAULT_FILL_TYPE = 0;
//默认伸缩大小
private static final float DEFAULT_SCALE = 5.0f;
//默认圆圈个数
private static final int DEFAULT_RIPPLE_COUNT = 5;
//默认扩散时间
private static final int DEFAULT_DURATION_TIME = 2500;
public RippleAnimationView(Context context) {
super(context);
}
public RippleAnimationView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public RippleAnimationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(final Context context, final AttributeSet attrs) {
//判断View当前是否处于 IDE 布局编辑(预览)状态,只有在编辑状态下才会返回true,
//在编写只有在运行时才能看到绘制效果的自定义View时,可以使用该方法查看布局预览。
if (isInEditMode()) {
return;
}
//加载自定义属性
typedArray = context.obtainStyledAttributes(attrs, R.styleable.RippleAnimationView);
rippleType = typedArray.getInt(R.styleable.RippleAnimationView_ripple_anim_type, DEFAULT_FILL_TYPE);
rippleColor = typedArray.getColor(R.styleable.RippleAnimationView_ripple_anim_color, ContextCompat.getColor(context, R.color.rippleColor));
rippleAmount = typedArray.getInt(R.styleable.RippleAnimationView_ripple_anim_amount, DEFAULT_RIPPLE_COUNT);
rippleScale = typedArray.getFloat(R.styleable.RippleAnimationView_ripple_anim_scale, DEFAULT_SCALE);
rippleRadius = typedArray.getDimension(R.styleable.RippleAnimationView_ripple_anim_radius, getResources().getDimension(R.dimen.rippleRadius));
rippleDuration = typedArray.getInt(R.styleable.RippleAnimationView_ripple_anim_duration, DEFAULT_DURATION_TIME);
rippleStrokeWidth = typedArray.getDimension(R.styleable.RippleAnimationView_ripple_anim_strokeWidth, getResources().getDimension(R.dimen.rippleStrokeWidth));
//注意回收TypedArray
typedArray.recycle();
int rippleDelay = rippleDuration / rippleAmount;
paint = new Paint();
paint.setAntiAlias(true);//抗锯齿
if (rippleType == DEFAULT_FILL_TYPE) {
rippleStrokeWidth = 0;
paint.setStyle(Paint.Style.FILL);
} else {
paint.setStyle(Paint.Style.STROKE);
}
paint.setColor(rippleColor);
LayoutParams rippleParams = new LayoutParams((int) (2 * (rippleRadius + rippleStrokeWidth)), (int) (2 * (rippleRadius + rippleStrokeWidth)));
rippleParams.addRule(CENTER_IN_PARENT, TRUE);
//分析该动画后将其拆分为缩放、渐变
ArrayList<Animator> animatorList = new ArrayList<>();
for (int i = 0; i < rippleAmount; i++) {
RippleCircleView rippleView = new RippleCircleView(this, context);
addView(rippleView, rippleParams);
rippleViewList.add(rippleView);
//ScaleX缩放
final ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(rippleView, "ScaleX", 1.0f, rippleScale);
scaleXAnimator.setRepeatCount(ObjectAnimator.INFINITE);//无限重复
scaleXAnimator.setRepeatMode(ObjectAnimator.RESTART);
scaleXAnimator.setStartDelay(i * rippleDelay);
scaleXAnimator.setDuration(rippleDuration);
animatorList.add(scaleXAnimator);
//ScaleY缩放
final ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(rippleView, "ScaleY", 1.0f, rippleScale);
scaleYAnimator.setRepeatCount(ObjectAnimator.INFINITE);//无限重复
scaleYAnimator.setRepeatMode(ObjectAnimator.RESTART);
scaleYAnimator.setStartDelay(i * rippleDelay);
scaleYAnimator.setDuration(rippleDuration);
animatorList.add(scaleYAnimator);
//Alpha渐变
final ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(rippleView, "Alpha", 1.0f, 0f);
alphaAnimator.setRepeatCount(ObjectAnimator.INFINITE);//无限重复
alphaAnimator.setRepeatMode(ObjectAnimator.RESTART);
alphaAnimator.setStartDelay(i * rippleDelay);
alphaAnimator.setDuration(rippleDuration);
animatorList.add(alphaAnimator);
}
animatorSet = new AnimatorSet();
animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
animatorSet.playTogether(animatorList);
}
/**
* 开始动画
*/
public void startRippleAnimation() {
if (!isRippleRunning()) {
for (RippleCircleView rippleView : rippleViewList) {
rippleView.setVisibility(VISIBLE);
}
animatorSet.start();
animationRunning = true;
}
}
/**
* 停止动画
*/
public void stopRippleAnimation() {
if (isRippleRunning()) {
Collections.reverse(rippleViewList);
for (RippleCircleView rippleView : rippleViewList) {
rippleView.setVisibility(INVISIBLE);
}
animatorSet.end();
animationRunning = false;
}
}
/**
* 是否正在执行
*
* @return boolean isRippleRunning
*/
public boolean isRippleRunning() {
return animationRunning;
}
}
RippleCircleView.java
/**
* Draw Circle
* Created by fdm on 2017/7/10.
*/
public class RippleCircleView extends View {
private RippleAnimationView rippleAnimationView;
public RippleCircleView(Context context) {
super(context);
}
public RippleCircleView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RippleCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public RippleCircleView(RippleAnimationView rippleAnimationView, Context context) {
super(context);
this.rippleAnimationView = rippleAnimationView;
this.setVisibility(View.INVISIBLE);
}
@Override
protected void onDraw(Canvas canvas) {
int radius = (Math.min(getWidth(), getHeight())) / 2;
canvas.drawCircle(radius, radius, radius - rippleAnimationView.rippleStrokeWidth, rippleAnimationView.paint);
}
}
源码不难理解,继承了RelativeLayout,然后再画几个圆,执行相应动画,效果就出来了。这里值得一提的是:
if (isInEditMode()) {
return;
}
···
scaleXAnimator.setStartDelay(i * rippleDelay);
第一,isInEditMode判断View当前是否处于 IDE 布局编辑(预览)状态,只有在编辑状态下才会返回true,在编写只有在运行时才能看到绘制效果的自定义View时,可以使用该方法查看布局预览,避免IDE报错。
第二,这个setStartDelay方法延迟执行动画,在这里是实现波纹效果的重点,首先我们定义了圆的个数(默认为5),然后往里面添加了5个圆,然后将动画执行时间(duration)均分,每个圆依次分档执行setStartDelay方法,结合缩放、渐变动画,最终完成了我们期待的效果。为了方便使用,暴露几个常用方法提供调用即可!
使用:
<com.fdm.neteasyripple.RippleAnimationView xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout_RippleAnimation"
android:layout_width="360dp"
android:layout_height="360dp"
app:ripple_anim_amount="5"
app:ripple_anim_color="@color/rippleColor"
app:ripple_anim_duration="2500"
app:ripple_anim_radius="@dimen/rippleRadius"
app:ripple_anim_scale="6"
app:ripple_anim_type="fillRipple">
<ImageView
android:id="@+id/ImageView"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:src="@mipmap/music" />
</com.fdm.neteasyripple.RippleAnimationView>
总结
这次借着模仿网易云音乐听歌识曲界面实践了自定义View的继承扩展方式,还原度上还有些细节需要完善,这里主要是为了自定义View,就不再细究了,感兴趣的可以下载源码进行调试。
国际惯例:源码送上https://github.com/brucevanfdm/NetEasyRipple 欢迎star/fork,并提出宝贵意见。