如果运行时出现Program type already present: android.support.v4.app.BackStackRecord$Op错误,参考:https://stackoverflow.com/questions/49917614/program-type-already-present-android-support-v4-app-backstackrecordop
首先膜拜以上大神的博客和github,本人在引用上方github项目时出现了些问题,所以记下来以备以后用到。先讲下改错的过程:本人在Android Studio新建了项目后,就把github上的代码粘贴了过来,然后在manifests删掉以下内容:
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="19" />
然后把github项目中的HeartRate-master\HeartRate-master\libs路径里的jar包复制到了AS模块里的libs路径下(后面看来android-support-v4.jar包用不到),选中后右键添加到库。然后在模块级的build.gradle中修改compileSdkVersion、targetSdkVersion为27(https://www.jianshu.com/p/808e1d127a33)。然后修改了两个implementation 如下:
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
然后修改MainActivity的几个地方如下:
wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen"); ====》
wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "myapp:wakeLock");
然后在运行app前授权允许使用照相机后即可运行app
运行效果图:
模块结构图:
模块级的build.gradle:
1 apply plugin: 'com.android.application'
2
3 android {
4 compileSdkVersion 27
5
6
7
8 defaultConfig {
9 applicationId "com.mingrisoft.heartdetect"
10 minSdkVersion 15
11 targetSdkVersion 27
12 versionCode 1
13 versionName "1.0"
14
15 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
16
17 }
18
19 buildTypes {
20 release {
21 minifyEnabled false
22 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
23 }
24 }
25
26 }
27
28 dependencies {
29 implementation fileTree(include: ['*.jar'], dir: 'libs')
30 implementation 'com.android.support:appcompat-v7:27.1.1'
31 implementation 'com.android.support:support-v4:27.1.1'
32 implementation 'com.android.support.constraint:constraint-layout:1.1.3'
33 testImplementation 'junit:junit:4.12'
34 androidTestImplementation 'com.android.support.test:runner:1.0.2'
35 androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
36 implementation files('libs/achartengine-1.0.0.jar')
37 }
manifests:
1 <?xml version="1.0" encoding="utf-8"?>
2 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3 package="com.mingrisoft.heartdetect">
4
5 <uses-permission android:name="android.permission.WAKE_LOCK" />
6 <uses-permission android:name="android.permission.CAMERA" />
7 <uses-feature android:name="android.hardware.camera" />
8 <uses-feature android:name="android.hardware.camera.autofocus" />
9
10 <application
11 android:allowBackup="true"
12 android:icon="@mipmap/ic_launcher"
13 android:label="@string/app_name"
14 android:roundIcon="@mipmap/ic_launcher_round"
15 android:supportsRtl="true"
16 android:theme="@style/AppTheme">
17 <activity android:name=".MainActivity">
18 <intent-filter>
19 <action android:name="android.intent.action.MAIN" />
20
21 <category android:name="android.intent.category.LAUNCHER" />
22 </intent-filter>
23 </activity>
24 </application>
25
26 </manifest>
strings.xml:
1 <resources>
2 <string name="app_name">heartDetect</string>
3 <string name="hello_world">Hello world!</string>
4 <string name="action_settings">Settings</string>
5 <string name="show">显示</string>
6
7 </resources>
activity_main.xml:
1 <?xml version="1.0" encoding="utf-8"?>
2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:orientation="vertical"
8 tools:context=".MainActivity">
9
10 <SurfaceView
11 android:id="@+id/id_preview"
12 android:layout_width="match_parent"
13 android:layout_height="200dp"
14 android:layout_marginLeft="50dp"
15 android:layout_marginRight="50dp" />
16
17 <LinearLayout
18 android:id="@+id/id_linearLayout_graph"
19 android:layout_width="match_parent"
20 android:layout_height="200dp"
21 android:orientation="vertical" >
22 </LinearLayout>
23
24 <TextView
25 android:id="@+id/id_tv_heart_rate"
26 android:layout_width="wrap_content"
27 android:layout_height="wrap_content"
28 android:layout_marginLeft="50dp"
29 android:layout_weight="1"
30 android:text="@string/show" >
31 </TextView>
32
33 <TextView
34 android:id="@+id/id_tv_Avg_Pixel_Values"
35 android:layout_width="wrap_content"
36 android:layout_height="wrap_content"
37 android:layout_marginLeft="50dp"
38 android:layout_weight="1"
39 android:text="@string/show" >
40 </TextView>
41
42 <TextView
43 android:id="@+id/id_tv_pulse"
44 android:layout_width="wrap_content"
45 android:layout_height="wrap_content"
46 android:layout_marginLeft="50dp"
47 android:layout_weight="1"
48 android:text="@string/show" >
49 </TextView>
50
51
52 </LinearLayout >
ImageProcessing:
1 package com.mingrisoft.heartdetect;
2
3 /**
4 * 图像处理类
5 */
6 public abstract class ImageProcessing {
7
8 /**
9 * 内部调用的处理图片的方法
10 */
11 private static int decodeYUV420SPtoRedSum(byte[] yuv420sp , int width , int height) {
12 if (yuv420sp == null) {
13 return 0;
14 }
15
16 final int frameSize = width * height;
17 int sum = 0;
18
19 for (int j = 0 , yp = 0 ; j < height ; j++) {
20 int uvp = frameSize + (j >> 1) * width;
21 int u = 0;
22 int v = 0;
23 for (int i = 0 ; i < width ; i++, yp++) {
24 int y = (0xff & ((int) yuv420sp[yp])) - 16;
25 if (y < 0) {
26 y = 0;
27 }
28 if ((i & 1) == 0) {
29 v = (0xff & yuv420sp[uvp++]) - 128;
30 u = (0xff & yuv420sp[uvp++]) - 128;
31 }
32 int y1192 = 1192 * y;
33 int r = (y1192 + 1634 * v);
34 int g = (y1192 - 833 * v - 400 * u);
35 int b = (y1192 + 2066 * u);
36
37 if (r < 0) {
38 r = 0;
39 }
40 else if (r > 262143) {
41 r = 262143;
42 }
43
44 if (g < 0) {
45 g = 0;
46 }
47 else if (g > 262143) {
48 g = 262143;
49 }
50
51 if (b < 0) {
52 b = 0;
53 }
54 else if (b > 262143) {
55 b = 262143;
56 }
57
58 int pixel = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
59 int red = (pixel >> 16) & 0xff;
60 sum += red;
61 }
62 }
63 return sum;
64 }
65
66 /**
67 * 对外开放的图像处理方法
68 */
69 public static int decodeYUV420SPtoRedAvg(byte[] yuv420sp , int width , int height) {
70 if (yuv420sp == null) {
71 return 0;
72 }
73 final int frameSize = width * height;
74 int sum = decodeYUV420SPtoRedSum(yuv420sp, width, height);
75 return (sum / frameSize);
76 }
77 }
MainActivity:
1 package com.mingrisoft.heartdetect;
2
3 import android.app.Activity;
4 import android.content.Context;
5 import android.content.res.Configuration;
6 import android.graphics.Color;
7 import android.graphics.Paint.Align;
8 import android.hardware.Camera;
9 import android.hardware.Camera.PreviewCallback;
10 import android.os.Bundle;
11 import android.os.Handler;
12 import android.os.Message;
13 import android.os.PowerManager;
14 import android.os.PowerManager.WakeLock;
15 import android.util.Log;
16 import android.view.SurfaceHolder;
17 import android.view.SurfaceView;
18 import android.view.ViewGroup.LayoutParams;
19 import android.widget.LinearLayout;
20 import android.widget.TextView;
21 import android.widget.Toast;
22
23 import org.achartengine.ChartFactory;
24 import org.achartengine.GraphicalView;
25 import org.achartengine.chart.PointStyle;
26 import org.achartengine.model.XYMultipleSeriesDataset;
27 import org.achartengine.model.XYSeries;
28 import org.achartengine.renderer.XYMultipleSeriesRenderer;
29 import org.achartengine.renderer.XYSeriesRenderer;
30
31 import java.util.Timer;
32 import java.util.TimerTask;
33 import java.util.concurrent.atomic.AtomicBoolean;
34
35 /**
36 * 程序的主入口
37 */
38 public class MainActivity extends Activity {
39 //曲线
40 private Timer timer = new Timer();
41 //Timer任务,与Timer配套使用
42 private TimerTask task;
43 private static int gx;
44 private static int j;
45
46 private static double flag = 1;
47 private Handler handler;
48 private String title = "pulse";
49 private XYSeries series;
50 private XYMultipleSeriesDataset mDataset;
51 private GraphicalView chart;
52 private XYMultipleSeriesRenderer renderer;
53 private Context context;
54 private int addX = -1;
55 double addY;
56 int[] xv = new int[300];
57 int[] yv = new int[300];
58 int[] hua=new int[]{9,10,11,12,13,14,13,12,11,10,9,8,7,6,7,8,9,10,11,10,10};
59
60 private static final AtomicBoolean processing = new AtomicBoolean(false);
61 //Android手机预览控件
62 private static SurfaceView preview = null;
63 //预览设置信息
64 private static SurfaceHolder previewHolder = null;
65 //Android手机相机句柄
66 private static Camera camera = null;
67 //private static View image = null;
68 private static TextView mTV_Heart_Rate = null;
69 private static TextView mTV_Avg_Pixel_Values = null;
70 private static TextView mTV_pulse = null;
71 private static WakeLock wakeLock = null;
72 private static int averageIndex = 0;
73 private static final int averageArraySize = 4;
74 private static final int[] averageArray = new int[averageArraySize];
75
76 /**
77 * 类型枚举
78 */
79 public static enum TYPE {
80 GREEN, RED
81 };
82
83 //设置默认类型
84 private static TYPE currentType = TYPE.GREEN;
85 //获取当前类型
86 public static TYPE getCurrent() {
87 return currentType;
88 }
89 //心跳下标值
90 private static int beatsIndex = 0;
91 //心跳数组的大小
92 private static final int beatsArraySize = 3;
93 //心跳数组
94 private static final int[] beatsArray = new int[beatsArraySize];
95 //心跳脉冲
96 private static double beats = 0;
97 //开始时间
98 private static long startTime = 0;
99
100
101
102 @Override
103 public void onCreate(Bundle savedInstanceState) {
104 super.onCreate(savedInstanceState);
105 setContentView(R.layout.activity_main); //这里注意要改成自己的布局文件
106
107 initConfig();
108 }
109
110 /**
111 * 初始化配置
112 */
113 @SuppressWarnings("deprecation")
114 private void initConfig() {
115 //曲线
116 context = getApplicationContext();
117
118 //这里获得main界面上的布局,下面会把图表画在这个布局里面
119 LinearLayout layout = (LinearLayout)findViewById(R.id.id_linearLayout_graph);
120
121 //这个类用来放置曲线上的所有点,是一个点的集合,根据这些点画出曲线
122 series = new XYSeries(title);
123
124 //创建一个数据集的实例,这个数据集将被用来创建图表
125 mDataset = new XYMultipleSeriesDataset();
126
127 //将点集添加到这个数据集中
128 mDataset.addSeries(series);
129
130 //以下都是曲线的样式和属性等等的设置,renderer相当于一个用来给图表做渲染的句柄
131 int color = Color.GREEN;
132 PointStyle style = PointStyle.CIRCLE;
133 renderer = buildRenderer(color, style, true);
134
135 //设置好图表的样式
136 setChartSettings(renderer, "X", "Y", 0, 300, 4, 16, Color.WHITE, Color.WHITE);
137
138 //生成图表
139 chart = ChartFactory.getLineChartView(context, mDataset, renderer);
140
141 //将图表添加到布局中去
142 layout.addView(chart, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
143
144 //这里的Handler实例将配合下面的Timer实例,完成定时更新图表的功能
145 handler = new Handler() {
146 @Override
147 public void handleMessage(Message msg) {
148 //刷新图表
149 updateChart();
150 super.handleMessage(msg);
151 }
152 };
153
154 task = new TimerTask() {
155 @Override
156 public void run() {
157 Message message = new Message();
158 message.what = 1;
159 handler.sendMessage(message);
160 }
161 };
162
163 timer.schedule(task, 1,20); //曲线
164 //获取SurfaceView控件
165 preview = (SurfaceView) findViewById(R.id.id_preview);
166 previewHolder = preview.getHolder();
167 previewHolder.addCallback(surfaceCallback);
168 previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
169
170 mTV_Heart_Rate = (TextView) findViewById(R.id.id_tv_heart_rate);
171 mTV_Avg_Pixel_Values = (TextView) findViewById(R.id.id_tv_Avg_Pixel_Values);
172 mTV_pulse = (TextView) findViewById(R.id.id_tv_pulse);
173
174 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
175 wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "myapp:wakeLock");
176 }
177
178 // 曲线
179 @Override
180 public void onDestroy() {
181 //当结束程序时关掉Timer
182 timer.cancel();
183 super.onDestroy();
184 };
185
186 /**
187 * 创建图表
188 */
189 protected XYMultipleSeriesRenderer buildRenderer(int color, PointStyle style, boolean fill) {
190 XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
191
192 //设置图表中曲线本身的样式,包括颜色、点的大小以及线的粗细等
193 XYSeriesRenderer r = new XYSeriesRenderer();
194 r.setColor(Color.RED);
195 r.setLineWidth(1);
196 renderer.addSeriesRenderer(r);
197 return renderer;
198 }
199
200 /**
201 * 设置图标的样式
202 * @param renderer
203 * @param xTitle:x标题
204 * @param yTitle:y标题
205 * @param xMin:x最小长度
206 * @param xMax:x最大长度
207 * @param yMin:y最小长度
208 * @param yMax:y最大长度
209 * @param axesColor:颜色
210 * @param labelsColor:标签
211 */
212 protected void setChartSettings(XYMultipleSeriesRenderer renderer, String xTitle, String yTitle,
213 double xMin, double xMax, double yMin, double yMax, int axesColor, int labelsColor) {
214 //有关对图表的渲染可参看api文档
215 renderer.setChartTitle(title);
216 renderer.setXTitle(xTitle);
217 renderer.setYTitle(yTitle);
218 renderer.setXAxisMin(xMin);
219 renderer.setXAxisMax(xMax);
220 renderer.setYAxisMin(yMin);
221 renderer.setYAxisMax(yMax);
222 renderer.setAxesColor(axesColor);
223 renderer.setLabelsColor(labelsColor);
224 renderer.setShowGrid(true);
225 renderer.setGridColor(Color.GREEN);
226 renderer.setXLabels(20);
227 renderer.setYLabels(10);
228 renderer.setXTitle("Time");
229 renderer.setYTitle("mmHg");
230 renderer.setYLabelsAlign(Align.RIGHT);
231 renderer.setPointSize((float) 3 );
232 renderer.setShowLegend(false);
233 }
234
235 /**
236 * 更新图标信息
237 */
238 private void updateChart() {
239 //设置好下一个需要增加的节点
240 if(flag == 1) {
241 addY = 10;
242 }
243 else {
244 flag = 1;
245 if(gx < 200){
246 if(hua[20] > 1){
247 Toast.makeText(MainActivity.this, "请用您的指尖盖住摄像头镜头!", Toast.LENGTH_SHORT).show();
248 hua[20] = 0;
249 }
250 hua[20]++;
251 return;
252 }
253 else {
254 hua[20] = 10;
255 }
256 j = 0;
257 }
258 if(j < 20){
259 addY=hua[j];
260 j++;
261 }
262
263 //移除数据集中旧的点集
264 mDataset.removeSeries(series);
265
266 //判断当前点集中到底有多少点,因为屏幕总共只能容纳100个,所以当点数超过100时,长度永远是100
267 int length = series.getItemCount();
268 int bz = 0;
269 //addX = length;
270 if (length > 300) {
271 length = 300;
272 bz=1;
273 }
274 addX = length;
275 //将旧的点集中x和y的数值取出来放入backup中,并且将x的值加1,造成曲线向右平移的效果
276 for (int i = 0; i < length; i++) {
277 xv[i] = (int) series.getX(i) - bz;
278 yv[i] = (int) series.getY(i);
279 }
280
281 //点集先清空,为了做成新的点集而准备
282 series.clear();
283 mDataset.addSeries(series);
284 //将新产生的点首先加入到点集中,然后在循环体中将坐标变换后的一系列点都重新加入到点集中
285 //这里可以试验一下把顺序颠倒过来是什么效果,即先运行循环体,再添加新产生的点
286 series.add(addX, addY);
287 for (int k = 0; k < length; k++) {
288 series.add(xv[k], yv[k]);
289 }
290 //在数据集中添加新的点集
291 //mDataset.addSeries(series);
292
293 //视图更新,没有这一步,曲线不会呈现动态
294 //如果在非UI主线程中,需要调用postInvalidate(),具体参考api
295 chart.invalidate();
296 } //曲线
297
298
299 @Override
300 public void onConfigurationChanged(Configuration newConfig) {
301 super.onConfigurationChanged(newConfig);
302 }
303
304 @Override
305 public void onResume() {
306 super.onResume();
307 wakeLock.acquire();
308 camera = Camera.open();
309 startTime = System.currentTimeMillis();
310 }
311
312 @Override
313 public void onPause() {
314 super.onPause();
315 wakeLock.release();
316 camera.setPreviewCallback(null);
317 camera.stopPreview();
318 camera.release();
319 camera = null;
320 }
321
322
323 /**
324 * 相机预览方法
325 * 这个方法中实现动态更新界面UI的功能,
326 * 通过获取手机摄像头的参数来实时动态计算平均像素值、脉冲数,从而实时动态计算心率值。
327 */
328 private static PreviewCallback previewCallback = new PreviewCallback() {
329 public void onPreviewFrame(byte[] data, Camera cam) {
330 if (data == null) {
331 throw new NullPointerException();
332 }
333 Camera.Size size = cam.getParameters().getPreviewSize();
334 if (size == null) {
335 throw new NullPointerException();
336 }
337 if (!processing.compareAndSet(false, true)) {
338 return;
339 }
340 int width = size.width;
341 int height = size.height;
342
343 //图像处理
344 int imgAvg = ImageProcessing.decodeYUV420SPtoRedAvg(data.clone(),height,width);
345 gx = imgAvg;
346 mTV_Avg_Pixel_Values.setText("平均像素值是" + String.valueOf(imgAvg));
347
348 if (imgAvg == 0 || imgAvg == 255) {
349 processing.set(false);
350 return;
351 }
352 //计算平均值
353 int averageArrayAvg = 0;
354 int averageArrayCnt = 0;
355 for (int i = 0; i < averageArray.length; i++) {
356 if (averageArray[i] > 0) {
357 averageArrayAvg += averageArray[i];
358 averageArrayCnt++;
359 }
360 }
361
362 //计算平均值
363 int rollingAverage = (averageArrayCnt > 0)?(averageArrayAvg/averageArrayCnt):0;
364 TYPE newType = currentType;
365 if (imgAvg < rollingAverage) {
366 newType = TYPE.RED;
367 if (newType != currentType) {
368 beats++;
369 flag=0;
370 mTV_pulse.setText("脉冲数是" + String.valueOf(beats));
371 }
372 } else if (imgAvg > rollingAverage) {
373 newType = TYPE.GREEN;
374 }
375
376 if(averageIndex == averageArraySize) {
377 averageIndex = 0;
378 }
379 averageArray[averageIndex] = imgAvg;
380 averageIndex++;
381
382 if (newType != currentType) {
383 currentType = newType;
384 }
385
386 //获取系统结束时间(ms)
387 long endTime = System.currentTimeMillis();
388 double totalTimeInSecs = (endTime - startTime) / 1000d;
389 if (totalTimeInSecs >= 2) {
390 double bps = (beats / totalTimeInSecs);
391 int dpm = (int) (bps * 60d);
392 if (dpm < 30 || dpm > 180|| imgAvg < 200) {
393 //获取系统开始时间(ms)
394 startTime = System.currentTimeMillis();
395 //beats心跳总数
396 beats = 0;
397 processing.set(false);
398 return;
399 }
400
401 if(beatsIndex == beatsArraySize) {
402 beatsIndex = 0;
403 }
404 beatsArray[beatsIndex] = dpm;
405 beatsIndex++;
406
407 int beatsArrayAvg = 0;
408 int beatsArrayCnt = 0;
409 for (int i = 0; i < beatsArray.length; i++) {
410 if (beatsArray[i] > 0) {
411 beatsArrayAvg += beatsArray[i];
412 beatsArrayCnt++;
413 }
414 }
415 int beatsAvg = (beatsArrayAvg / beatsArrayCnt);
416 mTV_Heart_Rate.setText("您的心率是"+String.valueOf(beatsAvg) +
417 " 值:" + String.valueOf(beatsArray.length) +
418 " " + String.valueOf(beatsIndex) +
419 " " + String.valueOf(beatsArrayAvg) +
420 " " + String.valueOf(beatsArrayCnt));
421 //获取系统时间(ms)
422 startTime = System.currentTimeMillis();
423 beats = 0;
424 }
425 processing.set(false);
426 }
427 };
428
429 /**
430 * 预览回调接口
431 */
432 private static SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {
433 //创建时调用
434 @Override
435 public void surfaceCreated(SurfaceHolder holder) {
436 try {
437 camera.setPreviewDisplay(previewHolder);
438 camera.setPreviewCallback(previewCallback);
439 } catch (Throwable t) {
440 Log.e("PreviewDemo","Exception in setPreviewDisplay()", t);
441 }
442 }
443
444 //当预览改变的时候回调此方法
445 @Override
446 public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) {
447 Camera.Parameters parameters = camera.getParameters();
448 parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
449 Camera.Size size = getSmallestPreviewSize(width, height, parameters);
450 if (size != null) {
451 parameters.setPreviewSize(size.width, size.height);
452 }
453 camera.setParameters(parameters);
454 camera.startPreview();
455 }
456
457 //销毁的时候调用
458 @Override
459 public void surfaceDestroyed(SurfaceHolder holder) {
460
461 }
462 };
463
464 /**
465 * 获取相机最小的预览尺寸
466 */
467 private static Camera.Size getSmallestPreviewSize(int width, int height, Camera.Parameters parameters) {
468 Camera.Size result = null;
469 for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
470 if (size.width <= width && size.height <= height) {
471 if (result == null) {
472 result = size;
473 }
474 else {
475 int resultArea = result.width * result.height;
476 int newArea = size.width * size.height;
477 if (newArea < resultArea) {
478 result = size;
479 }
480 }
481 }
482 }
483 return result;
484 }
485 }