自定义View实践:指南针的实现

本文将介绍如何通过自定义View实现了一个指南针的效果,效果图如下:

源码GitHub地址

首先是根据磁力计和加速度计计算南向和手机的夹角。通过Android的SensorManager类进行计算,使用的是右手坐标系:

获取SensorManager,并初始化磁力计和加速度计:

public class CompassActivity extends AppCompatActivity implements SensorEventListener {    private SensorManager mSensorManager;    private Sensor mMagneticSensor;    private Sensor mAccelerateSensor;@Overrideprotected void onCreate(Bundle savedInstanceState) {       ...mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);    if (mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) != null && mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null) {        mMagneticSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);        mAccelerateSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);        mHasNeededSensors = true;    } else {    Toast.makeText(this, "没有磁力计或加速度计", Toast.LENGTH_SHORT).show();    return;}
}

在onResume里面注册磁力计和加速度计,并在onPause的时候解除注册:

@Override
protected void onResume() {    super.onResume();    if (mHasNeededSensors) {        mSensorManager.registerListener(this, mMagneticSensor, SensorManager.SENSOR_DELAY_NORMAL);        mSensorManager.registerListener(this, mAccelerateSensor, SensorManager.SENSOR_DELAY_NORMAL);    }
}@Override
protected void onPause() {    super.onPause();    if (mHasNeededSensors) {        mSensorManager.unregisterListener(this);    }
}

实现onSensorChanged接口,这样当磁力计或加速度计数值发生变化的时候会调用该函数告知新数值:

@Override
public void onSensorChanged(SensorEvent event) {   if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {        mMagneticFieldValues = event.values;    } else if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {        mAccelerometerValues = event.values;    }    calculateOrientation();
}

通过calculateOrientation函数计算南向跟手机x轴的夹角,这里用到SensorManager的两个函数,getRotationMatrix和getOrientation。具体计算原理可以参阅两个函数的实现,用法很简单,传入加速度计和磁力计数值即可。得到一个3X1的矩阵,矩阵的三个值代表南向绕三个坐标轴旋转的角度,单位是弧度,我们绘制指南针只需要使用矩阵的第一个值,即南向绕Z轴顺时针旋转过的角度,用alpha表示。当南向指向手机正上方时,alpha=0;指向手机正下方时,alpha=MATH.PI,如下图所示:

为了方便使用极坐标绘制指南针的罗盘,我们把它转换成和X轴的夹角seta,seta=alpha-Math.PI/2,计算的代码如下:

// 计算指南针的南向和手机x轴的角度,以弧度表示(-PI, PI]
private void calculateOrientation() {    float[] results = new float[3];    float[] rotates = new float[9];    SensorManager.getRotationMatrix(rotates, null, mAccelerometerValues, mMagneticFieldValues);    SensorManager.getOrientation(rotates, results);    // alpha是南向和手机Y轴的夹角    float alpha = results[0];    float seta;    // 将alpha转换成南向和手机X轴的夹角,便于使用极坐标系绘制指南针的圆盘    if ((alpha - (-Math.PI)) < 0.000000001) {        seta = (float) (Math.PI / 2);    } else {        seta = (float) (alpha - Math.PI / 2);    }    Log.i("compass:", Math.toDegrees(seta)+"");    mCompassView.setSouth(seta);    mCompassView.invalidate();
}

利用夹角seta,我们就可以利用自定义View绘制指南针了。
首先定义指南针View的属性,通过这些属性,我们可以控制指南针的外观:

<resources>    <declare-styleable name="CompassViewStyle">        <attr name="radius" format="dimension" /> <!--罗盘半径--><attr name="short_dash" format="dimension" /> <!--罗盘外圈短辐射线的长度--><attr name="long_dash" format="dimension" />  <!--罗盘外圈长辐射线的长度--><attr name="text_size" format="dimension" />  <!--罗盘上文字的尺寸-->    </declare-styleable>
</resources>

在自定义View里解析这些属性:

public class CompassView extends View {    private Paint mPaint;    private double mRadius;    private double mDash_short;    private double mDash_long;    private double mTextSize;    private double mSeta = -Math.PI / 2;    public CompassView(Context context, AttributeSet attrs) {        super(context, attrs);        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CompassViewStyle);        mRadius = typedArray.getDimension(R.styleable.CompassViewStyle_radius, 0);        mDash_short = typedArray.getDimension(R.styleable.CompassViewStyle_short_dash, 20);        mDash_long = typedArray.getDimension(R.styleable.CompassViewStyle_long_dash, 30);        mTextSize = typedArray.getDimension(R.styleable.CompassViewStyle_text_size, 10);        typedArray.recycle();    }...
}

重写onDraw方法,完成绘制。第一步是在以屏幕中心为原点的XOY坐标系中计算出需要绘制的点的坐标(x, y),第二步是转换成以手机屏幕左上角为原点的屏幕坐标系的值(width/2 + x, height/2 - y),其中height和width是屏幕的宽高,最终的绘制是在屏幕坐标系中进行的:

public class CompassView extends View {@Overrideprotected void onDraw(Canvas canvas) {    mPaint.setColor(getResources().getColor(R.color.text_bg_green_stroke));    double width = getWidth();    double height = getHeight();   // choose the shortest in width, height, radius as actual radius    double radius = (mRadius < width && mRadius < height) ? mRadius : Math.min(width, height);    // draw the outer circle    for (int seta = -180; seta < 180; seta++) {        float x = (float) (width / 2 + radius * Math.cos(seta * Math.PI/180));        float y = (float) (height / 2 + radius * Math.sin(seta * Math.PI / 180));        // 罗盘外边缘每隔1°画一条短辐射线,每隔20°画一条长辐射线if ((seta - (-180)) % 20 == 0) {            float x2 = (float) (width / 2 + (radius + mDash_long) * Math.cos(seta * Math.PI/180));            float y2 = (float) (height / 2 + (radius + mDash_long) * Math.sin(seta * Math.PI / 180));            canvas.drawLine(x, y, x2, y2, mPaint);        } else {            float x2 = (float) (width / 2 + (radius + mDash_short) * Math.cos(seta * Math.PI/180));            float y2 = (float) (height / 2 + (radius + mDash_short) * Math.sin(seta * Math.PI / 180));            canvas.drawLine(x, y, x2, y2, mPaint);        }    }    // draw 东西南北    double spacing = 10;    mPaint.setTextSize((float)mTextSize);    Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();    canvas.drawText("东", (float)(width / 2 + radius - spacing - mTextSize), (float)(height / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.descent - fontMetrics.leading), mPaint);    canvas.drawText("西", (float)(width / 2 - radius + spacing), (float)(height / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.descent - fontMetrics.leading), mPaint);    canvas.drawText("南", (float)(width /2 - mTextSize /2), (float)(height / 2 + radius - spacing - fontMetrics.descent), mPaint);    canvas.drawText("北", (float)(width /2 - mTextSize /2), (float)(height / 2 - radius + spacing + fontMetrics.leading - fontMetrics.ascent), mPaint); // draw 8 triangles    double triangle_half_bottom = 40;    double triangle_vertical_line = 200;    double triangle_vertical_line_2 = 300;  Point p0 = new Point((int)(width / 2), (int)(height / 2 - triangle_vertical_line_2));    Point p00 = new Point((int)(width / 2 + triangle_half_bottom), (int)(height / 2));    Point p000 = new Point((int)(width / 2 - triangle_half_bottom), (int)(height / 2));    Point p1 = new Point((int)(width / 2 + triangle_vertical_line / Math.sqrt(2)), (int)(height / 2 - triangle_vertical_line / Math.sqrt(2)));    Point p11 = new Point((int)(width / 2 + triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 + triangle_half_bottom / Math.sqrt(2)));    Point p111 = new Point((int)(width / 2 - triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 - triangle_half_bottom / Math.sqrt(2)));    Point p2 = new Point((int)(width / 2 + triangle_vertical_line_2), (int)(height / 2));    Point p22 = new Point((int)(width / 2), (int)(height / 2 + triangle_half_bottom));    Point p222 = new Point((int)(width / 2), (int)(height / 2 - triangle_half_bottom));    Point p3 = new Point((int)(width / 2 + triangle_vertical_line / Math.sqrt(2)), (int)(height / 2 + triangle_vertical_line / Math.sqrt(2)));    Point p33 = new Point((int)(width / 2 - triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 + triangle_half_bottom / Math.sqrt(2)));    Point p333 = new Point((int)(width / 2 + triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 - triangle_half_bottom / Math.sqrt(2)));    Point p4 = new Point((int)(width / 2), (int)(height / 2 + triangle_vertical_line_2));    Point p44 = new Point((int)(width / 2 - triangle_half_bottom), (int)(height / 2));    Point p444 = new Point((int)(width / 2 + triangle_half_bottom), (int)(height / 2));    Point p5 = new Point((int)(width / 2 - triangle_vertical_line / Math.sqrt(2)), (int)(height / 2 + triangle_vertical_line / Math.sqrt(2)));    Point p55 = new Point((int)(width / 2 - triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 - triangle_half_bottom / Math.sqrt(2)));    Point p555 = new Point((int)(width / 2 + triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 + triangle_half_bottom / Math.sqrt(2)));    Point p6 = new Point((int)(width / 2 - triangle_vertical_line_2), (int)(height / 2));    Point p66 = new Point((int)(width / 2 ), (int)(height / 2 - triangle_half_bottom));    Point p666 = new Point((int)(width / 2), (int)(height / 2 + triangle_half_bottom));    Point p7 = new Point((int)(width / 2 - triangle_vertical_line / Math.sqrt(2)), (int)(height / 2 - triangle_vertical_line / Math.sqrt(2)));    Point p77 = new Point((int)(width / 2 + triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 - triangle_half_bottom / Math.sqrt(2)));    Point p777 = new Point((int)(width / 2 - triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 + triangle_half_bottom / Math.sqrt(2)));    Path path = new Path();    path.moveTo(p0.x, p0.y);    path.lineTo(p00.x, p00.y);    path.lineTo(p000.x, p000.y);    path.close();    path.moveTo(p1.x, p1.y);    path.lineTo(p11.x, p11.y);    path.lineTo(p111.x, p111.y);    path.close();    path.moveTo(p2.x, p2.y);    path.lineTo(p22.x, p22.y);    path.lineTo(p222.x, p222.y);    path.close();    path.moveTo(p3.x, p3.y);    path.lineTo(p33.x, p33.y);    path.lineTo(p333.x, p333.y);    path.close();    path.moveTo(p4.x, p4.y);    path.lineTo(p44.x, p44.y);    path.lineTo(p444.x, p444.y);    path.close();  path.moveTo(p5.x, p5.y);    path.lineTo(p55.x, p55.y);    path.lineTo(p555.x, p555.y);    path.close();    path.moveTo(p6.x, p6.y);    path.lineTo(p66.x, p66.y);    path.lineTo(p666.x, p666.y);    path.close();    path.moveTo(p7.x, p7.y);    path.lineTo(p77.x, p77.y);    path.lineTo(p777.x, p777.y);    path.close(); mPaint.setStyle(Paint.Style.FILL);      canvas.drawPath(path, mPaint);    // 计算指南针上的四个点    double spacing3 = 50;    Point b = new Point((int)(width / 2 + radius * Math.cos(mSeta)),            (int)(height / 2 - radius * Math.sin(mSeta)));    Point bb = new Point((int)(width / 2 + spacing3 * Math.cos(mSeta - Math.PI / 2)),            (int)(height / 2 - spacing3 * Math.sin(mSeta - Math.PI / 2)));    Point bbb = new Point((int)(width / 2 + spacing3 * Math.cos(Math.PI / 2 + mSeta)),            (int)(height / 2 - spacing3 * Math.sin(Math.PI / 2 + mSeta)));    Point b2 = new Point((int)(width / 2 - radius * Math.cos(mSeta)),            (int)(height / 2 + radius * Math.sin(mSeta)));    // 画南向指针    Path path2 = new Path();    path2.moveTo(b.x, b.y);    path2.lineTo(bb.x, bb.y);    path2.lineTo(bbb.x, bbb.y);    path2.close();    mPaint.setColor(getResources().getColor(R.color.red_a11));    canvas.drawPath(path2, mPaint);  // 北向指针  Path path3 = new Path();    path3.moveTo(b2.x, b2.y);    path3.lineTo(bbb.x, bbb.y);    path3.lineTo(bb.x, bb.y);    path3.close();    mPaint.setColor(getResources().getColor(R.color.blue_a1));    canvas.drawPath(path3, mPaint);
}

完毕。
源码GitHub地址

自定义View实践:指南针的实现相关推荐

  1. 自定义View之指南针(反编译别人的代码实现)

    一.说明 偶尔点开魅族手机内置的工具箱应用,发现其指南针做的还不错,就想模拟做一个类似的效果,在这里我们不准备自己从头开始编写代码,而是采用一点黑科技,首先,我们从魅族系统中导出工具箱应用的apk,然 ...

  2. Android自定义View实践 空气质量检测 pm2.5

    直接先看效果图 自定义气体检测视图我们先整理下需要做的的事情 画五个圆弧 每个圆弧上再通过具体的数据绘制一定角度的圆弧 甲醛那个进度条比较特殊,一头平一头椭圆该怎么实现? 文字的绘制 明白了需求我们开 ...

  3. Android 系统(201)---Android 自定义View实战系列 :时间轴

    Android 自定义View实战系列 :时间轴 Android开发中,时间轴的 UI需求非常常见,如下图: 本文将结合 自定义View & RecyclerView的知识,手把手教你实现该常 ...

  4. 自定义view - 收藏集 - 掘金

    Android 从 0 开始自定义控件之 View 的 draw 过程 (九) - Android - 掘金 转载请标明出处: http://blog.csdn.net/airsaid/... 本文出 ...

  5. Android自定义View,带你实现小米指南针和时钟

    吾幼时即嗜画,家贫无从至笔墨纸砚,遂从姑苏城外寒山寺搬得红枫叶两筐.未几,吾发现红枫叶蒸包子很不错,所以现在我包的包子很好吃. 我的测试机是小米,我发现小米的指南针和钟表挺好玩的,Android画画又 ...

  6. 自定义View——游动锦鲤实践

    目录 自定义View--游动锦鲤实践 布局文件 使用布局--MainAvtivity 主要实现--FishDrawable 不积跬步,无以至千里:不积小流,无以成江海.要沉下心来,诗和远方的路费真的很 ...

  7. android版本更新框架、新闻客户端、音乐播放器、自定义View、Github客户端、指南针等源码...

    Android精选源码 XUpdate 一个轻量级.高可用性的Android版本更新框架 Android一个可定制的圆形进度条 Android自定义View分享 打钩动画源码 android音乐文件播 ...

  8. android绘制心形_Android自定义View系列(一)——打造一个爱心进度条

    写作原因:Android进阶过程中有一个绕不开的话题--自定义View.这一块是安卓程序员更好地实现功能自主化必须迈出的一步.下面这个系列博主将通过实现几个例子来认识安卓自定义View的方法.从自定义 ...

  9. Android Paint应用之自定义View实现进度条控件

    在上一篇文章<Android神笔之Paint>学习了Paint的基本用法,但是具体的应用我们还没有实践过.从标题中可知,本文是带领读者使用Paint,自定义一个进度条控件. 上图就是本文要 ...

  10. Android 高级自定义View实战

    2019独角兽企业重金招聘Python工程师标准>>> 在android组件中主要分为两种:容器(LinearLayout....)和子View(TextView......),但是 ...

最新文章

  1. 表单高级应用和语义化
  2. 一文读懂AlphaGo背后的强化学习:它的背景知识与贝尔曼方程的原理
  3. Linux 的账号与群组(转)
  4. XSLT 与 Java集成常见技术关键点
  5. 插入排序、选择排序、快速排序以及归并排序(附Python代码)
  6. r语言系统计算上是奇异的_R语言实现并行计算
  7. 使用react开发管理后台
  8. 全球顶级开源大神们现身 COSCon‘20
  9. saltstack的探索-利用脚本增加用户
  10. 《路由器开发 - 路由器刷机指南》联想NWiFi3刷机
  11. 桂林瑶大叔名老中医馆
  12. Abaqus二次开发捕获几何元素方法归纳
  13. C语言怎样判断乘法越界,c语言算术运算符越界问题解决方案
  14. 批量修改Excel单元格内某些文字的颜色
  15. osgEarth示例分析——osgearth_los
  16. 顶会 INFOCOM 巴黎进行时,最高荣誉花落微软老将
  17. Framebuffer应用开发
  18. USB(一)——USB通用串行总线基础知识详述
  19. Echarts中太阳图(Sunburst)的实例
  20. cok无法连接验证服务器,黎明杀机在线服务无法连接解决方法一览

热门文章

  1. python 多张图片合成pdf_Python多图片合并PDF的方法
  2. visio增加连接点
  3. ROS开发--Qt接收摇杆话题
  4. 经纬财富:莆田炒现货白银技巧
  5. 如何检测页面是否允许访问Cookie
  6. 用vb调用bartender并打印
  7. 用户帐户控制---为了对电脑进行保护,已经阻止此应用。---管理员已阻止你运行此应。有关详细信息,请与管理员联系。
  8. 在centos上安装pycharm
  9. 明尼苏达大学Transportation Research Data Lab (TDRL)交通数据读取
  10. 计算机辅储存器有什么,计算机辅存储器包括