描述
android计步器的实现,自定义的一个弧形进度条,记步通过手机的传感器来实现,也就是说不支持传感器的机子(应该很老的了吧)就没有效果。看看效果图:
这里写图片描述这里写图片描述
自定义View
public class StepView extends View {
/**
* 圆弧的宽度
*/
private float borderWidth = dipToPx(10);
/**
* 画步数的数值的字体大小
*/
private float numberTextSize = 0;
/**
* 步数
*/
private String stepNumber = "0";
/**
* 开始绘制圆弧的角度
*/
private float startAngle = 125;
/**
* 终点对应的角度和起始点对应的角度的夹角
*/
private float angleLength = 290;
/**
* 所要绘制的当前步数的蓝色圆弧终点到起点的夹角
*/
private float currentAngleLength = 0;
/**
* 动画时长
*/
private int animationLength = 3000;
/**
* 当前运动类型
*/
private String type = "Riding";
/**
* 当前活跃等级
*/
private String level = "等级:轻度活跃";
/**
* 步数上方文字
*/
private String today = "今日步数";
/**
* 单位km是否显示
*/
private String unit = "Km";
public StepView(Context context) {
super(context);
}
public StepView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public StepView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**中心点的x坐标*/
float centerX = (getWidth()) / 2;
/**指定圆弧的外轮廓矩形区域*/
RectF rectF = new RectF(0 + borderWidth, borderWidth, 2 * centerX - borderWidth, 2 * centerX - borderWidth);
/**【第一步】绘制整体的灰色圆弧*/
drawArcYellow(canvas, rectF);
/**【第二步】绘制当前进度的蓝色圆弧*/
drawArcRed(canvas, rectF);
/**【第三步】绘制当前进度的白色数字*/
drawTextNumber(canvas, centerX);
/**【第四步】绘制"本次步数"的灰色文字*/
drawTextStepString(canvas, centerX);
/**【第五步】绘制当前记步类型*/
drawTextType(canvas, centerX);
/**【第六步】绘制当前等级类型*/
drawTextLevel(canvas, centerX);
/**【第七步】绘制骑行距离单位*/
drawTextUnit(canvas, centerX);
}
/**
* 1.绘制总步数的灰色圆弧
*
* @param canvas 画笔
* @param rectF 参考的矩形
*/
private void drawArcYellow(Canvas canvas, RectF rectF) {
Paint paint = new Paint();
/** 默认画笔颜色,灰色 */
paint.setColor(getResources().getColor(R.color.near_black));
/** 结合处为圆弧*/
paint.setStrokeJoin(Paint.Join.MITER);
/** 设置画笔的样式 Paint.Cap.Round ,Cap.SQUARE等分别为圆形、方形*/
paint.setStrokeCap(Paint.Cap.BUTT);
/** 设置画笔的填充样式 Paint.Style.FILL :填充内部;Paint.Style.FILL_AND_STROKE :填充内部和描边; Paint.Style.STROKE :仅描边*/
paint.setStyle(Paint.Style.STROKE);
float[] floats = {4,16,4,16};
paint.setPathEffect(new DashPathEffect(floats, 0));
/**抗锯齿功能*/
paint.setAntiAlias(true);
/**设置画笔宽度*/
paint.setStrokeWidth(borderWidth);
/**绘制圆弧的方法
* drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)//画弧,
参数一是RectF对象,一个矩形区域椭圆形的界限用于定义在形状、大小、电弧,
参数二是起始角(度)在电弧的开始,圆弧起始角度,单位为度。
参数三圆弧扫过的角度,顺时针方向,单位为度,从右中间开始为零度。
参数四是如果这是true(真)的话,在绘制圆弧时将圆心包括在内,通常用来绘制扇形;如果它是false(假)这将是一个弧线,
参数五是Paint对象;
*/
canvas.drawArc(rectF, startAngle, angleLength, false, paint);
}
/**
* 2.绘制当前步数的蓝色圆弧
*/
private void drawArcRed(Canvas canvas, RectF rectF) {
Paint paintCurrent = new Paint();
paintCurrent.setStrokeJoin(Paint.Join.MITER);
paintCurrent.setStrokeCap(Paint.Cap.BUTT);//圆角弧度
paintCurrent.setStyle(Paint.Style.STROKE);//设置填充样式
paintCurrent.setAntiAlias(true);//抗锯齿功能
paintCurrent.setStrokeWidth(borderWidth);//设置画笔宽度
paintCurrent.setColor(getResources().getColor(R.color.colorPrimary));//设置画笔颜色
float[] floats = {4,16,4,16};
paintCurrent.setPathEffect(new DashPathEffect(floats, 0));
canvas.drawArc(rectF, startAngle, currentAngleLength, false, paintCurrent);
}
/**
* 3.圆环中心的步数
*/
private void drawTextNumber(Canvas canvas, float centerX) {
Paint vTextPaint = new Paint();
vTextPaint.setTextAlign(Paint.Align.CENTER);
vTextPaint.setAntiAlias(true);//抗锯齿功能
vTextPaint.setTextSize(numberTextSize);
Typeface font = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
vTextPaint.setTypeface(font);//字体风格
vTextPaint.setColor(getResources().getColor(R.color.white));
Rect bounds_Number = new Rect();
vTextPaint.getTextBounds(stepNumber, 0, stepNumber.length(), bounds_Number);
canvas.drawText(stepNumber, centerX, getHeight() / 2 + bounds_Number.height() / 2, vTextPaint);
}
/**
* 4.圆环中心[本次步数]的文字
*/
private void drawTextStepString(Canvas canvas, float centerX) {
Paint vTextPaint = new Paint();
vTextPaint.setTextSize(dipToPx(16));
vTextPaint.setTextAlign(Paint.Align.CENTER);
vTextPaint.setAntiAlias(true);//抗锯齿功能
vTextPaint.setColor(getResources().getColor(R.color.gray));
Rect bounds = new Rect();
vTextPaint.getTextBounds(today, 0, today.length(), bounds);
canvas.drawText(today, centerX, getHeight() / 2 + bounds.height() - 2 * getFontHeight(numberTextSize), vTextPaint);
}
/**
* 5.圆环中下[Walking]等文字
*/
private void drawTextType(Canvas canvas, float centerX) {
Paint mTypePaint = new Paint();
mTypePaint.setTextSize(dipToPx(22));
mTypePaint.setTextAlign(Paint.Align.CENTER);
mTypePaint.setAntiAlias(true);
mTypePaint.setColor(getResources().getColor(R.color.text_blue));
Rect bounds = new Rect();
mTypePaint.getTextBounds(type, 0, type.length(), bounds);
canvas.drawText(type, centerX, getHeight() / 2 + 2 * bounds.height() + getFontHeight(numberTextSize), mTypePaint);
}
/**
* 6.绘制圆环下方等级
*/
private void drawTextLevel(Canvas canvas, float centerX) {
Paint mLevelPaint = new Paint();
mLevelPaint.setTextSize(dipToPx(12));
mLevelPaint.setTextAlign(Paint.Align.CENTER);
mLevelPaint.setAntiAlias(true);
mLevelPaint.setColor(getResources().getColor(R.color.input_hint_gray));
Rect bounds = new Rect();
mLevelPaint.getTextBounds(level, 0, level.length(), bounds);
canvas.drawText(level, centerX, getHeight() / 2 + 2 * bounds.height() + 2 * getFontHeight(numberTextSize), mLevelPaint);
}
/**
* 7.绘制骑行单位km
*/
private void drawTextUnit(Canvas canvas, float centerX) {
Paint mUnitPaint = new Paint();
mUnitPaint.setTextSize(dipToPx(16));
mUnitPaint.setTextAlign(Paint.Align.CENTER);
mUnitPaint.setAntiAlias(true);
mUnitPaint.setColor(getResources().getColor(R.color.input_hint_gray));
Rect bounds = new Rect();
mUnitPaint.getTextBounds(unit, 0, unit.length(), bounds);
canvas.drawText(unit, centerX+ stepNumber.length()*80, getHeight() / 2 + bounds.height() * 3 / 2, mUnitPaint);
}
/**
* 获取当前步数的数字的高度
*
* @param fontSize 字体大小
* @return 字体高度
*/
public int getFontHeight(float fontSize) {
Paint paint = new Paint();
paint.setTextSize(fontSize);
Rect bounds_Number = new Rect();
paint.getTextBounds(stepNumber, 0, stepNumber.length(), bounds_Number);
return bounds_Number.height();
}
/**
* dip 转换成px
*
* @param dip
* @return
*/
private int dipToPx(float dip) {
float density = getContext().getResources().getDisplayMetrics().density;
return (int) (dip * density + 0.5f * (dip >= 0 ? 1 : -1));
}
/**
* 所走的步数进度
* @param totalStepNum 设置的步数
* @param currentCounts 所走步数
*/
public void setCurrentCount(int totalStepNum, int currentCounts) {
/**如果当前走的步数超过总步数则圆弧还是270度,不能成为园*/
if (currentCounts > totalStepNum) {
currentCounts = totalStepNum;
}
/**上次所走步数占用总共步数的百分比*/
float scalePrevious = (float) Integer.valueOf(stepNumber) / totalStepNum;
/**换算成弧度最后要到达的角度的长度-->弧长*/
float previousAngleLength = scalePrevious * angleLength;
/**所走步数占用总共步数的百分比*/
float scale = (float) currentCounts / totalStepNum;
/**换算成弧度最后要到达的角度的长度-->弧长*/
float currentAngleLength = scale * angleLength;
/**开始执行动画*/
setAnimation(previousAngleLength, currentAngleLength, animationLength);
stepNumber = String.valueOf(currentCounts);
setTextSize(currentCounts);
}
/**
* 设置各个参数
*/
public void setParams(String today, String unit, String type, String level) {
this.today = today;
this.unit = unit;
this.type = type;
this.level = level;
}
/**
* 为进度设置动画
* ValueAnimator是整个属性动画机制当中最核心的一个类,属性动画的运行机制是通过不断地对值进行操作来实现的,
* 而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。
* 它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,
* 我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,
* 那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。
*
* @param start 初始值
* @param current 结束值
* @param length 动画时长
*/
private void setAnimation(float start, float current, int length) {
ValueAnimator progressAnimator = ValueAnimator.ofFloat(start, current);
progressAnimator.setDuration(length);
progressAnimator.setTarget(currentAngleLength);
progressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
/**每次在初始值和结束值之间产生的一个平滑过渡的值,逐步去更新进度*/
currentAngleLength = (float) animation.getAnimatedValue();
invalidate();
}
});
progressAnimator.start();
}
/**
* 设置文本大小,防止步数特别大之后放不下,将字体大小动态设置
*
* @param num
*/
public void setTextSize(int num) {
String s = String.valueOf(num);
int length = s.length();
if (length 4 && length 6 && length 8) {
numberTextSize = dipToPx(25);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
整个View的代码逻辑还是比较清晰的,自定义的view会首先调用onDraw()方法,获取了整个布局的中心和轮廓,然后开始七步分别绘制控件,这就是大家可以发挥的部分了,想怎么设计怎么设计。
第一步着重看一下,这个虚线的实现:
float[] floats = {4,16,4,16};
paint.setPathEffect(new DashPathEffect(floats, 0));
1
2
floats的四个参数意思是{画宽度,间隔宽度,画宽度,间隔宽度},这里的floats是数组也就是说你可以把每一个宽度和间隔都写出来,当然如果一样的话你可以只列出两个,系统会自动识别来延续后面的宽度和间隔。
因为注释很清楚,其他的这里就不详述了,想改啥基本都能看明白。
记步服务
public class StepService extends Service implements SensorEventListener {
private String TAG = "StepService";
/**
* 默认为10秒进行一次存储
*/
private static int duration = 10 * 1000;
/**
* 传感器管理对象
*/
private SensorManager sensorManager;
/**
* 保存记步计时器
*/
private TimeCount time;
/**
* 当前所走的步数
*/
private int CURRENT_STEP;
/**
* 计步传感器类型 Sensor.TYPE_STEP_COUNTER或者Sensor.TYPE_STEP_DETECTOR
*/
private static int stepSensorType = -1;
/**
* 每次第一次启动记步服务时是否从系统中获取了已有的步数记录
*/
private boolean hasRecord = false;
/**
* 系统中获取到的已有的步数
*/
private int hasStepCount = 0;
/**
* 上一次的步数
*/
private int previousStepCount = 0;
/**
* IBinder对象,向Activity传递数据的桥梁
*/
private StepBinder stepBinder = new StepBinder();
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate()");
new Thread(new Runnable() {
public void run() {
startStepDetector();
}
}).start();
startTimeCount();
}
/**
* 开始保存记步数据
*/
private void startTimeCount() {
if (time == null) {
time = new TimeCount(duration, 1000);
}
time.start();
}
/**
* UI监听器对象
*/
private UpdateUiCallBack mCallback;
/**
* 注册UI更新监听
*
* @param paramICallback
*/
public void registerCallback(UpdateUiCallBack paramICallback) {
this.mCallback = paramICallback;
}
@Override
public IBinder onBind(Intent intent) {
return stepBinder;
}
/**
* 向Activity传递数据的纽带
*/
public class StepBinder extends Binder {
/**
* 获取当前service对象
*
* @return StepService
*/
public StepService getService() {
return StepService.this;
}
}
/**
* 获取当前步数
*
* @return
*/
public int getStepCount() {
return CURRENT_STEP;
}
@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
/**
* 获取传感器实例
*/
private void startStepDetector() {
if (sensorManager != null) {
sensorManager = null;
}
// 获取传感器管理器的实例
sensorManager = (SensorManager) this
.getSystemService(SENSOR_SERVICE);
//android4.4以后可以使用计步传感器
addCountStepListener();
}
/**
* 添加传感器监听
* 1. TYPE_STEP_COUNTER API的解释说返回从开机被激活后统计的步数,当重启手机后该数据归零,
* 该传感器是一个硬件传感器所以它是低功耗的。
* 为了能持续的计步,请不要反注册事件,就算手机处于休眠状态它依然会计步。
* 当激活的时候依然会上报步数。该sensor适合在长时间的计步需求。
*
* 2.TYPE_STEP_DETECTOR翻译过来就是走路检测,
* API文档也确实是这样说的,该sensor只用来监监测走步,每次返回数字1.0。
* 如果需要长事件的计步请使用TYPE_STEP_COUNTER。
*/
private void addCountStepListener() {
Sensor countSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
Sensor detectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR);
if (countSensor != null) {
stepSensorType = Sensor.TYPE_STEP_COUNTER;
Log.v(TAG, "Sensor.TYPE_STEP_COUNTER");
sensorManager.registerListener(StepService.this, countSensor, SensorManager.SENSOR_DELAY_NORMAL);
} else if (detectorSensor != null) {
stepSensorType = Sensor.TYPE_STEP_DETECTOR;
Log.v(TAG, "Sensor.TYPE_STEP_DETECTOR");
sensorManager.registerListener(StepService.this, detectorSensor, SensorManager.SENSOR_DELAY_NORMAL);
}
}
/**
* 传感器监听回调
* @param event
*/
@Override
public void onSensorChanged(SensorEvent event) {
if (stepSensorType == Sensor.TYPE_STEP_COUNTER) {
//获取当前传感器返回的临时步数
int tempStep = (int) event.values[0];
//首次如果没有获取手机系统中已有的步数则获取一次系统中APP还未开始记步的步数
if (!hasRecord) {
hasRecord = true;
hasStepCount = tempStep;
} else {
//获取APP打开到现在的总步数=本次系统回调的总步数-APP打开之前已有的步数
int thisStepCount = tempStep - hasStepCount;
//本次有效步数=(APP打开后所记录的总步数-上一次APP打开后所记录的总步数)
int thisStep = thisStepCount - previousStepCount;
//总步数=现有的步数+本次有效步数
CURRENT_STEP += (thisStep);
//记录最后一次APP打开到现在的总步数
previousStepCount = thisStepCount;
}
} else if (stepSensorType == Sensor.TYPE_STEP_DETECTOR) {
if (event.values[0] == 1.0) {
CURRENT_STEP++;
}
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
/**
* 保存记步数据
*/
class TimeCount extends CountDownTimer {
public TimeCount(long millisInFuture, long countDownInterval) {
super(millisInFuture, countDownInterval);
}
@Override
public void onFinish() {
// 如果计时器正常结束,则开始计步
time.cancel();
mCallback.updateUi(CURRENT_STEP);
startTimeCount();
}
@Override
public void onTick(long millisUntilFinished) {
}
}
@Override
public void onDestroy() {
super.onDestroy();
//取消前台进程
stopForeground(true);
}
@Override
public boolean onUnbind(Intent intent) {
return super.onUnbind(intent);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
因为记步是在后台进行的,开启之后我们可以干其他事情,所以采用服务的方式,而我们需要获取记步的数据,所以要通过绑定的方式来注册服务,这样才能获得通信用的connection。逻辑思路是,通过计步器来获取步数,然后设置监听器来监听步数的改变,如果步数改变更新步数的变量,然后开启一个计时器来定时去记录,设置回调参数来返回步数。回调接口只有一个方法:
public interface UpdateUiCallBack {
/**
* 更新UI步数
*
* @param stepCount 步数
*/
void updateUi(int stepCount);
}
1
2
3
4
5
6
7
8
当然别忘了在AndroidManifest.xml中注册Service:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
还有记步需要用的权限:
1
2
3
调用MainActivity
public class MainActivity extends AppCompatActivity {
private StepView stepView;
private Button button;
private boolean isBind = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initData();
}
private void initView() {
stepView = (StepView) findViewById(R.id.step_walk_arv);
button = (Button) findViewById(R.id.begin_btn);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
begin();
}
});
}
private void initData() {
stepView.setParams("今日步数", "", "Walking", "等级:轻度活跃");
stepView.setCurrentCount(1000,0);
}
private void begin() {
if (isBind == false){
//开启记步
Intent intent = new Intent(this, StepService.class);
isBind = bindService(intent, conn, Context.BIND_AUTO_CREATE);
startService(intent);
button.setText("正在记步");
}
}
/**
* 用于查询应用服务(application Service)的状态的一种interface,
* 更详细的信息可以参考Service 和 context.bindService()中的描述,
* 和许多来自系统的回调方式一样,ServiceConnection的方法都是进程的主线程中调用的。
*/
ServiceConnection conn = new ServiceConnection() {
/**
* 在建立起于Service的连接时会调用该方法,目前Android是通过IBind机制实现与服务的连接。
* @param name 实际所连接到的Service组件名称
* @param service 服务的通信信道的IBind,可以通过Service访问对应服务
*/
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
StepService stepService = ((StepService.StepBinder) service).getService();
//设置初始化数据
stepView.setCurrentCount(1000, stepService.getStepCount());
//设置步数监听回调
stepService.registerCallback(new UpdateUiCallBack() {
@Override
public void updateUi(int stepCount) {
stepView.setCurrentCount(1000, stepCount);
}
});
}
/**
* 当与Service之间的连接丢失的时候会调用该方法,
* 这种情况经常发生在Service所在的进程崩溃或者被Kill的时候调用,
* 此方法不会移除与Service的连接,当服务重新启动的时候仍然会调用 onServiceConnected()。
* @param name 丢失连接的组件名称
*/
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
//服务解绑
@Override
public void onDestroy() {
super.onDestroy();
if (isBind) {
unbindService(conn);
isBind = false;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
调用也很简单,首先在布局文件中引用StepView,然后初始化,设置圆弧里面的参数,设置总步数和当前步数(开始当然为0):
stepView.setParams("今日步数", "", "Walking", "等级:轻度活跃");
stepView.setCurrentCount(1000,0);
1
2
其中还设置了一个boolean值isBind区分是否已经启动服务并绑定到了activity上,以便在onDestroy()方法是解除绑定。
整个计步器的实现还是相对简单的,之前还想过要在圆弧圈外面画一个手指来指向当前的进度,不过暂时还没想通怎么实现,有兴趣的小伙伴研究之后告诉一声。