利用Android系统的传感器开发水平仪应用的全过程介绍

快过年了,大家都回家了,我还在发博客,是不是很有心呢!
其实是今天最后一篇博文,给明年开个好头吧。下面上干货。


通过Android设备自带的方向传感器,开发水平仪,先上图:
倾斜状态 水平状态

整个应用通过一个Activity接收传感器数据传递给自定义水平仪控件显示完成。

[转载请注明:Canney 原创:http://blog.csdn.net/canney_chen/article/details/54693563 ]

1. 自定义水平仪控件

代码(me.kaini.level.LevelView.java )

package me.kaini.level;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.view.View;

/**
 * 水平仪控件
 * 通过设置{@link #setAngle(double, double)}
 * @author [email protected]
 */
public class LevelView extends View {

    /**
     * 最大圈半径
     */
    private float mLimitRadius = 0;

    /**
     * 气泡半径
     */
    private float mBubbleRadius;

    /**
     * 最大限制圈颜色
     */
    private int mLimitColor;

    /**
     * 限制圈宽度
     */
    private float mLimitCircleWidth;


    /**
     * 气泡中心标准圆颜色
     */
    private int mBubbleRuleColor;

    /**
     * 气泡中心标准圆宽
     */
    private float mBubbleRuleWidth;

    /**
     * 气泡中心标准圆半径
     */
    private float mBubbleRuleRadius;

    /**
     * 水平后的颜色
     */
    private int mHorizontalColor;

    /**
     * 气泡颜色
     */
    private int mBubbleColor;

    private Paint mBubblePaint;
    private Paint mLimitPaint;
    private Paint mBubbleRulePaint;

    /**
     * 中心点座标
     */
    private PointF centerPnt = new PointF();

    /**
     * 计算后的气泡点
     */
    private PointF bubblePoint;
    private double pitchAngle = -90;
    private double rollAngle = -90;
    private Vibrator vibrator;

    public LevelView(Context context) {
        super(context);
        init(null, 0);
    }

    public LevelView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public LevelView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs, defStyle);
    }

    private void init(AttributeSet attrs, int defStyle) {
        // Load attributes
        final TypedArray a = getContext().obtainStyledAttributes(
                attrs, R.styleable.LevelView, defStyle, 0);

        mBubbleRuleColor = a.getColor(R.styleable.LevelView_bubbleRuleColor, mBubbleRuleColor);

        mBubbleColor = a.getColor(R.styleable.LevelView_bubbleColor, mBubbleColor);
        mLimitColor = a.getColor(R.styleable.LevelView_limitColor, mLimitColor);

        mHorizontalColor = a.getColor(R.styleable.LevelView_horizontalColor, mHorizontalColor);


        mLimitRadius = a.getDimension(R.styleable.LevelView_limitRadius, mLimitRadius);
        mBubbleRadius = a.getDimension(R.styleable.LevelView_bubbleRadius, mBubbleRadius);
        mLimitCircleWidth = a.getDimension(R.styleable.LevelView_limitCircleWidth, mLimitCircleWidth);

        mBubbleRuleWidth = a.getDimension(R.styleable.LevelView_bubbleRuleWidth, mBubbleRuleWidth);

        mBubbleRuleRadius = a.getDimension(R.styleable.LevelView_bubbleRuleRadius, mBubbleRuleRadius);


        a.recycle();


        mBubblePaint = new Paint();

        mBubblePaint.setColor(mBubbleColor);
        mBubblePaint.setStyle(Paint.Style.FILL);
        mBubblePaint.setAntiAlias(true);

        mLimitPaint = new Paint();

        mLimitPaint.setStyle(Paint.Style.STROKE);
        mLimitPaint.setColor(mLimitColor);
        mLimitPaint.setStrokeWidth(mLimitCircleWidth);
        //抗锯齿
        mLimitPaint.setAntiAlias(true);

        mBubbleRulePaint = new Paint();
        mBubbleRulePaint.setColor(mBubbleRuleColor);
        mBubbleRulePaint.setStyle(Paint.Style.STROKE);
        mBubbleRulePaint.setStrokeWidth(mBubbleRuleWidth);
        mBubbleRulePaint.setAntiAlias(true);

        vibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        calculateCenter(widthMeasureSpec, heightMeasureSpec);
    }

    private void calculateCenter(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.UNSPECIFIED);

        int height = MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.UNSPECIFIED);

        int center = Math.min(width, height) / 2;

        centerPnt.set(center, center);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        boolean isCenter = isCenter(bubblePoint);
        int limitCircleColor = isCenter ? mHorizontalColor : mLimitColor;
        int bubbleColor = isCenter ? mHorizontalColor : mBubbleColor;

        //水平时振动
        if(isCenter){
            vibrator.vibrate(10);
        }

        mBubblePaint.setColor(bubbleColor);
        mLimitPaint.setColor(limitCircleColor);

        canvas.drawCircle(centerPnt.x, centerPnt.y, mBubbleRuleRadius, mBubbleRulePaint);
        canvas.drawCircle(centerPnt.x, centerPnt.y, mLimitRadius, mLimitPaint);

        drawBubble(canvas);

    }

    private boolean isCenter(PointF bubblePoint){

        if(bubblePoint == null){
            return false;
        }

        return Math.abs(bubblePoint.x - centerPnt.x) < 1 && Math.abs(bubblePoint.y - centerPnt.y) < 1;
    }

    private void drawBubble(Canvas canvas) {
        if(bubblePoint != null){
            canvas.drawCircle(bubblePoint.x, bubblePoint.y, mBubbleRadius, mBubblePaint);
        }
    }

    /**
     * Convert angle to screen coordinate point.
     * @param rollAngle 横滚角(弧度)
     * @param pitchAngle 俯仰角(弧度)
     * @return
     */
    private PointF convertCoordinate(double rollAngle, double pitchAngle, double radius){
        double scale = radius / Math.toRadians(90);

        //以圆心为原点,使用弧度表示座标
        double x0 = -(rollAngle * scale);
        double y0 = -(pitchAngle * scale);

        //使用屏幕座标表示气泡点
        double x = centerPnt.x - x0;
        double y = centerPnt.y - y0;

        return new PointF((float)x, (float)y);
    }

    /**
     *
     * @param pitchAngle (弧度)
     * @param rollAngle (弧度)
     */
    public void setAngle(double rollAngle, double pitchAngle) {

        this.pitchAngle = pitchAngle;
        this.rollAngle = rollAngle;

        //考虑气泡边界不超出限制圆,此处减去气泡的显示半径,做为最终的限制圆半径
        float limitRadius = mLimitRadius - mBubbleRadius;

        bubblePoint = convertCoordinate(rollAngle, pitchAngle, mLimitRadius);
        outLimit(bubblePoint, limitRadius);

        //座标超出最大圆,取法向圆上的点
        if(outLimit(bubblePoint, limitRadius)){
            onCirclePoint(bubblePoint, limitRadius);
        }

        invalidate();
    }

    /**
     * 验证气泡点是否超过限制{@link #mLimitRadius}
     * @param bubblePnt
     * @return
     */
    private boolean outLimit(PointF bubblePnt, float limitRadius){

        float cSqrt = (bubblePnt.x - centerPnt.x)*(bubblePnt.x - centerPnt.x)
                + (centerPnt.y - bubblePnt.y) * + (centerPnt.y - bubblePnt.y);


        if(cSqrt - limitRadius * limitRadius > 0){
            return true;
        }
        return false;
    }

    /**
     * 计算圆心到 bubblePnt点在圆上的交点座标
     * 即超出圆后的最大圆上座标
     * @param bubblePnt 气泡点
     * @param limitRadius 限制圆的半径
     * @return
     */
    private PointF onCirclePoint(PointF bubblePnt, double limitRadius) {
        double azimuth = Math.atan2((bubblePnt.y - centerPnt.y), (bubblePnt.x - centerPnt.x));
        azimuth = azimuth < 0 ? 2 * Math.PI + azimuth : azimuth;

        //圆心+半径+角度 求圆上的座标
        double x1 = centerPnt.x + limitRadius * Math.cos(azimuth);
        double y1 = centerPnt.y + limitRadius * Math.sin(azimuth);

        bubblePnt.set((float) x1, (float) y1);

        return bubblePnt;
    }

    public double getPitchAngle(){
        return this.pitchAngle;
    }

    public double getRollAngle(){
        return this.rollAngle;
    }


}

属性(attrs_level_view.xml)

<resources>
    <declare-styleable name="LevelView">
        <attr name="limitRadius" format="dimension" />
        <attr name="limitColor" format="color"/>
        <attr name="limitCircleWidth" format="dimension"/>
        <attr name="bubbleRadius" format="dimension"/>
        <attr name="bubbleRuleColor" format="color"/>
        <attr name="bubbleRuleWidth" format="dimension"/>
        <attr name="bubbleRuleRadius" format="dimension"/>
        <attr name="bubbleColor" format="color" />
        <attr name="horizontalColor" format="color"/>
    </declare-styleable>
</resources>

属性说明

属性 说明
limitRadius 边界圆的半径
limitColor 边界圆的颜色
limitCircleWidth 边界圆的宽
bubbleRadius 气泡半径
bubbleColor 气泡颜色
bubbleRuleColor 水平中心圆的颜色
bubbleRuleWidth 水平中心圆的宽
bubbleRuleRadius 水平中心圆的半径
horizontalColor 水平后的边界圆,气泡的颜色

[转载请注明:Canney 原创:http://blog.csdn.net/canney_chen/article/details/54693563 ]

2. 示例

代码(me.kaini.level.MainActivity)

负责接收传感器数据传递给水平仪控件

package me.kaini.level;

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;

/**
 * @author [email protected]
 */
public class MainActivity extends AppCompatActivity implements SensorEventListener {

    private SensorManager sensorManager;
    private Sensor acc_sensor;
    private Sensor mag_sensor;

    private float[] accValues = new float[3];
    private float[] magValues = new float[3];
    // 旋转矩阵,用来保存磁场和加速度的数据
    private float r[] = new float[9];
    // 模拟方向传感器的数据(原始数据为弧度)
    private float values[] = new float[3];

    private LevelView levelView;
    private TextView tvHorz;
    private TextView tvVert;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        levelView = (LevelView)findViewById(R.id.gv_hv);

        tvVert = (TextView)findViewById(R.id.tvv_vertical);
        tvHorz = (TextView)findViewById(R.id.tvv_horz);

        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    }

    @Override
    public void onResume() {
        super.onResume();

        acc_sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        mag_sensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
        // 给传感器注册监听:
        sensorManager.registerListener(this, acc_sensor, SensorManager.SENSOR_DELAY_NORMAL);
        sensorManager.registerListener(this, mag_sensor, SensorManager.SENSOR_DELAY_NORMAL);
    }

    @Override
    protected void onPause() {
        // 取消方向传感器的监听
        sensorManager.unregisterListener(this);
        super.onPause();
    }

    @Override
    protected void onStop() {
        // 取消方向传感器的监听
        sensorManager.unregisterListener(this);
        super.onStop();
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        // 获取手机触发event的传感器的类型
        int sensorType = event.sensor.getType();
        switch (sensorType) {
            case Sensor.TYPE_ACCELEROMETER:
                accValues = event.values.clone();
                break;
            case Sensor.TYPE_MAGNETIC_FIELD:
                magValues = event.values.clone();
                break;

        }

        SensorManager.getRotationMatrix(r, null, accValues, magValues);
        SensorManager.getOrientation(r, values);

        // 获取 沿着Z轴转过的角度
        float azimuth = values[0];

        // 获取 沿着X轴倾斜时 与Y轴的夹角
        float pitchAngle = values[1];

        // 获取 沿着Y轴的滚动时 与X轴的角度
        //此处与官方文档描述不一致,所在加了符号(https://developer.android.google.cn/reference/android/hardware/SensorManager.html#getOrientation(float[], float[]))
        float rollAngle = - values[2];

        onAngleChanged(rollAngle, pitchAngle, azimuth);

    }

    /**
     * 角度变更后显示到界面
     * @param rollAngle
     * @param pitchAngle
     * @param azimuth
     */
    private void onAngleChanged(float rollAngle, float pitchAngle, float azimuth){

        levelView.setAngle(rollAngle, pitchAngle);

        tvHorz.setText(String.valueOf((int)Math.toDegrees(rollAngle)) + "°");
        tvVert.setText(String.valueOf((int)Math.toDegrees(pitchAngle)) + "°");
    }
}

布局(activity_main.xml)

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000"
    android:paddingBottom="20dp"
    android:paddingTop="50dp">

    <me.kaini.level.LevelView

        android:id="@+id/gv_hv"
        android:layout_width="250dp"
        android:layout_height="250dp"
        android:layout_centerHorizontal="true"
        app:bubbleColor="#e03524"
        app:bubbleRadius="25dp"
        app:bubbleRuleColor="#ffffff"
        app:bubbleRuleRadius="27dp"
        app:bubbleRuleWidth="1dp"
        app:limitCircleWidth="6dp"
        app:limitColor="#e03524"
        app:limitRadius="119dp"
        app:horizontalColor="#00ff00"/>


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:padding="30dp">

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="0.5">

            <TextView
                android:id="@+id/tvv_vertical"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:gravity="center_horizontal"
                android:text="vert"
                android:textColor="#fff"
                android:textSize="40sp" />

            <TextView
                android:id="@+id/tvl_vertical"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/tvv_vertical"
                android:layout_centerHorizontal="true"
                android:text="@string/vertical"
                android:gravity="center_horizontal"
                android:textColor="@android:color/darker_gray"
                android:textSize="20sp" />
        </RelativeLayout>

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_weight="0.5">

            <TextView
                android:id="@+id/tvv_horz"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:gravity="center_horizontal"
                android:text="horiz"
                android:textColor="#fff"
                android:textSize="40sp" />

            <TextView
                android:id="@+id/tvl_horz"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/tvv_horz"
                android:layout_centerHorizontal="true"
                android:gravity="center_horizontal"
                android:text="@string/horizontal"
                android:textColor="@android:color/darker_gray"
                android:textSize="20sp" />
        </RelativeLayout>
    </LinearLayout>

</RelativeLayout>

要点

  1. 如何获取设备传感器的方向角。
  2. 如何将横滚角、俯仰角转为以圆心为座标原点的座标值。
  3. 再将圆心座标转为屏幕座标。
  4. 如何计算超出圆的座标。

源代码传送门:Level

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章