iOS跑步軟件開發(fā)-從無到有


前言

經(jīng)過兩個多月的開發(fā)與調(diào)試,全民星跑1.0.1終于上線了,首先要感謝曲總洛洛愛吃肉的技術(shù)支持.全民星跑作為一個以跑步計步為主要功能的軟件,騷棟在開發(fā)過程中實在是遇到了不少的坑,這篇博客會分為加速儀計步和跑步計步兩個模塊來說明,不過有一點我想先聲明,因為人力資源有限,所以可能在計步的邏輯上跟不上咕咚或者是Keep這些大廠,望各位看官見諒 . ?? ?? ??


功能規(guī)劃

一個App如何統(tǒng)計一個人的運動?這里主要有兩種方式,一種是使用陀螺儀(或是加速儀)獲取手機各個方向的加速度來統(tǒng)計用戶的運動,另外一種就是通過GPS定位地圖來統(tǒng)計用戶的運動.在我的做的應(yīng)用里面也是兩種方案都采用了.接下來,我們分別講解每一種方式是如何使用的.


陀螺儀簡介以及原始數(shù)據(jù)獲取

陀螺儀又叫角速度傳感器,是不同于加速度計(G-sensor)的,他的測量物理量是偏轉(zhuǎn)、傾斜時的轉(zhuǎn)動角速度。在手機上,僅用加速度計沒辦法測量或重構(gòu)出完整的3D動作,測不到轉(zhuǎn)動的動作的,G-sensor只能檢測軸向的線性動作。但陀螺儀則可以對轉(zhuǎn)動、偏轉(zhuǎn)的動作做很好的測量,這樣就可以精確分析判斷出使用者的實際動作。而后根據(jù)動作,可以對手機做相應(yīng)的操作!

上面是概念部分.但是在說陀螺儀使用之前,我們要談一談兩個框架,一個是CoreMotion框架,另外一個是HealthKit框架,好多剛搞跑步軟件的童鞋都會有這樣的疑問,這兩個框架根據(jù)不同的回調(diào)方法獲取到用戶的運動信息,那么它們有什么不同呢?其實CoreMotion框架獲取的是陀螺儀的加速度,然后通過加速度來計算用戶的運動情況.這是實時更新的,而HealthKit框架是從蘋果自帶的健康軟件中獲取到數(shù)據(jù),并不是實時的更新,這個就需要我們根據(jù)App的需求來酌情處理了.

對于HealthKit框架這里就不過啰嗦了.下面我們就來說明陀螺儀是如何使用的.我們使用的框架是CoreMotion這個iOS原生框架,那么這個框架在實際開發(fā)中是如何使用的呢?

我們先導(dǎo)入在需要的地方導(dǎo)入CoreMotion這框架.

#import <CoreMotion/CoreMotion.h>

在初始化階段,不管你要獲取的是什么數(shù)據(jù),首先需要做的就是創(chuàng)建一個CMMotionManager對象.

motionManager = [[CMMotionManager alloc] init]; 

所有的操作都會由這個manager接管。后面的初始化操作相當直觀,

if (!motionManager.accelerometerAvailable) {  
// fail code // 檢查傳感器到底在設(shè)備上是否可用  
}  
motionManager.accelerometerUpdateInterval = 0.01; // 告訴manager,更新頻率是100Hz  
[motionManager startAccelerometerUpdates]; // 開始更新,后臺線程開始運行。這是pull方式。 

我在項目中是使用block回調(diào)的方式來獲取數(shù)據(jù)的.代碼如下所示.

[motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMAccelerometerData *latestAcc, NSError *error)  
{  
// Your code here  
}]; 

接下來就是獲取x,y,z軸三個方法的加速度數(shù)據(jù)了。如下所示.

                //三個方向加速度值
                double x = accelerometerData.acceleration.x;
                double y = accelerometerData.acceleration.y;
                double z = accelerometerData.acceleration.z;

這樣我們就拿到了x,y,z軸三個方法加速度的原始數(shù)據(jù)了.


陀螺儀的數(shù)據(jù)處理

那么,拿到數(shù)據(jù)之后我們該如何處理呢?獲取原始數(shù)據(jù)的操作很簡單,但是我們還需要做最重要的部分,那就是處理原始數(shù)據(jù),有的童鞋就會問,為什么要處理這些數(shù)據(jù)每一次獲取數(shù)據(jù),難道手機不都是在動嗎?實際上確實如此,但是我們需要的是最大程度上來估算用戶的運動步數(shù),如果一個用戶在不斷晃動手機,那么我們還需要把這種數(shù)據(jù)計算進來嗎?這時候就需要我們把這種數(shù)據(jù)給過濾掉,來減少數(shù)據(jù)的誤差.提高數(shù)據(jù)的精確性.

首先我們創(chuàng)建一個數(shù)據(jù)Model.Model的屬性有震動幅度的系數(shù)(通過x,y,z軸三個方法加速度來獲取,),Model對象的獲取時間.Model獲取時間的格式化時間.Model獲取的位置.代碼如下所示.

#import <Foundation/Foundation.h>

@interface StepModel : NSObject

@property(nonatomic,strong) NSDate *date;

@property(nonatomic,assign) int record_no;

@property(nonatomic, strong) NSString *record_time;

//g是一個震動幅度的系數(shù),通過一定的判斷條件來判斷是否計做一步
@property(nonatomic,assign) double g;

@end

其他的幾個參數(shù)都好理解,最關(guān)鍵的就是這個震動幅度系數(shù)了,說白了 ,它是存儲了手機x,y,z軸三種方向加速度的總和.具體我們會怎么計算呢?如下所示.

                double g = sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2)) - 1;
                StepModel *stepsAll = [[StepModel alloc] init];
                stepsAll.g = g;
                      .    
                      . 
                      . 
                // 加速度傳感器采集的原始數(shù)組
                [arrAll addObject:stepsAll];            

然后,我們會把每一個Model都放在原始數(shù)組里面.因為陀螺儀我們設(shè)置的頻率一般會很快,所以,我們要在必要的時間內(nèi)清理數(shù)據(jù),防止出現(xiàn)內(nèi)存暴增.大約十條數(shù)據(jù)左右我們就可以做一下數(shù)據(jù)出來,然后清除原始數(shù)據(jù)即可.

                if (arrAll.count == 10) {
                    // 步數(shù)緩存數(shù)組
                    NSMutableArray *arrBuffer = [[NSMutableArray alloc] init];
                    arrBuffer = [arrAll mutableCopy];
                    [arrAll removeAllObjects];
                }

接下來我們就處理第一步數(shù)據(jù),根據(jù)震動幅度系數(shù)來判斷是否是合適的數(shù)據(jù),如果震動幅度系數(shù)相比于前后兩個數(shù)據(jù)的震動系數(shù)過大或者過小,那樣這樣的數(shù)據(jù)就不是我們所需要的數(shù)據(jù).如下圖所示.這種判斷的依據(jù)是一個人很少會在1秒之后又加速又減速,你當你是Car呢!?? ?? ??,具體代碼如下所示.

                    // 踩點數(shù)組
                    NSMutableArray *arrCaiDian = [[NSMutableArray alloc] init];
                    
                    //遍歷步數(shù)緩存數(shù)組
                    for (int i = 1; i < arrBuffer.count - 2; i++) {
                        //如果數(shù)組個數(shù)大于3,繼續(xù),否則跳出循環(huán),用連續(xù)的三個點,要判斷其振幅是否一樣,如果一樣,然并卵
                        if (![arrBuffer objectAtIndex:i-1] || ![arrBuffer objectAtIndex:i] || ![arrBuffer objectAtIndex:i+1])
                        {
                            continue;
                        }
                        StepModel *bufferPrevious = (StepModel *)[arrBuffer objectAtIndex:i-1];
                        StepModel *bufferCurrent = (StepModel *)[arrBuffer objectAtIndex:i];
                        StepModel *bufferNext = (StepModel *)[arrBuffer objectAtIndex:i+1];
                        //控制震動幅度,,,,,,根據(jù)震動幅度讓其加入踩點數(shù)組,
                        if (bufferCurrent.g < -0.12 && bufferCurrent.g < bufferPrevious.g && bufferCurrent.g < bufferNext.g) {
                            [arrCaiDian addObject:bufferCurrent];
                        }
                    }
                    

通過,加速度處理完的數(shù)據(jù)難道就沒有問題了嗎?假定加速度合適,用戶用手快速晃動手機,這時候也是會有誤差數(shù)據(jù)的產(chǎn)生,所以這時候我們還是需要根據(jù)一個值來判斷arrCaiDian數(shù)組中的數(shù)據(jù)是否合理.這個屬性就是時間,時間從哪里來呢?時間當然從Model中取到了.

具體的操作步驟是我們先遍歷arrCaiDian這個數(shù)據(jù),然后先判斷是否是第一個數(shù)據(jù),如果是我們存儲它的時間屬性,如果不是,我們直接比較當前Model和前一個Model的時間差,看是否在允許范圍之內(nèi).如果在允許范圍之內(nèi),那么我們就認為當前這個數(shù)據(jù)是一個有效的數(shù)據(jù).具體代碼如下所示.

                    for (int j = 0; j < arrCaiDian.count; j++) {
                        StepModel *caidianCurrent = (StepModel *)[arrCaiDian objectAtIndex:j];

                        //如果之前的步數(shù)為0,則重新開始記錄
                        if (j == 0) {
                            //上次記錄的時間
                            lastDate = caidianCurrent.date;
                        }else {
                            int intervalCaidian = [caidianCurrent.date timeIntervalSinceDate:lastDate] * 1000;
                            // 步行最大每秒2.5步,跑步最大每秒3.5步,超過此范圍,數(shù)據(jù)有可能丟失
                            int min = 259;
                            if (intervalCaidian >= min) {
                                if (motionManager.isAccelerometerActive) {
                                    //存一下時間
                                    lastDate = caidianCurrent.date;
                                }
                            }
                        }
                    }

經(jīng)過這兩步的數(shù)據(jù)處理,基本上數(shù)據(jù)的處理就完成了,接下來我們就直接在if(motionManager.isAccelerometerActive) {}這里面計算用戶的步數(shù)即可.代碼如下所示.self.step就是我們需要的步數(shù).

// 計步器開始計步時間(秒)
#define ACCELERO_START_TIME 2

// 計步器開始計步步數(shù)(步)
#define ACCELERO_START_STEP 1
                                    if (intervalCaidian >= ACCELERO_START_TIME * 1000) {// 計步器開始計步時間(秒)
                                        self.startStep = 0;
                                    }
                                    
                                    if (self.startStep < ACCELERO_START_STEP) {//計步器開始計步步數(shù) (步)
                                        
                                        self.startStep ++;
                                        break;
                                    }
                                    else if (self.startStep == ACCELERO_START_STEP) {
                                        self.startStep ++;
                                        // 計步器開始步數(shù)
                                        // 運動步數(shù)(總計)
                                        self.step = self.step + self.startStep;
                                    }
                                    else {
                                        self.step ++;
                                    }

好了,基本上陀螺儀的開發(fā)就到這里了,Demo我會放在文章最后,各位看官去下載就好.


GPS定位開發(fā)運動

上面陀螺儀開發(fā)運動主要適用于室內(nèi)跑步機,或者日常走路情況,當用戶需要看到他們的運動軌跡的時候,這時候我們就不能使用陀螺儀進行開發(fā)了,而是使用GPS定位+地圖軌跡繪制來進行開發(fā).這里我是基于高德地圖進行開發(fā)的,這里是需要注意.具體如何集成高德地圖這里就不過多啰嗦了.下面我們就幾個問題來探討一下如何使用高德地圖來實時繪制用戶的運動軌跡.


如何處理雜亂的運動軌跡?

其實這個問題說白了就是運動軌跡的容錯處理,現(xiàn)在市面上的大廠App一共有兩種方案,一種是軌跡繪制時間短,用戶運動軌跡比較具體,但是如果信號不好,那么會造成用戶運動軌跡線條雜亂;另外一種方案就是繪制時間長,這樣線條就比較筆直,用戶體驗比較好,但是用戶軌跡就沒有那么具體了.我測試多種方案之后,決定偏向于第二種進行開發(fā)運動軌跡的繪制.

由于我使用的是高德地圖,我們都知道高德地圖是直接封裝了蘋果的原生地圖.所以,很多方法也類似.我們先對地圖和定位對象進行初始化.代碼如下所示.具體屬性什么的我就不過多啰嗦了.

-(MAMapView *)mapView{

    if (_mapView == nil) {
        
        ///初始化地圖
        _mapView = [[MAMapView alloc] initWithFrame:self.view.bounds];
        _mapView.desiredAccuracy = kCLLocationAccuracyBest;
        _mapView.distanceFilter = 1.0f;
        _mapView.showsUserLocation = YES;
        _mapView.userTrackingMode = MAUserTrackingModeFollow;//地圖跟著位置移動
        MAUserLocationRepresentation *r = [[MAUserLocationRepresentation alloc] init];
        r.showsAccuracyRing = NO;///精度圈是否顯示,默認YES
        r.showsHeadingIndicator = YES;
        [_mapView updateUserLocationRepresentation:r];
        _mapView.zoomLevel = 16;
        _mapView.maxZoomLevel = 18;
        //不顯示比例尺
        _mapView.showsScale =NO;
        //不顯示羅盤
        _mapView.showsCompass = NO;    
        _mapView.delegate  = self;
    }
    
    return _mapView;
}
-(void)startRun{
    self.locationManager = [[AMapLocationManager alloc] init];
    self.locationManager.delegate = self;
    self.locationManager.distanceFilter = 10;//設(shè)置移動精度(單位:米)
    self.locationManager.locationTimeout = 2;//定位時間
    self.locationManager.allowsBackgroundLocationUpdates = YES;//開啟后臺定位
    [self.locationManager setLocatingWithReGeocode:YES];
    [self.locationManager startUpdatingLocation];
    self.distance = 0;
    self.startTime = [NSDate date];
}

當我們使用[self.locationManager startUpdatingLocation];就可以讓下面的回調(diào)方法不斷的回調(diào),然后獲取到我們的原始數(shù)據(jù).參數(shù)location就是我們需要的位置信息原始數(shù)據(jù).

- (void)amapLocationManager:(AMapLocationManager *)manager didUpdateLocation:(CLLocation *)location reGeocode:(AMapLocationReGeocode *)reGeocode{}

接下來我們就需要處理數(shù)據(jù)了,當時一開始做的時候我想到了幾個因素,一個是GPS信號強弱,另外一個是兩點之間的速度.但是后來發(fā)現(xiàn)在iOS這邊使用GPS信號來做判斷效果并不是太好,所以就去掉了.現(xiàn)在就是通過了兩點之間的速度來進行判斷是否是合理的點.

定位原始數(shù)據(jù)處理
首先我們先創(chuàng)建一個Model,用來存儲當前點的時間,位置兩個信息.代碼如下所示.

#import <Foundation/Foundation.h>
#import <AMapFoundationKit/AMapFoundationKit.h>

@interface RunLocationModel : NSObject

//RunLocationModel是跑步過程中每一條記錄的Model
@property(nonatomic,assign)CLLocationCoordinate2D location;
@property(nonatomic,strong)NSDate *time;//每一次記錄的時間點

@end

接下來,我們就處理我們的數(shù)據(jù)了.在實際過程中遇到這么一個坑,那就是定位的第一個位置是在大西洋東海岸剛果附近.這是怎么造成的?我分析主要是由于定位還未來及打開,或者說定位的初始點位就是在那里.我們做的就是要把這個點去除即可.我們從第二個點進行取值,這樣就不會造成這樣的問題了.因為是在開啟一瞬間,所以用戶也是感覺不到的.符合我們的用戶體驗性.

那么數(shù)據(jù)處理,我自己寫了一個方法,就是根據(jù)前一個有效點(第一個有效的定位點就直接拿了第一個原始數(shù)據(jù))和新的定位點來通過距離和時間計算速度,比較速度的合理性即可.聯(lián)合上面的定位回調(diào)方法代碼如下.

- (void)amapLocationManager:(AMapLocationManager *)manager didUpdateLocation:(CLLocation *)location reGeocode:(AMapLocationReGeocode *)reGeocode{
    
    RunLocationModel *locationModel = [[RunLocationModel alloc]init];
    locationModel.location = location.coordinate;
    locationModel.time = [NSDate date];
    RunLocationModel *lastModel =locationModel;
    RunLocationModel *lastButOneModel =_finishLocationArray.lastObject;
    [self distanceWithLocation:lastModel andLastButOneModel:lastButOneModel];
}

//計算距離,估算誤差值
-(void)distanceWithLocation:(RunLocationModel *)lastModel andLastButOneModel:(RunLocationModel *)lastButOneModel{

        MAMapPoint point1 = MAMapPointForCoordinate(lastModel.location);
        MAMapPoint point2 = MAMapPointForCoordinate(lastButOneModel.location);
        //2.計算距離
        CLLocationDistance newdistance = MAMetersBetweenMapPoints(point1,point2);
    
        //估算兩者之間的時間差,單位 秒
        NSTimeInterval secondsBetweenDates= [lastModel.time timeIntervalSinceDate:lastButOneModel.time];
        
        //世界飛人9.97秒百米,當超過這個速度,即為誤差值,可能是GPS不準
        if ((float)newdistance/secondsBetweenDates < (float)100/9.74) {
            
            [self.finishLocationArray addObject:lastModel];
            [self mapAddCommonPolyline];//繪制運動軌跡
            self.distance  = self.distance +newdistance;
        }
}

上面,self.distance就是用戶的運動距離了,那么運動軌跡我們該如何搞呢,難道說我們每回調(diào)一次我們都需要繪制一條運動軌跡?NONONO,如果是那樣的話,我們的運動軌跡就會非常的凌亂的.所以我們的處理原則,我們判斷地圖上繪制的最后一個點和從finishLocationArray中取的點是否在距離上合適,如果合適,那么我們就進行繪制,如果不合適,我們就等待下一個點的出現(xiàn),然后再進行判斷.當然了,找點就少不了遍歷finishLocationArray數(shù)組,我們需要從繪制的最后一個點進行遍歷,這樣會大大減少遍歷的次數(shù),減少程序的內(nèi)存損耗.代碼如下所示.

-(void)mapAddCommonPolyline{

    for (int i = _endIndexPath; i<self.finishLocationArray.count; i++) {
        
        RunLocationModel *newlocation = self.finishLocationArray[i];
        MAMapPoint point1 = MAMapPointForCoordinate(newlocation.location);
        MAMapPoint point2 = MAMapPointForCoordinate(_endLocation.location);

        //2.計算距離
        CLLocationDistance newDistance = MAMetersBetweenMapPoints(point1,point2);
        if (newDistance>10) {

            CLLocationCoordinate2D commonPolylineCoords[2];
            commonPolylineCoords[0] = newlocation.location;
            commonPolylineCoords[1] = _endLocation.location;
            
            MAPolyline *commonPolyline = [MAPolyline polylineWithCoordinates:commonPolylineCoords count:2];
            
            [_lineArray addObject:commonPolyline];
            [self.mapView addOverlay: commonPolyline];
            
            _endIndexPath = i;
            _endLocation = newlocation;
        }
    }
}

這樣,用戶的運動軌跡繪制就基本完成了.


如何實現(xiàn)GPS信號的強弱的展示?

GPS信號是沒有直接數(shù)據(jù)的展示的.我們需要從回調(diào)方法的location參數(shù)中拿到horizontalAccuracy屬性和verticalAccuracy屬性的值,這兩個值就是判斷精度圈大小的,如果GPS信號弱的話,那么精度圈就會很大,horizontalAccuracy屬性和verticalAccuracy這兩個值就會很大.相反,如果GPS信號強的話,那么兩者的值就會很小.具體代碼如下所示.

typedef enum : NSUInteger {
    strengthGradeBest  = 1,//信號最好 可精確到0-20米
    strengthGradeBetter,//信號強 可精確到20-100米
    strengthGradeAverage,//信號弱 可精確到100-200米
    strengthGradeBad,//信號很弱 ,200米開外
} strengthGrade;
- (void)amapLocationManager:(AMapLocationManager *)manager didUpdateLocation:(CLLocation *)location reGeocode:(AMapLocationReGeocode *)reGeocode{
   
    locationModel.gpsStrength = [self gpsStrengthWithLocation:location];
}

#pragma mark ---GPS信號強弱---
-(strengthGrade)gpsStrengthWithLocation:(CLLocation *)location{
    if (location.horizontalAccuracy>200 &&location.verticalAccuracy >200) {
        
        return strengthGradeBad;
    }
    if (location.horizontalAccuracy>100 &&location.verticalAccuracy >100&&location.horizontalAccuracy<200 &&location.verticalAccuracy <200) {
        
        return strengthGradeAverage;
    }
    if (location.horizontalAccuracy>20 &&location.verticalAccuracy >20&&location.horizontalAccuracy<100 &&location.verticalAccuracy <100) {
        
        return strengthGradeBetter;
    }
    if (location.horizontalAccuracy<20 &&location.verticalAccuracy <20) {
        
        return strengthGradeBest;
    }
    
    return strengthGradeBad;
}


如何實現(xiàn)用戶方向的展示?

跑步軟件都會有用戶方向的展示,那么這是怎么做到的呢?這時候,我們需要另外的一個回調(diào)方法.那就是-(void)mapView:(MAMapView *)mapView didUpdateUserLocation:(MAUserLocation *)userLocation updatingLocation:(BOOL)updatingLocation,通過這個方法,我們獲取當前的heading信息,然后方向圖標通過heading信息旋轉(zhuǎn)對應(yīng)的角度即可.代碼如下所示.

//根據(jù)頭部信息顯示方向
-(void)mapView:(MAMapView *)mapView didUpdateUserLocation:(MAUserLocation *)userLocation updatingLocation:(BOOL)updatingLocation{

    if(nil == userLocation || nil == userLocation.heading
       || userLocation.heading.headingAccuracy < 0) {
        return;
    }
    
    CLLocationDirection  theHeading = userLocation.heading.magneticHeading;
    
    float direction = theHeading;
    
    if(nil != _myLocationAnnotationView) {
        if (direction > 180)
        {
            direction = 360 - direction;
        }
        else
        {
            direction = 0 - direction;
        }
        _myLocationAnnotationView.image = [self.myLocationImage imageRotatedByDegrees:-direction];
    }

}


總結(jié)

本來這篇文章打算在8月初就寫的,但是由于近來一直在做Java項目,所以一直沒有時間,直到今天終于抽時間寫完了這篇跑步軟件項目總結(jié),希望大家喜歡,如果有什么問題或者疑問,歡迎和騷棟一起探討.最后附上Demo.Demo非本人撰寫,乃洛洛愛吃肉所有,如有侵權(quán),請通知騷棟,立馬刪除,謝謝大家的一路陪伴.

文章參考鏈接
加速儀Demo的地址
地圖軌跡Demo的地址
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容