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

概述

蛮久没写关于自定义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 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)- 写一个雷达扫描界面)