快過年了,大家都回家了,我還在發博客,是不是很有心呢!
其實是今天最後一篇博文,給明年開個好頭吧。下面上乾貨。
通過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>
要點
- 如何獲取設備傳感器的方向角。
- 如何將橫滾角、俯仰角轉爲以圓心爲座標原點的座標值。
- 再將圓心座標轉爲屏幕座標。
- 如何計算超出圓的座標。