全栈工程师Android学习知识Android 自定义view

Android自定义View之继承扩展(仿网易云音乐听歌识曲)

2017-07-10  本文已影响1402人  brucevanfdm

前言

上篇文章说到了自定义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,并提出宝贵意见。

上一篇下一篇

猜你喜欢

热点阅读