美文网首页Android 自定义view自定义View安卓开发
Android自定义View(10)- 写一个雷达扫描界面

Android自定义View(10)- 写一个雷达扫描界面

作者: 碧云天EthanLee | 来源:发表于2021-08-05 00:32 被阅读0次
    概述

    蛮久没写关于自定义View的东西了,现在来一个。写一个类似雷达扫面界面的View,可用于蓝牙设备搜索界面的显示。还是先看图:


    Screenrecorder-2021-08-04-18-15-57-5262021842252183.gif

    我们按照上面的效果,拆解分步实现:

    • 从里到外画6个圆
    • 实现中间扫描的动态效果
    • 将外部添加进来的设备,以小圆的形式显示
    • 完善对外接口,可添加和删除界面的设备、停止扫描、开始扫描等。
    1、从里到外画6个圆

    第一部分先从里到外画6个圆,且相邻圆的半径差相等。这个简单,不用多解释:

    // spaceBetweenCircle = (int) ((dipToPx(size / 2) - 10) / 6);
     private void drawCircle(Canvas canvas) {
            for (int i = 0; i <= 6; i++) {
                canvas.drawCircle(getWidth() >> 1, getHeight() >> 1, spaceBetweenCircle * i, circlePaint);
            }
        }
    

    上面for循环画出了6个圆,圆心取宽高的一半处,也就是View的中心位置。上面的 spaceBetweenCircle是两圆之间的半径差,是用屏幕宽度计算出来的,乘以 i表示从里到外半径递增。

    2、实现中间扫面的动态效果

    要实现图中的扫描效果要借助两个类,Shader和Matrix。Shader就是阴影,图中颜色渐变的效果就是给画圆的笔设置阴影来实现的。Shader有几个子类:BitmapShader、SweepGradient、LinearGradient 、RadialGradient。这几个大概分别代表底部图层Bitmap渲染、梯度渲染、线性渲染、环形渲染。而我们这次要用到的渲染方式就是梯度渲染,使用到子类SweepGradient实现颜色渐变。

    我们用到SweepGradient也只是实现颜色渐变,而还不能实现动态的扫面效果。所以我们还要让它转起来,这就用到了矩阵Matrix。下面看代码:

     private void initShader() {
            // 注释 1,创建阴影 SweepGradient
            mShader = new SweepGradient(getWidth() >> 1, getHeight() >> 1,
                    new int[]{Color.TRANSPARENT, Color.parseColor("#FF60A8A1")}, null);
           // 注释 2,给画笔设置阴影
            scanPaint.setShader(mShader);
        }
    
    private void drawScan(Canvas canvas) {
            // 注释 3,绘制扫描区域的圆
            canvas.drawCircle(getWidth() >> 1, getHeight() >> 1,
     spaceBetweenCircle * 5, scanPaint);
        }
    
     private void postScan() {
           // 注释 4,每次绘制完之后改变角度,循环绘制
            if (rotationInt >= 360) {
                rotationInt = 0;
            }
            rotationInt += 2;
            // 注释 5,设置新的矩阵角度
            matrix.setRotate(rotationInt, getWidth() >> 1, getHeight() >> 1);// 会先清除之前的状态
    //        matrix.postRotate(2f, getWidth() >> 1, getHeight() >> 1); // 状态累加
           给阴影设置矩阵
            mShader.setLocalMatrix(matrix);
            if (!stopScan) invalidate();
        }
    

    上面注释1、注释 2处分别创建了一个扫描风格的颜色阴影SweepGradient及给画扫描区域的画笔设置阴影。上面的SweepGradient设置了两种颜色值,其中一种是透明色Color.TRANSPARENT,这样过度就实现了渐变。

    上面注释 4、注释 5的地方开始处理矩阵角度。这个 View每次调用绘制完之后,我们就改变矩阵角度,然后给渐变阴影mShader设置矩阵,再重新绘制。如此循环就可以实现扫描的效果。Matrix还可以实现平移、缩放等效果,这里不多解释。

    3、将扫描结果显示在扫描区域

    这里的“扫描结果”当然不是这个View扫描出来的,而是外界扫描到设备之后传进来的。那么这一步我们将图中的实现小圆画在扫描界面上。

    我们看效果可以发现,新显示的设备是跟着扫面线走的。也就是新画的实心小圆要在扫面线刚扫过的地方出现。而且最新的点先显示大的半径,然后再变小。并且颜色随机。还有一个要实现的点就是,实心小圆和圆心的距离代表当前扫描到的设备信号强弱,或者说代表设备距离远近。

    下面我们写一个类来封装扫描到的设备信息,这些信息包括信号等级、设备该现实的位置坐标及设备名称等。

    /**
     * 设备信息
     * 
     * EthanLee
     */
    public class PointPosition {
        // 信号等级
        private int rank = 0;
        // 所能显示的区域半径,这里是默认值
        private int radio = 60;
        // 扫描区域中心
        private PointF centerPoint = new PointF(0f, 0f);
        // 实心小圆圆心
        private PointF mPoint;
        private Random random = new Random();
        // 设备名称
        private String userName = "";
        private int[] colors = {Color.parseColor("#FFE10505"),
                Color.parseColor("#FFFF9800"),
                Color.parseColor("#FF9C27B0"),
                Color.parseColor("#FF02188F"),
                Color.parseColor("#FF0431D8")};
        public int pointColor;
    
        public int getRank() {
            return rank;
        }
    
        public int getRadio() {
            return radio;
        }
    
        public PointF getCenterPoint() {
            return centerPoint;
        }
    
        public PointPosition setRank(int mRank) {
            if (mRank > 70) {
                mRank = 70;
            }
            if (mRank < 20) {
                mRank = 20;
            }
            this.rank = mRank + 20;
            return this;
        }
    
        public PointPosition setRadio(int radio) {
            if (radio < 0) return this;
            this.radio = radio;
            return this;
        }
    
        public PointPosition setCenterPoint(PointF centerPoint) {
            if (centerPoint == null) return this;
            this.centerPoint = centerPoint;
            return this;
        }
       // 注释 6 ,currentDegree是当前扫描线所处的角度
        public PointPosition setPoint(int currentDegree) {
            if (mPoint == null) mPoint = new PointF(0f, 0f);
            // rank是信号等级,这里设置的范围是 20 - 90
            // radio 是可现实的区域半径,也就是扫面区域大圆半径
            // distance 是根据等级 rank和 区域半径算出来的实心小圆到大圆中心处的距离
            float distance = radio * rank / 100;
            // 三角函数分别算出View中心点距离目标点的横、纵坐标距离
            float xDistance = (float) (distance * Math.cos(currentDegree * 2 * Math.PI / 360));
            float yDistance = (float) (distance * Math.sin(currentDegree * 2 * Math.PI / 360));
            // 算出点的横纵坐标
            mPoint.x = centerPoint.x + xDistance;
            mPoint.y = centerPoint.y + yDistance;
            // 算一个随机颜色
            pointColor = colors[random.nextInt(4)];
            return this;
    
        }
        public PointF getPoint() {
            return this.mPoint;
        }
    }
    

    上面注释 6处的方法会获得扫描线当前所处的角度,还会获得当前设备的信号等级以及扫描区域大半径等信息,然后就可以根据这些信息,在上面方法里通过三角函数求得新加入的设备应该显示的坐标点,然后绘制出来。关于使用三角函数求坐标这次就不画图分析了,可以参考我之前的文章:画一个加载控件

    上面确定好设备显示的坐标点之后,就可以添加进来绘制了:

    // 注释 7 往列表里添加设备
    public void addPoint(PointPosition point) {
            if (stopScan) return;
            if (this.pointList.contains(point)) return;
            point.setRadio(spaceBetweenCircle * 6)
                    .setCenterPoint(new PointF(getWidth() >> 1, getHeight() >> 1))
                    .setPoint(rotationInt);
            this.pointList.add(point);
        }
      // 注释 8 绘制所有设备点
      private void drawPoint(Canvas canvas) {
            for (PointPosition pointPosition : pointList) {
                pointPaint.setColor(pointPosition.pointColor);
                if (pointList.indexOf(pointPosition) == pointList.size() - 1) {
                    canvas.drawCircle(pointPosition.getPoint().x, pointPosition.getPoint().y, spaceBetweenCircle >> 1, pointPaint);
                }
                canvas.drawCircle(pointPosition.getPoint().x, pointPosition.getPoint().y, spaceBetweenCircle >> 2, pointPaint);
            }
        }
    
    4、完善对外接口

    下面最后一步我们来完善一下对外接口。文章开头说了,外界可以添加设备、清除设备,控制开始扫描、停止扫描等。这个也简单:

    添加设备在上面注释 8处,这里保存设备的列表是一个 CopyOnWriteArrayList,(写时复制)。

    清除设备:

    // 清除特定设备
     public void removePoint(PointPosition point) {
            if (this.pointList.contains(point)) {
                pointList.remove(point);
            }
            invalidate();
        }
        // 清除所有设备
        public void clearPoint() {
            if (pointList.size() == 0) return;
            pointList.clear();
            invalidate();
        }
    

    开始扫描:

      public void setStartScan(){
            if (!this.stopScan) return;
            this.stopScan = false;
            invalidate();
        }
    

    停止扫描:

     public void setScanStop(){
            if (this.stopScan) return;
            this.stopScan = true;
        }
    

    最后,ScanView的代码:

    /**
     * 蓝牙扫描
     * 
     * EthanLee
     */
    public class ScanView extends View {
        private Paint circlePaint;
        // 两圆间的半径差
        private int spaceBetweenCircle;
        private Paint scanPaint;
        private Shader mShader;
        private Matrix matrix;
        // 实心小圆
        private Paint pointPaint;
        // 扫描到的设备
        private CopyOnWriteArrayList<PointPosition> pointList = new CopyOnWriteArrayList();
        // 阴影旋转角度
        private int rotationInt = 0;
        // 停止扫描
        private boolean stopScan = false;
    
        public ScanView(Context context) {
            this(context, null);
        }
    
        public ScanView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public ScanView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initRes(context, attrs, 0);
        }
    
        private void initRes(Context context, AttributeSet attrs, int defStyleAttr) {
            circlePaint = new Paint();
            circlePaint.setStyle(Paint.Style.STROKE);
            circlePaint.setAntiAlias(true);
            circlePaint.setDither(true);
            circlePaint.setStrokeWidth(2);
            circlePaint.setColor(Color.parseColor("#FF605F5F"));
    
            scanPaint = new Paint();
            scanPaint.setAntiAlias(true);
            scanPaint.setDither(true);
    
            pointPaint = new Paint();
            pointPaint.setStyle(Paint.Style.FILL);
            pointPaint.setAntiAlias(true);
            pointPaint.setDither(true);
            pointPaint.setColor(Color.parseColor("#FF3700B3"));
    
            matrix = new Matrix();
        }
    
        private void initShader() {
            mShader = new SweepGradient(getWidth() >> 1, getHeight() >> 1,
                    new int[]{Color.TRANSPARENT, Color.parseColor("#FF60A8A1")}, null);
            scanPaint.setShader(mShader);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int size = Math.min(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec));
            setMeasuredDimension(size, size);
            spaceBetweenCircle = (int) ((dipToPx(size / 2) - 10) / 6);
        }
    
        private int getMeasureSize(int measureSpec) {
            int measureMode = MeasureSpec.getMode(measureSpec);
            int measureSize = MeasureSpec.getSize(measureSpec);
            if (measureMode == MeasureSpec.EXACTLY) return measureSize;
            if (measureMode == MeasureSpec.AT_MOST) return Math.min(600, measureSize);
            return 600;
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            Log.d("tag", "getMeasuredWidth = " + getMeasuredWidth());
            Log.d("tag", "getMeasuredHeight = " + getMeasuredHeight());
            Log.d("tag", "getWidth = " + getWidth());
            Log.d("tag", "getHeight = " + getHeight());
            initShader();
    //        setBackground(getResources().getDrawable(R.mipmap.start));
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            drawCircle(canvas);
        }
    
        private void drawCircle(Canvas canvas) {
            for (int i = 0; i <= 6; i++) {
                canvas.drawCircle(getWidth() >> 1, getHeight() >> 1, spaceBetweenCircle * i, circlePaint);
            }
            drawScan(canvas);
        }
    
        private void drawScan(Canvas canvas) {
            canvas.drawCircle(getWidth() >> 1, getHeight() >> 1, spaceBetweenCircle * 5, scanPaint);
            drawPoint(canvas);
            postScan();
        }
    
        private void drawPoint(Canvas canvas) {
            for (PointPosition pointPosition : pointList) {
                pointPaint.setColor(pointPosition.pointColor);
                if (pointList.indexOf(pointPosition) == pointList.size() - 1) {
                    canvas.drawCircle(pointPosition.getPoint().x, pointPosition.getPoint().y, spaceBetweenCircle >> 1, pointPaint);
                }
                canvas.drawCircle(pointPosition.getPoint().x, pointPosition.getPoint().y, spaceBetweenCircle >> 2, pointPaint);
            }
        }
    
        private void postScan() {
            if (rotationInt >= 360) {
                rotationInt = 0;
            }
            rotationInt += 2;
            matrix.setRotate(rotationInt, getWidth() >> 1, getHeight() >> 1);// 会先清除之前的状态
    //        matrix.postRotate(2f, getWidth() >> 1, getHeight() >> 1); // 状态累加
            mShader.setLocalMatrix(matrix);
            if (!stopScan) invalidate();
        }
    
        public void setScanStop(){
            if (this.stopScan) return;
            this.stopScan = true;
        }
    
        public void setStartScan(){
            if (!this.stopScan) return;
            this.stopScan = false;
            invalidate();
        }
    
        public void addPoint(PointPosition point) {
            if (stopScan) return;
            if (this.pointList.contains(point)) return;
            point.setRadio(spaceBetweenCircle * 6)
                    .setCenterPoint(new PointF(getWidth() >> 1, getHeight() >> 1))
                    .setPoint(rotationInt);
            this.pointList.add(point);
        }
    
        public void removePoint(PointPosition point) {
            if (this.pointList.contains(point)) {
                pointList.remove(point);
            }
            invalidate();
        }
    
        public void clearPoint() {
            if (pointList.size() == 0) return;
            pointList.clear();
            invalidate();
        }
    
        private float dipToPx(int dip) {
            return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, dip, getResources().getDisplayMetrics());
        }
    }
    

    相关文章

      网友评论

        本文标题:Android自定义View(10)- 写一个雷达扫描界面

        本文链接:https://www.haomeiwen.com/subject/heypvltx.html