利用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

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