Android自定义控件之扫描动画UI

前言

最近有一个需求,就是做一个扫描的UI,看了很多扫描动画,发现酷狗的扫描动画挺漂亮的,所以就做了一个相似的扫描动画,废话不多说,先看一下最终的效果吧。

最终效果图

Android自定义控件之扫描动画UI_第1张图片

介绍

首先我们看一下这个效果,它由以下几部分组成:
1.中间一个音符图片
2.在音符图片外面画了三个圆环
3.一个旋转的扇形动画
既然知道它的结构那就来一步一步实现它吧。

准备

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        viewSize = 600;
        setMeasuredDimension(viewSize, viewSize);
    }


    /**
     * 从资源中解码bitmap
     */
    private void initBitmap() {
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.music_note);
    }

在这里为了方便直接把视图的长宽设置为600,实际应用中可以自己去定制,但是长宽要一样,然后就是在构造的时候解码一下资源文件中的音符图片。然后就可以开始绘图了。

绘图

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint mPaint = new Paint();
        Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        Rect rectd = new Rect((viewSize / 10) * 4, (viewSize / 10) * 4, viewSize - (viewSize / 10) * 4, viewSize - (viewSize / 10) * 4);
        canvas.drawBitmap(bitmap, rect, rectd, mPaint);

        circlePaint = new Paint();
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setAntiAlias(true);
        circlePaint.setStrokeWidth((viewSize / 10));

        circlePaint.setColor(Color.parseColor("#DDFA7298"));
        canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 2.5f, circlePaint);

        circlePaint.setColor(Color.parseColor("#AAFA7298"));
        canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 3.5f, circlePaint);

        circlePaint.setColor(Color.parseColor("#66FA7298"));
        canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 4.5f, circlePaint);

        sectorPaint = new Paint();
        sectorPaint.setAntiAlias(true);
        sectorPaint.setStyle(Paint.Style.STROKE);
        sectorPaint.setStrokeWidth((viewSize / 10) * 3);
        Shader sectorShader = new SweepGradient(viewSize / 2, viewSize / 2, new int[]{Color.TRANSPARENT, 0x00FA7298, 0xFFFA7298},
                new float[]{0, 0.875f, 1f});
        sectorPaint.setShader(sectorShader);

        if (threadFlag) {
            canvas.concat(matrix);
            canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 3.5f, sectorPaint);
        }
    }

这一段有点长,一个一个看,第一部分是画图片

Paint mPaint = new Paint();
        Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        Rect rectd = new Rect((viewSize / 10) * 4, (viewSize / 10) * 4, viewSize - (viewSize / 10) * 4, viewSize - (viewSize / 10) * 4);
        canvas.drawBitmap(bitmap, rect, rectd, mPaint);


在这里我们的目的是把整张图画进整张图的最中心,这里使用的是drawBitmap(Bitmap,Rect,Rect,Paint);这个方法。画这张图片需要创建两个Rect,第一个Rect 代表要绘制的bitmap 区域,第二个 Rect 代表的是要将bitmap 绘制在屏幕的什么地方。所以这里第一个Rect我们 取值为整个Bitmap 区域 ,第二个Rect我们取值为view的五等分圆环由内向外第二圆环的外接正方形状,说的这么绕,大家仔细看下应该不难,也就是下图中的蓝色矩形。

Android自定义控件之扫描动画UI_第2张图片



        circlePaint = new Paint();
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setAntiAlias(true);
        circlePaint.setStrokeWidth((viewSize / 10));

        circlePaint.setColor(Color.parseColor("#DDFA7298"));
        canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 2.5f, circlePaint);

        circlePaint.setColor(Color.parseColor("#AAFA7298"));
        canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 3.5f, circlePaint);

        circlePaint.setColor(Color.parseColor("#66FA7298"));
        canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 4.5f, circlePaint);

这一部分就是画外面的三个圆环了,在这里,我们给画笔设置描边不填充的属性,然后再设置画笔的宽度则可以实现圆环的绘制了,半径的取值我相信大家稍微看下也能看懂。

        sectorPaint = new Paint();
        sectorPaint.setAntiAlias(true);
        sectorPaint.setStyle(Paint.Style.STROKE);
        sectorPaint.setStrokeWidth((viewSize / 10) * 3);
        Shader sectorShader = new SweepGradient(viewSize / 2, viewSize / 2, new int[]{Color.TRANSPARENT, 0x00FA7298, 0xFFFA7298},
                new float[]{0, 0.875f, 1f});
        sectorPaint.setShader(sectorShader);

接下来的这部分用到了SweepGradient扫描梯度渲染,这里简单的介绍一下:

public SweepGradient(float cx, float cy, int colors[], float positions[])
public SweepGradient(float cx, float cy, int color0, int color1)

第一个方法 cx 、cy指圆型坐标; colors[]、渲染颜色数组,至少要两个,positions[]设置相对位置的渲染,如果为null则均匀梯度渲染
第二个方法 cx 、cy指圆型坐标;color0、 color1分别指起始渲染颜色和终止渲染颜色。

我们这里使用的是第一种方法,从代码可以看出我们只渲染了1/8的扇形区域。

    if (threadFlag) {
        canvas.concat(matrix);
        canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 3.5f, sectorPaint);
    }

这部分代码的功能就是只有在线程开启时才会绘制扇形渲染。同时根据在矩阵matrix中改变角度持续绘制达到扇形渲染扫描的效果。

动画扫描线程

class ScanThread extends Thread {

        private ScanView view;

        public ScanThread(ScanView view) {
            this.view = view;
        }

        @Override
        public void run() {
            super.run();
            while (threadFlag) {
                if (start) {
                    view.post(new Runnable() {
                        @Override
                        public void run() {
                            angle = angle + 5;
                            if (angle == 360) {
                                angle = 0;
                            }
                            matrix = new Matrix();
                            matrix.setRotate(angle, viewSize / 2, viewSize / 2);
                            view.invalidate();
                        }
                    });
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

这里通过在线程中不断对角度进行改变重绘才有了最终的效果


完整代码

public class ScanView extends View {

    private static final String TAG = "ScanView";

    /**
     * 画圆的笔
     */
    private Paint circlePaint;

    /**
     * 画扇形渲染的笔
     */
    private Paint sectorPaint;

    /**
     * 扫描线程
     */
    private ScanThread mThread;

    /**
     * 线程进行标志
     */
    private boolean threadFlag = false;

    /**
     * 线程启动标志
     */
    private boolean start = false;

    /**
     * 扇形转动的角度
     */
    private int angle = 0;

    /**
     * 当前视图的宽高,这里相同
     */
    private int viewSize;

    /**
     * 画在view中间的图片
     */
    private Bitmap bitmap;

    /**
     * 对图形进行处理的矩阵类
     */
    private Matrix matrix;

    public ScanView(Context context) {
        this(context, null);
    }

    public ScanView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initBitmap();
    }

    /**
     * 此处设置viewSize固定值为600
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        viewSize = 600;
        setMeasuredDimension(viewSize, viewSize);
    }


    /**
     * 从资源中解码bitmap
     */
    private void initBitmap() {
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.music_note);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint mPaint = new Paint();
        Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        Rect rectd = new Rect((viewSize / 10) * 4, (viewSize / 10) * 4, viewSize - (viewSize / 10) * 4, viewSize - (viewSize / 10) * 4);
        canvas.drawBitmap(bitmap, rect, rectd, mPaint);

        circlePaint = new Paint();
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setAntiAlias(true);
        circlePaint.setStrokeWidth((viewSize / 10));

        circlePaint.setColor(Color.parseColor("#DDFA7298"));
        canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 2.5f, circlePaint);

        circlePaint.setColor(Color.parseColor("#AAFA7298"));
        canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 3.5f, circlePaint);

        circlePaint.setColor(Color.parseColor("#66FA7298"));
        canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 4.5f, circlePaint);

        sectorPaint = new Paint();
        sectorPaint.setAntiAlias(true);
        sectorPaint.setStyle(Paint.Style.STROKE);
        sectorPaint.setStrokeWidth((viewSize / 10) * 3);
        Shader sectorShader = new SweepGradient(viewSize / 2, viewSize / 2, new int[]{Color.TRANSPARENT, 0x00FA7298, 0xFFFA7298},
                new float[]{0, 0.875f, 1f});
        sectorPaint.setShader(sectorShader);


        if (threadFlag) {
            canvas.concat(matrix);
            canvas.drawCircle(viewSize / 2, viewSize / 2, (viewSize / 10) * 3.5f, sectorPaint);
        }
    }


    public void start() {
        mThread = new ScanThread(this);
        mThread.start();
        threadFlag = true;
        start = true;
    }

    public void stop() {
        if (start) {
            threadFlag = false;
            start = false;
            invalidate();
        }
    }


    class ScanThread extends Thread {

        private ScanView view;

        public ScanThread(ScanView view) {
            this.view = view;
        }

        @Override
        public void run() {
            super.run();
            while (threadFlag) {
                if (start) {
                    view.post(new Runnable() {
                        @Override
                        public void run() {
                            angle = angle + 5;
                            if (angle == 360) {
                                angle = 0;
                            }
                            matrix = new Matrix();
                            matrix.setRotate(angle, viewSize / 2, viewSize / 2);
                            view.invalidate();
                        }
                    });
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

}


终于实现了,再贴一下效果图

Android自定义控件之扫描动画UI_第3张图片

完整的代码demo

https://github.com/lijunyandev/ScanDemo


结语

这是我写的第一篇博客,前前后后花了几天晚上的时间,所幸今天终于出来了,虽然不难,但是也付出了努力,还是很激动的。希望以后有空能够持续写下去。

你可能感兴趣的:(Android自定义控件,android,ui,控件,动画)