Android自定义View实现方位刻度尺(类似于吃鸡手游)

Android自定义View实现方位刻度尺(类似于吃鸡手游)

先上效果图

动态效果图
gif可能看不清,我下面放几张图片
第一张
第二张
第三张
第四张


原理解析

  • 首先,我们应该把看得到的内容从上至下分成三部分:最上面的文字、中间的竖线和最下面的三角形。
  • 其次,每个竖线的间隔,根据View宽度均分为10等份,每等分是5°,最小刻度单位1°(相当于1等份里又分成了5等份),屏幕上显示的区间间隔为50°。所以,每次画线只需要画11条竖线和对应的文字(for循环,从0开始遍历10次即可),最下面的三角形固定在中间。
  • 只要方向传感器检测到方位改变,就传入当前的值(这里定义为x,值的区间为0 - 359,0代表正北,90代表正东,180代表正南,270代表正西)。由于x经过换算需要显示在屏幕正中间(也就是三角形上方),所以x只能是for循环的第6个数(在本for循环里表示为5,因为是从0开始),其它的数根据公式:y = x + 5 * i - 25 得到。
  • 计算偏移量,由于最小刻度为1°,而显示的每等份的单位是5°,如果x不是以0或者5结尾的,就不一定在刻度线上,刻度线会往左偏移。

关键代码解析

自定义View的操作肯定是在onDraw方法中实现的,View的左上角是坐标的(0,0)点

  • 首先,先把一些全局常量代码附上来,下面代码中用到的常量都出自这
 private static final int SPACING = 40; // 每边距离View边缘的间距
 private static final int SCALE_LINE_LENGTH = 24; // 刻度尺每条线的长度
 private static final int SCALE_LINE_UNIT = 10; // 需要把刻度尺分成几等份
  • Demo中是均分为10份,每边留出的距离是40,两边就是80,获取到View宽度后,就能计算出每等份刻度尺的间隔,这个值是用来计算需要画刻度线和文字的x轴坐标的
 // 每等份刻度尺的间隔
 float unit = (getWidth() - SPACING * 2) / SCALE_LINE_UNIT;
  • 通过for循环画出11条刻度线和相对应的值
 for (int i = 0; i < 11; i++) {
     // 需要画线的x坐标值
     float x = unit * i + SPACING - offset①;
     // 画刻度线
     canvas.drawLine(x, SCALE_LINE_LENGTH, x, SCALE_LINE_LENGTH * 2, mScaleLinePaint);
     // 画刻度值
     int tt;
     int t = temp② + 5 * i - 25;
     // 此公式在0°前后会出现正负,所以需要进行下面的判断,保证最后的数为0 - 360之间的数
     if (t < 0) {
         tt = 360 + t;
      } else if (t > 360) {
         tt = t - 360;
      } else {
         tt = t;
      }
      // 画对应的刻度值
      canvas.drawText(OrientationUtils.calculateDegree(tt)③, x, SCALE_LINE_LENGTH, mTextPaint);
 }

上面的代码中我打了三个标记,分别是①offset,②temp,③OrientationUtils.calculateDegree(tt),这是主要的步骤,具体的值是怎么来的,下面一步步来说明

offset偏移量的计算

原理解析里说了,Demo中是分成了10等份,每等份是5°,最小刻度单位是1°,所以,显示在屏幕上的一共有50°,每个刻度的距离就算出来了,是 View宽度 / 50,至于偏移了多少距离,是根据 mValues%5(mValues是从外部传进来的当前方位的角度,后面出现的mValue都是该值,这个计算出来的值就是当前值较之末尾为0或5的值偏移了多少单位)乘以每个刻度的距离得出来的。

 // 偏移值
 int offset = getWidth() / 50 * (mValue % 5);

temp值的计算

temp值的作用只有一个,那就是得到比当前值(也就是mValue )小的末尾为0或5的数(后面统称为05),如果当前值正好为05,那这个temp值就等于mValue。为什么要这么做呢?因为刻度上显示的单位刻度就是5°,所以只会显示05,如果不是这个数,那就找它左边的数(也就是比它小,05),这个数是一个基准,是画线的中心点,也是for循环中最中心的一个点

要得到比当前值(05除外)小的05,首先要知道这个数的个位比5大还是小,5是一个基准,举个例子最直观,例如:74的05就是70,76的05就是75。那么,这个05怎么得到呢?还是举个例子,例如,当前值为12,需要先判断它的个位是大于5还是小于5,这个个位数值怎么拿,通过公式:y = x % 10 得到,将12代入,得到的个位数就是2,小于5,就通过公式: y = x / 10 * 10 得到temp值为10。有人肯定会疑惑,这个公式算出来不是应该还是12吗?这个x是int类型(因为本Demo把方位角强转为int了,原型是float类型)。如果当前值为16,代入公式得到的个位数是6,大于5,需要另外一个公式:y = x / 10 * 10 + 5 得到temp值为15。如果当前值就是05,那么temp值就是等于当前值。

 // 商
 int merchant = mValue / 10;
 // 余数
 int remainder = mValue % 10;
 // 偏移值
 int offset = getWidth() / 50 * (mValue % 5);
 // 当前值换算成(末尾为5或者0)的值
 int temp;
 if (remainder > 0) {
     if (remainder < 5) {
         temp = merchant * 10;
     } else if (remainder > 5) {
         temp = merchant * 10 + 5;
     } else {
         temp = mValue;
     }
 } else {
    temp = mValue;
 }

画三角形

这个简单,画个起始点,然后再画另外两个点,连起来就行了,直接上代码

 // 画下面的标志(三角形)
 // 起点
 mSignPath.moveTo(getWidth() / 2, SCALE_LINE_LENGTH * 2 + 10);
 // 另外两个点
 mSignPath.lineTo(getWidth() / 2 - SCALE_LINE_LENGTH, SCALE_LINE_LENGTH * 3 + 10);
 mSignPath.lineTo(getWidth() / 2 + SCALE_LINE_LENGTH, SCALE_LINE_LENGTH * 3 + 10);
 // 闭合
 mSignPath.close();
 canvas.drawPath(mSignPath, mSignPaint);

关键的代码都介绍的差不多了,剩下的 OrientationUtils.calculateDegree(tt) 其实就是一个工具类,用来把当前的方位角度换算成自己要显示的信息,直接上代码

public class OrientationUtils {
     /**
     * 根据角度计算方位角度(方向传感器)
     *
     * @param degree 角度
     * @return 方位角度
     */
    public static String calculateDegree(int degree) {
        if (degree > 0 && degree < 90) {
            if (degree == 45) {
                return "东北";
            }
            return degree + "°";
        } else if (degree > 90 && degree < 180) {
            if (degree == 135) {
                return "东南";
            }
            return (degree - 90) + "°";
        } else if (degree > 180 && degree < 270) {
            if (degree == 225) {
                return "西南";
            }
            return (degree - 180) + "°";
        } else if (degree > 270 && degree < 360) {
            if (degree == 315) {
                return "西北";
            }
            return (degree - 270) + "°";
        } else if (degree == 0 || degree == 360) {
            return "正北";
        } else if (degree == 90) {
            return "正东";
        } else if (degree == 180) {
            return "正南";
        } else if (degree == 270) {
            return "正西";
        }

        return "";
    }
 }

附上源码

package com.xx.electroniccompass.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import com.xx.electroniccompass.util.OrientationUtils;

/**
 * 日期:2018/7/19
 * 描述:刻度尺
 *
 * @author XX
 */
public class ScaleLineView extends View {

    private static final int SPACING = 40; // 每边距离View边缘的间距
    private static final int SCALE_LINE_LENGTH = 24; // 刻度尺每条线的长度
    private static final int SCALE_LINE_UNIT = 10; // 需要把刻度尺分成几等份

    private Paint mScaleLinePaint;
    private Paint mSignPaint;
    private Paint mTextPaint;
    private Path mSignPath;

    private int mValue;

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

    public ScaleLineView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScaleLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mScaleLinePaint = new Paint();
        mScaleLinePaint.setColor(Color.WHITE);
        mScaleLinePaint.setAntiAlias(true);
        mScaleLinePaint.setStyle(Paint.Style.STROKE);
        mScaleLinePaint.setStrokeWidth(2);

        mTextPaint = new Paint();
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        mTextPaint.setStrokeWidth(2);
        mTextPaint.setTextSize(24);

        mSignPaint = new Paint();
        mSignPaint.setColor(Color.RED);
        mSignPaint.setAntiAlias(true);
        mSignPaint.setStyle(Paint.Style.FILL_AND_STROKE);

        mSignPath = new Path();

        setBackgroundColor(Color.argb(50, 0, 0, 0));
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 每等份刻度尺的间隔
        float unit = (getWidth() - SPACING * 2) / SCALE_LINE_UNIT;

        // 画下面的一条横线
//        canvas.drawLine(SPACING, SCALE_LINE_LENGTH * 2, getWidth() - SPACING, SCALE_LINE_LENGTH * 2, mScaleLinePaint);

        // 画下面的标志(三角形)
        // 起点
        mSignPath.moveTo(getWidth() / 2, SCALE_LINE_LENGTH * 2 + 10);
        // 另外两个点
        mSignPath.lineTo(getWidth() / 2 - SCALE_LINE_LENGTH, SCALE_LINE_LENGTH * 3 + 10);
        mSignPath.lineTo(getWidth() / 2 + SCALE_LINE_LENGTH, SCALE_LINE_LENGTH * 3 + 10);
        // 闭合
        mSignPath.close();
        canvas.drawPath(mSignPath, mSignPaint);

        // 商
        int merchant = mValue / 10;
        // 余数
        int remainder = mValue % 10;
        // 偏移值
        int offset = getWidth() / 50 * (mValue % 5);
        // 当前值换算成(末尾为5或者0)的值
        int temp;
        if (remainder > 0) {
            if (remainder < 5) {
                temp = merchant * 10;
            } else if (remainder > 5) {
                temp = merchant * 10 + 5;
            } else {
                temp = mValue;
            }
        } else {
            temp = mValue;
        }

        for (int i = 0; i < 11; i++) {
            // 需要画线的x坐标值
            float x = unit * i + SPACING - offset;
            // 画刻度线
            canvas.drawLine(x, SCALE_LINE_LENGTH, x, SCALE_LINE_LENGTH * 2, mScaleLinePaint);
            // 画刻度值
            int tt;
            int t = temp + 5 * i - 25;
            // 此公式在0°前后会出现正负,所以需要进行下面的判断,保证最后的数为0 - 360之间的数
            if (t < 0) {
                tt = 360 + t;
            } else if (t > 360) {
                tt = t - 360;
            } else {
                tt = t;
            }
            canvas.drawText(OrientationUtils.calculateDegree(tt), x, SCALE_LINE_LENGTH, mTextPaint);
        }
    }

    private int measureHeight(int measureSpec) {
        int result;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);

        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = 75;
            if (mode == MeasureSpec.AT_MOST) {
                result = Math.min(result, size);
            }
        }
        return result;

    }

    private int measureWidth(int measureSpec) {
        int result;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);

        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = 75;
            if (mode == MeasureSpec.AT_MOST) {
                result = Math.min(result, size);
            }
        }
        return result;

    }

    /**
     * 设置当前值
     *
     * @param value 当前值
     */
    public void setValues(int value) {
        this.mValue = value;
        invalidate();
    }
}

本Demo只是很简单的实现,大家可以自己做拓展,包括属性设置、功能优化等,至于怎么根据方向传感器获取当前方位角信息,大家自己百度一下吧,网上资料有很多。最后,感谢大家的阅读,有什么问题可以留言…

猜你喜欢

转载自blog.csdn.net/baidu_35559769/article/details/81165537