Android 自定义雷达扫描动画

362 阅读4分钟

一、前言

雷达扫描动画是比较常见的动画,其绘制难点不在于旋转,而在于着色和收放处理,本篇会进行一次实践。

二、实现

利用不同的Animator去进行组合,我们这里定义了3种不同的Animator,并且通过不同的组合形式实现。另外我们这里实现了图标的圆形布局,这个和我们之前的转盘菜单类似,只不过这次使用的ViewGroup,通过onLayout布局而不是通过onDraw绘制。我们需要对Layout的测量和布局进行学习

2.1 测量

我们这里测量,目的是保证每个icon大小一致,防止有的大有的小的情况,毕竟在一些情况下,Drawable会影响到View的默认尺寸。

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {


        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));
        widthMeasureSpec = heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
                View.MeasureSpec.EXACTLY);
        //正方形布局
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);


        int width = getMeasuredWidth();
        int height = getMeasuredHeight();


        outS = width / 2;  // 188/125 = width/S   =>    S = width/(188/125) = (125*width)/188
        centerS = (int) (((125 * width) / 188f) / 2);
        innerS = (int) (((62 * width) / 188f) / 2);
        pointInner = (int) (((10 * width) / 188f) / 2);
        centerX = width / 2;
        centerY = height / 2;


        int count = getChildCount();
        if (count <= 1) {
            return;
        }
       // 计算ItemView大小,保证View分布咋圆上
        int childMaxWidth = (int) ((outS - innerS) * 1f / 2);
        if (childMaxWidth <= 0) {
            childMaxWidth = (int) (dp2px(5) * 2);
        }

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            final int childWidthMeasureSpec = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(childMaxWidth, MeasureSpec.EXACTLY),
                    getPaddingLeft() + getPaddingRight() +
                            lp.leftMargin + lp.rightMargin,
                    lp.width);

            final int childHeightMeasureSpec = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(childMaxWidth, MeasureSpec.EXACTLY),
                    getPaddingTop() + getPaddingBottom() +
                            lp.topMargin + lp.bottomMargin,
                    lp.height);

            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }

2.2 布局

这里我们主要实现的逻辑是保证每个View都能分布到圆上,我们计算出View按45度角的区域分配空间。

  @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();

        if (width == 0 || height == 0) {
            return;
        }

        int centerX = width / 2;
        int centerY = height / 2;

        float outlineLength = (float) (Math.cos(Math.toRadians(45)) * width / 2);

        int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {
            ImageView child = (ImageView) getChildAt(i);

            float perAngle = 360f / childCount;
            float angle = (i % childCount) * perAngle;

            float r = outlineLength * 9 / 10;

            float dx = (float) Math.sin(Math.toRadians(angle)) * r;
            float dy = (float) Math.cos(Math.toRadians(angle)) * r;

            float center_x = centerX + dx;
            float center_y = centerY + dy;

            IconItem iconItem = new IconItem(child, angle);
            mIcons.add(iconItem);

            child.layout((int) (center_x - child.getMeasuredWidth() / 2), (int) (center_y - child.getMeasuredHeight() / 2), (int) (center_x + child.getMeasuredWidth() / 2), (int) (center_y + child.getMeasuredHeight() / 2));
        }

    }

2.3 动画

本篇我们使用了大量的属性动画,属性动画是按序列执行的,当然,这里一定要处理好动画的顺序问题。

2.4 完整逻辑

下面是完整代码,这里我们最多只允许6个icon展示,因此一定要清楚的是,子View的数量不能超过6个,当然,你也可以扩展更多的icon数量。

public class RadarScanAppViewLayout extends FrameLayout {

    private int outS;
    private int centerS;
    private int innerS;
    private int pointInner;
    private int centerX;
    private int centerY;

    private float pointerAngle = 0;

    private boolean isInit = false;

    private int MAX_ICON = 6;

    private Paint mCirclePaint;
    private Paint mDotPaint;
    private Paint mPointerPaint;
    private Paint mPointBitmapPaint;

    private Bitmap mBitmapPointer;
    private Rect mRectPointer;

    private float mCircle0Scale = 1;
    private float mCircle1Scale = 1;
    private float mCircle2Scale = 1;
    private float mCircle3Scale = 1;

    private float mPointerLenRatio = 0;

    private float mPointerAngle = 45;

    private float mDismissFraction;

    private boolean mbPointerLooperEnd;

    private boolean isFirstLoadAppIcon = true;
    private ArrayList<IconItem> mIcons = new ArrayList<IconItem>();

    private ArrayList<Drawable> mAllIconDrawable = new ArrayList<Drawable>();
    private int fromIndex = 0;
    private int rotateRoundCount = 0;

    private class IconItem {
        private final float cur_angle;
        private boolean hide;
        private Drawable icon;
        private long startDismissT = 0;
        private ImageView iconView;

        public IconItem(ImageView iconView, float angle) {
            this.iconView = iconView;
            this.cur_angle = angle;
        }

        private boolean isInit() {
            return this.icon != null;
        }

        public void setIcon(Drawable iconDrawable) {
            this.icon = iconDrawable;
            this.iconView.setImageDrawable(icon);
        }
    }

    public RadarScanAppViewLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    public RadarScanAppViewLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

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


    private void init(Context context) {

        setWillNotDraw(false);

        mCirclePaint = new Paint();
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setColor(0xffFFEAC4);
        mCirclePaint.setStyle(Paint.Style.FILL);

        mDotPaint = new Paint();
        mDotPaint.setAntiAlias(true);
        mDotPaint.setColor(0xffFFEAC4);
        mDotPaint.setStyle(Paint.Style.FILL);

        mPointerPaint = new Paint();
        mPointerPaint.setAntiAlias(true);
        mPointerPaint.setColor(0xffFFEAC4);
        mPointerPaint.setStrokeWidth(dp2px(1.5f));
        mPointerPaint.setStyle(Paint.Style.FILL);

        mBitmapPointer = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon_radar_pointer);
        mPointBitmapPaint = new Paint();
        mPointBitmapPaint.setAntiAlias(true);

        for (int i = 0; i < MAX_ICON; i++) {
            ImageView imageView = createImageView();
            if (imageView != null) {
                addView(imageView);
            }
        }
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!isInit) {
            return;
        }
        mRectPointer = new Rect(centerX - outS, centerY - outS, centerX + outS, centerY + outS);
        drawCircle(canvas);
        updateIcons();

    }


    private float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (!isInit) {
            return;
        }

        drawPointer(canvas);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();

        if (width == 0 || height == 0) {
            return;
        }

        int centerX = width / 2;
        int centerY = height / 2;

        float outlineLength = (float) (Math.cos(Math.toRadians(45)) * width / 2);

        int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {
            ImageView child = (ImageView) getChildAt(i);

            float perAngle = 360f / childCount;
            float angle = (i % childCount) * perAngle;

            float r = outlineLength * 9 / 10;

            float dx = (float) Math.sin(Math.toRadians(angle)) * r;
            float dy = (float) Math.cos(Math.toRadians(angle)) * r;

            float center_x = centerX + dx;
            float center_y = centerY + dy;

            IconItem iconItem = new IconItem(child, angle);
            mIcons.add(iconItem);

            child.layout((int) (center_x - child.getMeasuredWidth() / 2), (int) (center_y - child.getMeasuredHeight() / 2), (int) (center_x + child.getMeasuredWidth() / 2), (int) (center_y + child.getMeasuredHeight() / 2));
        }

    }


    private ImageView createImageView() {
        ImageView imageView = new ImageView(this.getContext());
        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        imageView.setLayoutParams(lp);
        return imageView;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {


        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));
        widthMeasureSpec = heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
                View.MeasureSpec.EXACTLY);
        //正方形布局
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);


        int width = getMeasuredWidth();
        int height = getMeasuredHeight();


        outS = width / 2;  // 188/125 = width/S   =>    S = width/(188/125) = (125*width)/188
        centerS = (int) (((125 * width) / 188f) / 2);
        innerS = (int) (((62 * width) / 188f) / 2);
        pointInner = (int) (((10 * width) / 188f) / 2);
        centerX = width / 2;
        centerY = height / 2;


        int count = getChildCount();
        if (count <= 1) {
            return;
        }

        int childMaxWidth = (int) ((outS - innerS) * 1f / 2);
        if (childMaxWidth <= 0) {
            childMaxWidth = (int) (dp2px(5) * 2);
        }

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            final int childWidthMeasureSpec = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(childMaxWidth, MeasureSpec.EXACTLY),
                    getPaddingLeft() + getPaddingRight() +
                            lp.leftMargin + lp.rightMargin,
                    lp.width);

            final int childHeightMeasureSpec = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(childMaxWidth, MeasureSpec.EXACTLY),
                    getPaddingTop() + getPaddingBottom() +
                            lp.topMargin + lp.bottomMargin,
                    lp.height);

            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }

    private void updateIcons() {
        if (mIcons == null) return;
        int iconNum = mIcons.size();
        for (int i = 0; i < iconNum; i++) {
            IconItem item = mIcons.get(i);
            if (!item.isInit()) {
                continue;
            }
            if (mbPointerLooperEnd) {
                float hide_process = mDismissFraction;
                if (hide_process > 0 && hide_process <= 1f) {
                    float alpha = 0.7f * (1 - hide_process);
                    item.iconView.setAlpha((int) (alpha * 255));
                }
            }else{
                item.iconView.setAlpha((int) ( 255));
            }
        }
    }


    public void showRadarAnimation() {
        if (isInit) {
            return;
        }

        isInit = true;
        enterAnimation();

    }

    private void updateRardarIcons(List<Drawable> drawableList) {
        if (null == drawableList || drawableList.isEmpty()) {
            return;
        }

        Log.d("logView", "updateRardarIcons allSize=" + drawableList.size());
        for (int i = 0; i < drawableList.size(); i++) {
            if (MAX_ICON <= i) {
                break;
            }
            Drawable drawable = drawableList.get(i);
            if (drawable == null) {
                continue;
            }
            IconItem iconItem = null;
            if (mIcons.size() > i) {
                iconItem = mIcons.get(i);
                iconItem.setIcon(drawable);
            }

        }

    }


    private void switchRardarIcons() {
        post(new Runnable() {
            @Override
            public void run() {
                doSwitchIcons();
            }
        });
    }

    private void doSwitchIcons() {
        int size = mAllIconDrawable.size();
        Log.d("logView", "switchRardarIcons allSize=" + size + ",fromIndex=" + this.fromIndex);
        if (size <= 0) {
            return;
        }
        if (mbPointerLooperEnd) {
            Log.d("logView", "switchRardarIcons 开始结束动画,不允许切换icon");
            return;
        }
        if (this.fromIndex >= size) {
            Log.d("logView", "所有icon都已经展示了");
            return;
        }

        if ((size - this.fromIndex) < MAX_ICON) {
            List<Drawable> bmplist = new ArrayList<>();
            for (int i = size - 1; i >= 0; i--) {
                if (bmplist.size() >= MAX_ICON) {
                    break;
                }
                bmplist.add(mAllIconDrawable.get(i));
            }
            this.fromIndex = size;
            updateRardarIcons(bmplist);
        } else {
            List<Drawable> bmplist = new ArrayList<>();
            for (int i = this.fromIndex; i < (this.fromIndex + MAX_ICON); i++) {
                if (bmplist.size() >= MAX_ICON) {
                    break;
                }
                bmplist.add(mAllIconDrawable.get(i));
            }
            updateRardarIcons(bmplist);
            this.fromIndex += MAX_ICON;
        }
    }


    public void addRadarIcons(List<Drawable> drawables) {
        int sumSize = mAllIconDrawable.size();

        if (sumSize >= MAX_ICON * 6) {
            return;
        }
        Log.d("logView", "addRadarIcons size=" + sumSize + ",bitmaps.size=" + drawables.size());

        if (null != drawables && !drawables.isEmpty()) {
            for (int i = 0; i < drawables.size(); i++) {
                Drawable drawable = drawables.get(i);
                if (drawable == null) {
                    continue;
                }
                mAllIconDrawable.add(drawable);
            }
        }

        if (isFirstLoadAppIcon && mAllIconDrawable.size() > 0) {
            this.fromIndex = 0;
            isFirstLoadAppIcon = false;
            switchRardarIcons();
        }
    }

    public void recycle() {
        if (mAllIconDrawable == null) return;
        mAllIconDrawable.clear();
    }


    private void drawCircle(Canvas canvas) {
        mCirclePaint.setAlpha((int) (0.1 * 255));
        canvas.drawCircle(centerX, centerY, mCircle1Scale * outS, mCirclePaint);
        canvas.drawCircle(centerX, centerY, mCircle2Scale * centerS, mCirclePaint);
        canvas.drawCircle(centerX, centerY, mCircle3Scale * innerS, mCirclePaint);
        canvas.drawCircle(centerX, centerY, mCircle0Scale * pointInner, mDotPaint);
    }

    private void drawPointer(Canvas canvas) {
        if (pointerAngle > 0 || mPointerLenRatio > 0) {
            canvas.save();
            canvas.rotate(180 + pointerAngle, centerX, centerY);
            canvas.drawLine(centerX, centerY, centerX, (centerY + outS * mPointerLenRatio), mPointerPaint);
            canvas.restore();
            if (mPointerLenRatio > 0.3f) {
                float alpha = mPointerLenRatio;
                canvas.save();
                canvas.rotate(pointerAngle, centerX, centerY);
                mPointBitmapPaint.setAlpha((int) (alpha * 255));
                canvas.drawBitmap(mBitmapPointer, null, mRectPointer, mPointBitmapPaint);
                canvas.restore();
            }
            if (mbPointerLooperEnd) {
                mPointerAngle = pointerAngle;
                if (mPointerAngle > 360) {
                    mPointerAngle = -360 + mPointerAngle;
                }
            }
        }
    }


    private void pointerLooperControl() {
        final ValueAnimator sweep = ObjectAnimator.ofFloat(0f, 360f);
        sweep.setInterpolator(new LinearInterpolator());
        sweep.setDuration(1000);
        sweep.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                pointerAngle = (Float) valueAnimator.getAnimatedValue();
                postInvalidate();
            }
        });
        sweep.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {

            }

            @Override
            public void onAnimationEnd(Animator animator) {
                if (!mbPointerLooperEnd) {
                    pointerLooperControl();
                    notifySwitchIcon();
                } else {
                    pointerDismissControl();
                }
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {


            }
        });
        sweep.start();
    }

    private void notifySwitchIcon() {
        if (rotateRoundCount > 0 && rotateRoundCount % 3 == 0) {
            switchRardarIcons();
        }
        rotateRoundCount++;
        if (rotateRoundCount > (Integer.MAX_VALUE - 3)) {
            rotateRoundCount = 0;
        }
        Log.d("logView", "notifySwitchIcon rotateRoundCount=" + rotateRoundCount);
    }

    private void pointerShowControl() {
        final AnimatorSet set = new AnimatorSet();
        ValueAnimator step1 = ObjectAnimator.ofFloat(0.0f, 1f);
        step1.setInterpolator(new AccelerateInterpolator());
        step1.setDuration(300);
        step1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mPointerLenRatio = (Float) valueAnimator.getAnimatedValue();
                postInvalidate();
            }
        });
        set.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {

            }

            @Override
            public void onAnimationEnd(Animator animator) {
                if (mbPointerLooperEnd) {
                    pointerDismissControl();
                } else {
                    pointerLooperControl();
                }
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });
        set.playSequentially(step1);
        set.start();
    }

    private void pointerDismissControl() {
        final AnimatorSet set = new AnimatorSet();
        final ValueAnimator sweep = ObjectAnimator.ofFloat(0f, 360f);
        sweep.setInterpolator(new LinearInterpolator());
        sweep.setDuration(1000);
        sweep.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                pointerAngle = (Float) valueAnimator.getAnimatedValue();
                mDismissFraction =  valueAnimator.getAnimatedFraction();
                invalidate();
            }
        });
        ValueAnimator dismiss = ObjectAnimator.ofFloat(1f, 0f);
        dismiss.setDuration(500);
        dismiss.setInterpolator(new DecelerateInterpolator());
        dismiss.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mPointerLenRatio = (Float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        set.playSequentially(sweep, dismiss);
        set.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {

            }

            @Override
            public void onAnimationEnd(Animator animator) {
                dismissCircleControl();
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });
        set.start();
    }

    private void enterAnimation() {
        showCircleControl();
    }

    private void showCircleControl() {
        pointerShowControl();
    }

    private void dismissCircleControl() {
        long duration = 300;
        AnimatorSet set = new AnimatorSet();
        ValueAnimator circle0 = ObjectAnimator.ofFloat(1f, 0f);
        circle0.setInterpolator(new AccelerateInterpolator());
        circle0.setDuration(duration);
        circle0.setStartDelay(duration * 3 / 4);
        circle0.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mCircle0Scale = (Float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        ValueAnimator circle1 = ObjectAnimator.ofFloat(1f, 0f);
        circle1.setInterpolator(new AccelerateInterpolator());
        circle1.setDuration(duration);
        circle1.setStartDelay(duration / 2);
        circle1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mCircle1Scale = (Float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        ValueAnimator circle2 = ObjectAnimator.ofFloat(1f, 0f);
        circle2.setDuration(duration);
        circle2.setStartDelay(duration / 4);
        circle2.setInterpolator(new AccelerateInterpolator());
        circle2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mCircle2Scale = (Float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        ValueAnimator circle3 = ObjectAnimator.ofFloat(1f, 0f);
        circle3.setDuration(duration);
        circle3.setInterpolator(new DecelerateInterpolator());
        circle3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mCircle3Scale = (Float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        set.playTogether(circle0, circle1, circle2, circle3);
        set.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {

            }

            @Override
            public void onAnimationEnd(Animator animator) {
                if (mAnimationListener != null) {
                    mAnimationListener.onRadarAnimationEnd();
                }
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });
        set.start();
    }

    public IAnimListener mAnimationListener;

    public void setListener(IAnimListener listener) {
        mAnimationListener = listener;
    }

    public void setSweepEnd() {
        mbPointerLooperEnd = true;
    }


    public static interface IAnimListener {
        public void onRadarAnimationEnd();
    }

}

三、用法

具体用法也是很简单

  • 调用addRadarIcons添加drawable

  • showRadarAnimation 开始动画

  • setSweepEnd 停止动画循环

四、总结

本篇其实主要学习的是测量、布局和动画,实际上本篇作为典型案例,指定View大小的方式,实现View大小一致,然后通过圆周角进行布局,最终通过动画组合展示。