精华内容
下载资源
问答
  • 2019-07-13 20:50:54

    版权声明:本文来自门心叼龙的博客,属于原创内容,转载请注明出处:https://menxindiaolong.blog.csdn.net/article/details/95789128

    github源码下载地址:https://github.com/geduo83/android-amap-movecar

    关于车辆运动的相关文章一共写过两篇,一篇为Android车辆运动轨迹大数据采集最佳实践,另外一篇是Android车辆运动轨迹数据采集服务保活的探索与发现
    一直打算写一篇车辆运动轨迹平滑移动的文章,年后由于工作项目太忙也就没时间写,工作的事情忙完了,紧接着就是忙自己的另外两个开源项目,一个是Android客户端框架FlyTour
    、和另外一个SpringCloud微服务框架FlyCloud,上个周终于告一段落。

    数据也能采集了,而且采集服务保活也做了,有效的避免了数据采集服务在后台被系统杀死的可能,接着就需要把采集到的数据在移动设备上实时的展示出来,这两天我也特意在网上搜索了一下,关于Android车辆运动轨迹平滑移动的文章,还真没有,大部分都是提问的多,问怎么实现的?都是一些只言片语很零碎的一些回答,在实战项目当中没有太大的实用价值。

    关于车辆运动,在我们在日常生活中见到最多的就是滴滴打车,想必这款app大家都使用过,当你在app的叫车页面,输入完毕你的目的地,点击叫车,如果有司机接单了,你就清楚的看到车辆会平滑的移动到你所在的位置去接你。今天我就带领大家一步步的实现一个车辆平滑移动的功能。

    这个功能是基于高德地图开发的,因此我特意去高德官网查阅了一下,高德的确提供了官方轨迹移动api,我们暂且不用官方API.先用自己的方法去实现一个最简单的功能
    我们先来一睹为快:
    在这里插入图片描述

    单次轨迹回放

    已知有一段轨迹数据,点击回放按钮,小车沿着路线自动的往前运动,播放完毕也就结束了

     public class MoveSingleThread extends Thread{
        private List<LatLng> mLatLngList;
        private Marker mCarMarker;
        public MoveSingleThread(List<LatLng> latLngs, Marker marker) {
            super();
            mLatLngList = latLngs;
            mCarMarker = marker;
        }
    
        @Override
        public void run() {
            super.run();
        }
        public void moveTrack(){
            // 第一个for循环用来计算走了多少部
            int step = 0;
            for (int i = 0; i < mLatLngList.size() - 1; i++) {
                LatLng startPoint = mLatLngList.get(i);
                LatLng endPoint = mLatLngList.get(i + 1);
                double slope = getSlope(startPoint, endPoint);
                // 是不是正向的标示(向上设为正向)
                boolean isReverse = (startPoint.latitude > endPoint.latitude);
                double xMoveDistance = isReverse ? getXMoveDistance(slope) : -1 * getXMoveDistance(slope);
                // 应该对经纬度同时处理
                for (double j = startPoint.latitude; !((j >= endPoint.latitude) ^ isReverse); j =
                        j - xMoveDistance) {
                    step++;
                }
            }
    
            // 通过距离,计算轨迹动画时间间隔
            double mTimeInterval = 0;// 轨迹回放时间戳
            if (!TextUtils.isEmpty(mDistance)) {
                float totalDistance = Float.parseFloat(mDistance) * 1000;
                if (totalDistance <= 500) {
                    mTimeInterval = 1000.0 / step;
                } else if (totalDistance > 500 && totalDistance <= 7500) {
                    mTimeInterval = 2.0 * totalDistance / step;
                } else {
                    mTimeInterval = 15000.0 / step;
                }
            }
    
            // while (true) {
            for (int i = 0; i < mLatLngList.size() - 1; i++) {
                if (stopFlag) {
                    stopFlag = false;
                    break;
                }
                mIsCarMoveing = true;
                LatLng startPoint = mLatLngList.get(i);
                LatLng endPoint = mLatLngList.get(i + 1);
                mCarMarker.setPosition(startPoint);
                mCarMarker.setRotateAngle((float) getAngle(startPoint, endPoint));
                double slope = getSlope(startPoint, endPoint);
                // 是不是正向的标示(向上设为正向)
                boolean isReverse = (startPoint.latitude > endPoint.latitude);
                double intercept = getInterception(slope, startPoint);
                double xMoveDistance = isReverse ? getXMoveDistance(slope) : -1 * getXMoveDistance(slope);
                // 应该对经纬度同时处理
                double mSleep = 0;
                for (double j = startPoint.latitude; !((j >= endPoint.latitude) ^ isReverse); j =
                        j - xMoveDistance) {
                    LatLng latLng = null;
                    if (slope != Double.MAX_VALUE) {
                        latLng = new LatLng(j, (j - intercept) / slope);
                        // latLng = new LatLng(j, k);
                    } else {
                        latLng = new LatLng(j, startPoint.longitude);
                    }
                    mCarMarker.setPosition(latLng);
                    // 如果间隔时间小于1毫秒,则略过当前休眠,累加直到休眠时间到1毫秒:会损失精度
                    if (mTimeInterval < 1) {
                        mSleep += mTimeInterval;
                        if (mSleep >= 1) {
                            SystemClock.sleep((long) mSleep);
                            mSleep = 0;
                        }
                    } else
                        SystemClock.sleep((long) mTimeInterval);
                }
            }
        }
    }
    

    实时轨迹数据排队问题

    如果要显示实时轨迹怎么办 ,上面的代码就有问题了,Thread.start()方法调用后,就会立马执行他的run方法,run方法执行完毕,线程也就结束了,也是说上面的代码只能跑一次轨迹数据,如果每间隔五秒从后台取一次轨迹数据,就需要一数据队列来存储这些数据,每跑完一次数据,就从数据队列里面去取,如果有就取来接着跑,如果没有就处于等待状态。 我们创建异步消息处理线程,这一问题就可以迎刃而解,来数据了我们就可以通过handler把数据post给我们的子线程,Handler自带数据队列,它处于排队状态,如果有数据了就开始跑轨迹,如果没有数据就处于等待状态,直到有数据的到来,如果对异步消息处理线程不熟悉,请查看我的另外一篇文章Android实战开发Handler机制深度解析https://menxindiaolong.blog.csdn.net/article/details/86560330

    一个标准的异步消息处理线程应该怎么写?
    方法1:

    class LooperThread extends Thread {
          public Handler mHandler;
     
          public void run() {
              Looper.prepare();
              mHandler = new Handler() {
                  public void handleMessage(Message msg) {
                      // process incoming messages here
                  }
              };
              Looper.loop();
          }
    }
    

    方法2:

      // Step 1: 创建并启动HandlerThread线程,内部包含Looper
        HandlerThread handlerThread = new HandlerThread("gityuan.com");
        handlerThread.start();
     
        // Step 2: 创建Handler
        Handler handler = new Handler(handlerThread.getLooper()) {
           public void handleMessage(Message msg) {
             // process incoming messages here
           }
         };
        // Step 3: 发送消息
         handler.post(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread id="+Thread.currentThread().getId());
            }
        });
    

    上面就是Android系统中异步消息处理线程的通用写法

    运动轨迹的暂停、继续问题

    由于运动轨迹是在子线程里面完成的,我们自然而然会想到线程的等待、唤醒,也就是wait、notify的问题了
    因此我们在运动过程加上就如下代码就可以了

    if (pause) {
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    怎么让他恢复运动呢?notify一下即可

    public void reStartMove() {
            synchronized (lock) {
                pause = false;
                lock.notify();
            }
    }
    

    完整的代码如下:

    在这里插入图片描述

    /**
     * Description: <MoveCarCustomThread><br>
     * Author:      mxdl<br>
     * Date:        2019/7/10<br>
     * Version:     V1.0.0<br>
     * Update:     <br>
     */
    public class MoveCarCustomThread extends Thread {
        public static final String TAG = MoveCarCustomThread.class.getSimpleName();
        private Handler moveCarHandler;//发送数据的异步消息处理器
        private Object lock = new Object();//线程锁
        private boolean moveing = false;//是否线程正在移动
        private boolean pause = false;//暂停状态,为true则暂停
        private boolean stop = false;//停止状态,为true则停止移动
        private WeakReference<MainActivity> mActivityWeakReference;//防止内存Activity导致的内容泄漏
        private MOVE_STATE currMoveState = MOVE_STATE.START_STATUS;
    
        public void setCurrMoveState(MOVE_STATE currMoveState) {
            this.currMoveState = currMoveState;
        }
    
        public MOVE_STATE getCurrMoveState() {
            return currMoveState;
        }
        public MoveCarCustomThread(MainActivity activity) {
            mActivityWeakReference = new WeakReference<>(activity);
        }
        //暂停移动
        public void pauseMove() {
            pause = true;
        }
        //设置暂停之后,再次移动调用它
        public void reStartMove() {
            synchronized (lock) {
                pause = false;
                lock.notify();
            }
        }
    
        public void stopMove() {
            stop = true;
            if(moveCarHandler != null){
                moveCarHandler.removeCallbacksAndMessages(null);
            }
            if(mActivityWeakReference.get() != null){
                mActivityWeakReference.get().mLatLngList.clear();
                mActivityWeakReference.get().mMainHandler.removeCallbacksAndMessages(null);
            }
        }
    
        public Handler getMoveCarHandler() {
            return moveCarHandler;
        }
        public boolean isMoveing() {
            return moveing;
        }
    
    
        @Override
        public void run() {
            super.run();
            //设置该线程为loop线程
            Looper.prepare();
            moveCarHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    //通过锁保证发过来的数据同步入列
                    synchronized (lock) {
                        if (msg.obj != null && msg.obj instanceof List) {
                            List<LatLng> latLngList = (List<LatLng>) msg.obj;
                            moveCoarseTrack(latLngList);
                        }
                    }
                }
            };
            //启动loop线程
            Looper.loop();
        }
    
        private void moveCoarseTrack(List<LatLng> latLngList) {
            if (latLngList == null || latLngList.size() == 0 || latLngList.size() == 1) {
                return;
            }
            Log.v(TAG, "moveCoarseTrack start.........................................................");
            long startTime = System.currentTimeMillis();
            Log.v(TAG, "startTime:" + startTime);
            int step = TrackMoveUtil.getStep(latLngList);// 通过距离,计算轨迹动画运动步数
            Log.v(TAG, "move step:" + step);
            float distance = TrackMoveUtil.getDistance(latLngList);
            Log.v(TAG, "move distance:" + distance);
            double mTimeInterval = TrackMoveUtil.getMoveTime(distance, step);// 通过距离,计算轨迹动画时间间隔
            mTimeInterval = 10;// 每走一步停止10毫秒
            Log.v(TAG, "move mTimeInterval:" + mTimeInterval);
    
            moveing = true;
            for (int i = 0; i < latLngList.size() - 1; i++) {
                // 暂停状态,线程停止了
                if (pause) {
                    movePause();
                }
                if (stop) {
                    break;
                }
                moveing = true;
                LatLng startLatLng = latLngList.get(i);
                LatLng endLatLng = latLngList.get(i + 1);
                MainActivity mainActivity = mActivityWeakReference.get();
                moveCar(startLatLng, endLatLng, mainActivity);
                moveLine(startLatLng, mainActivity);
                moveCamera(startLatLng, mainActivity);
    
                double slope = TrackMoveUtil.getSlope(startLatLng, endLatLng);// 计算两点间的斜率
                double intercept = TrackMoveUtil.getInterception(slope, startLatLng);// 根据点和斜率算取截距
                boolean isReverse = (startLatLng.latitude > endLatLng.latitude);// 是不是正向的标示(向上设为正向)
                double xMoveDistance = isReverse ? TrackMoveUtil.getXMoveDistance(slope) : -1 * TrackMoveUtil.getXMoveDistance(slope);
                // 应该对经纬度同时处理
                double sleep = 0;
                int flag = 0;
                for (double j = startLatLng.latitude; !((j >= endLatLng.latitude) ^ isReverse); j = j - xMoveDistance) {
                    // 非暂停状态地图才进行跟随移动
                    if (pause) {
                        movePause();
                    }
                    if (stop) {
                        break;
                    }
                    moveing = true;
                    flag++;
                    if (slope != Double.MAX_VALUE) {
                        startLatLng = new LatLng(j, (j - intercept) / slope);
                    } else {
                        startLatLng = new LatLng(j, startLatLng.longitude);
                    }
                    moveCar(startLatLng, mainActivity);
                    moveLine(startLatLng, mainActivity);
                    if (flag % 100 == 0) {
                        moveCamera(startLatLng, mainActivity);
                    }
                    // 如果间隔时间小于1毫秒,则略过当前休眠,累加直到休眠时间到1毫秒:会损失精度
                    if (mTimeInterval < 1) {
                        sleep += mTimeInterval;
                        if (sleep >= 1) {
                            Log.v(TAG, "sleep:" + sleep);
                            SystemClock.sleep((long) sleep);
                            sleep = 0;
                        }
                    } else {
                        SystemClock.sleep((long) mTimeInterval);
                    }
    
                }
            }
            long endTime = System.currentTimeMillis();
            moveing = false;
            Log.v(TAG, "endTime:" + endTime);
            Log.v(TAG, "run mTimeInterval:" + (endTime - startTime));
            Log.v(TAG, "moveCoarseTrack end.........................................................");
        }
    
        private void moveLine(LatLng startLatLng, MainActivity mainActivity) {
            mainActivity.mLatLngList.add(startLatLng);// 向轨迹集合增加轨迹点
            mainActivity.mMovePolyline.setPoints(mainActivity.mLatLngList);// 轨迹画线开始
        }
    
        private void moveCar(LatLng startLatLng, LatLng endLatLng, MainActivity mainActivity) {
            moveCar(startLatLng,mainActivity);
            if (mainActivity.mCarMarker != null) {
                mainActivity.mCarMarker.setRotateAngle((float) TrackMoveUtil.getAngle(startLatLng, endLatLng));// 设置小车车头的方向
            }
        }
        private void moveCar(LatLng startLatLng,MainActivity mainActivity) {
            if (mainActivity.mCarMarker != null) {
                mainActivity.mCarMarker.setPosition(startLatLng);// 小车移动
            }
        }
        private void movePause() {
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        private void moveCamera(LatLng startLatLng, MainActivity mainActivity) {
            Message message = Message.obtain();
            message.what = MainActivity.EventType.MapMove;
            message.obj = startLatLng;
            mainActivity.mMainHandler.sendMessage(message);
        }
    
    }
    

    核心算法工具类

     * <h1>轨迹平滑所需要的工具方法</h1> Date: 2016-10-27 Created by mxdl
     */
    public class TrackMoveUtil {
      private static double DISTANCE = 0.0001;
    
      /**
       * 根据两点算斜率
       */
      public static double getSlope(LatLng fromPoint, LatLng toPoint) {
        if (fromPoint == null || toPoint == null) {
          return 0;
        }
        if (toPoint.longitude == fromPoint.longitude) {
          return Double.MAX_VALUE;
        }
        double slope =
            ((toPoint.latitude - fromPoint.latitude) / (toPoint.longitude - fromPoint.longitude));
        return slope;
    
      }
    
      /**
       * 根据两点算取图标转的角度
       */
      public static double getAngle(LatLng fromPoint, LatLng toPoint) {
        if (fromPoint == null || toPoint == null) {
          return 0;
        }
        double slope = getSlope(fromPoint, toPoint);
        if (slope == Double.MAX_VALUE) {
          if (toPoint.latitude > fromPoint.latitude) {
            return 0;
          } else {
            return 180;
          }
        }
        float deltAngle = 0;
        if ((toPoint.latitude - fromPoint.latitude) * slope < 0) {
          deltAngle = 180;
        }
        double radio = Math.atan(slope);
        double angle = 180 * (radio / Math.PI) + deltAngle - 90;
        return angle;
      }
    
      /**
       * 根据点和斜率算取截距
       */
      public static double getInterception(double slope, LatLng point) {
        if (point == null) {
          return 0;
        }
        return point.latitude - slope * point.longitude;
      }
    
      /**
       * 计算x方向每次移动的距离
       */
      public static double getXMoveDistance(double slope) {
        if (slope == Double.MAX_VALUE) {
          return DISTANCE;
        }
        return Math.abs((DISTANCE * slope) / Math.sqrt(1 + slope * slope));
      }
    
      /**
       * 根据轨迹线段计算小车走了多少步
       * 
       * @param latLngList
       * @return
       */
      public static int getStep(List<LatLng> latLngList) {
        int step = 0;
        if (latLngList != null && latLngList.size() > 1) {
          for (int i = 0; i < latLngList.size() - 1; i++) {
            try {
              LatLng startPoint = latLngList.get(i);
              LatLng endPoint = latLngList.get(i + 1);
              double slope = getSlope(startPoint, endPoint);
              // 是不是正向的标示(向上设为正向)
              boolean isReverse = (startPoint.latitude > endPoint.latitude);
              double xMoveDistance = isReverse ? getXMoveDistance(slope) : -1 * getXMoveDistance(slope);
              // 应该对经纬度同时处理
              for (double j = startPoint.latitude; !((j >= endPoint.latitude) ^ isReverse); j =
                  j - xMoveDistance) {
                step++;
              }
            } catch (Exception e) {
              e.printStackTrace();
            }
          }
    
        }
        return step;
      }
    
      /**
       * 根据总距离和步数计算运动时间
       * 
       * @param distance
       * @param step
       * @return
       */
      public static double getMoveTime(float distance, int step) {
        double timeInterval = 0;
        if (distance > 0) {
          float totalDistance = distance * 1000;
          if (totalDistance <= 500) {
            timeInterval = 1000.0 / step;
          } else if (totalDistance > 500 && totalDistance <= 7500) {
            timeInterval = 2.0 * totalDistance / step;
          } else {
            timeInterval = 15000.0 / step;
          }
        }
        return timeInterval;
      }
    
      /**
       * 根据轨迹点集合计算总距离
       * 
       * @param latLngList
       * @return
       */
      public static float getDistance(List<LatLng> latLngList) {
        float distance = 0;
        if (latLngList != null && latLngList.size() > 1) {
          for (int i = 0; i < latLngList.size() - 1; i++) {
            try {
              distance += AMapUtils.calculateLineDistance(latLngList.get(i), latLngList.get(i + 1));
            } catch (Exception e) {
              e.printStackTrace();
            }
          }
        }
        return distance;
      }
    
      // latitude - 地点的纬度,在-90 与90 之间的double 型数值。
      // longitude - 地点的经度,在-180 与180 之间的double 型数值。
      /**
       * 根据一个经纬度字符串求一个经纬度集合a|b|c|d;
       * 
       * @param latlonStr
       * @return
       */
      public static List<LatLng> getListLatLng(String latlonStr) {
        if (!TextUtils.isEmpty(latlonStr)) {
          String[] trackArr = latlonStr.split("\\|");
          if (trackArr != null && trackArr.length > 0) {
            List<LatLng> latLngList = new ArrayList<LatLng>();
            for (int i = 0; i < trackArr.length - 1; i = i + 2) {
              try {
                String lat = trackArr[i + 1];
                String lng = trackArr[i];
                // Logger.v(TAG,"trackArr index:" + i);
                // Logger.v(TAG,"trackArr lat:" + lat);
                // Logger.v(TAG,"trackArr lng:" + lng);
                if (!TextUtils.isEmpty(lat) && !TextUtils.isEmpty(lng)) {
                  Double dLat = Double.valueOf(lat);
                  Double dLng = Double.valueOf(lng);
                  if (dLat >= -90 && dLat <= 90 && dLng >= -180 && dLng <= 180
                      && !(dLat == 0 && dLng == 0)) {
                    LatLng latLng = new LatLng(dLat, dLng);
                    latLngList.add(latLng);
                  }
                }
              } catch (Exception e) {
                e.printStackTrace();
              }
            }
            return latLngList;
          }
        }
        return null;
      }
    }
    

    高德API实现的实时运动轨迹

    /**
     * Description: <MoveCarCustomThread><br>
     * Author:      mxdl<br>
     * Date:        2019/7/10<br>
     * Version:     V1.0.0<br>
     * Update:     <br>
     */
    public class MoveCarSmoothThread implements IMoveCar {
        public static final String TAG = MoveCarSmoothThread.class.getSimpleName();
        private MovingPointOverlay mMovingPointOverlay;
        private WeakReference<MainActivity> mActivityWeakReference;
        private boolean isfirst = true;
        private MOVE_STATE currMoveState = MOVE_STATE.START_STATUS;
    
        public void setCurrMoveState(MOVE_STATE currMoveState) {
            this.currMoveState = currMoveState;
        }
    
        public MOVE_STATE getCurrMoveState() {
            return currMoveState;
        }
    
        public MoveCarSmoothThread(MainActivity activity) {
            mActivityWeakReference = new WeakReference<>(activity);
        }
        @Override
        public void startMove(List<LatLng> latLngs) {
            if (latLngs == null || latLngs.size() == 0) {
                return;
            }
    
            Log.v("MYTAG","startMove start:"+Thread.currentThread().getName());
            Log.v(TAG, "moveCoarseTrack start.........................................................");
            long startTime = System.currentTimeMillis();
            Log.v(TAG, "startTime:" + startTime);
            final MainActivity mainActivity = mActivityWeakReference.get();
            if (mMovingPointOverlay == null) {
                mMovingPointOverlay = new MovingPointOverlay(mainActivity.mAMap, mainActivity.mCarMarker);
                mMovingPointOverlay.setTotalDuration(5);
                mMovingPointOverlay.setMoveListener(new MovingPointOverlay.MoveListener() {
                    @Override
                    public void move(double v) {
                        if(isfirst){
                            isfirst = false;
                            Log.v("MYTAG","MoveCarSmoolthThread move start:"+Thread.currentThread().getName());
                        }
    
                        LatLng position = mMovingPointOverlay.getPosition();
                        mainActivity.mLatLngList.add(position);// 向轨迹集合增加轨迹点
                        mainActivity.mMovePolyline.setPoints(mainActivity.mLatLngList);// 轨迹画线开始
    
                        Message message = Message.obtain();
                        message.what = MainActivity.EventType.MapMove;
                        message.obj = position;
                        message.arg1 = (int)v;
                        mainActivity.mMainHandler.sendMessage(message);
                    }
                });
            }
            mMovingPointOverlay.setPoints(latLngs);
            mMovingPointOverlay.startSmoothMove();
            long endTime = System.currentTimeMillis();
            Log.v(TAG, "endTime:" + endTime);
            Log.v(TAG, "moveCoarseTrack end.........................................................");
        }
    
        @Override
        public void reStartMove() {
            if(mMovingPointOverlay != null){
                mMovingPointOverlay.startSmoothMove();
            }
        }
        @Override
        public void pauseMove(){
            if(mMovingPointOverlay != null){
                mMovingPointOverlay.stopMove();
            }
        }
        @Override
        public void stopMove(){
            if(mMovingPointOverlay != null){
                mMovingPointOverlay.destroy();
                mMovingPointOverlay = null;
            }
            if(mActivityWeakReference.get() != null){
                mActivityWeakReference.get().mLatLngList.clear();
            }
        }
    
    }
    

    最后我把整个项目的的完整代码传到GitHub上了https://github.com/geduo83/android-amap-movecar

    更多相关内容
  •  最近帝都的天气有些冷,天寒地冻,天气虽冷,但也无法阻挡我写文章的热情,之前很少写文章,记得写文章已经是很久很久以前的事情了,一直有计划说要写点什么,但是一直感觉没时间,没有什么可写,最近机会来了。...

    csdn源码下载地址:https://download.csdn.net/download/geduo_83/10841480

    前言:

        最近帝都的天气有些冷,天寒地冻,天气虽冷,但也无法阻挡我写文章的热情,之前很少写文章,记得写文章已经是很久很久以前的事情了,一直有计划说要写点什么,但是一直感觉没时间,没有什么可写,最近机会来了。

        这阵子由于项目需要,需要从手机上采集用户的运动轨迹数据,这样的功能大家都见到的很多了,比如咕咚、悦动圈,对跑步运动轨迹数据进行采集,再如,微信运动、钉钉运动,对于每一天你走步进行计数,如果要记录轨迹就离不开的手机定位,如果要记录步数那就离不开陀螺仪(角速度传感器),花了一天多的时间实现了一个定位数据实时采集的功能。

        技术类的文章不好写,现在写的人也不少,有的人虽然写的多,但是评价并不高,并不是技术不好,而是写的太枯燥了,深度把握不当,而且大部分读者都是初学者,所以我尽量以浅显易懂的文字把每个问题讲清楚。

        运动轨迹数据采集,那就离不开手机定位,定位不是本文的重点,如果不太熟悉定位知识请移步:https://www.jianshu.com/p/00420c1fefe2, 这篇文章很详细的阐述了,GPS定位,A-GPS,基站定位,WIFI定位等技术实现原理,本文重点在于GPS数据采集,数据存储。

    1.UI效果图

        我们先把实现的效果先看一下,有一个宏观上的认识,其实很简单,就一个打底的地图,三个按钮,开始、停止、显示。 点击开始按钮启动服务开始采集数据,点击停止按钮停止数据采集,点击显示把采集到的轨迹数据在地图上展示一下。 

    2.数据源的选择

        数据采集可采用Android系统原生的定位服务,也可以使用第三方的定位服务比如高德定位,百度定位等,根据多年来的开发经验,还是高德好用些,曾经做导航的时候,就发现百度导航会出现主路辅路不分等情况,前阵子还曝光了百度地图盗用高德地图的采集数据的丑闻,高德毕竟页是专业做地图出身的,而且现在都是免费的 ,高德定位的优势请参见:https://lbs.amap.com/faq/android/android-location/15

    3.数据持久化

        解决了数据源问题,接下来问题就是数据往哪里存的问题,在android系统中实现数据持久化通常有一下几个解决方案,

    • 3.1 SharedPreferences

      适用与存储一些app的配置信息,例如缓存用户登录的用户名,密码等信息,版本信息等小量信息

    • 3.2. ContentProvider

      它为不同的应用程序之间数据访问提供了统一的访问接口,例如通讯录数据,相册数据,这些数据在第三方app中经常会用到

    • 3.3 File

      通过IO流,把数据存储于文件,文件内容可以是xml形式,也可以是json形式

    • 3.4 SQLiteDatabase

      android系统自带的一个小型的关系型数据库

          很显然SharedPreferences、ContentProvider不在考虑范围,由于数据采集是一个持续时间长,频率高的操作,对于频繁对文件进行读写操作是非常消耗系统资源的,对于采集的多个文件也不好管理,如果删除某个点的数据,在整个文件中进行检索将是非常痛苦的,最要命的是,File文件只能存储在机身存储的外部存储,这个区域是一个共享区域,如果用户手贱,私自删除数据也是有可能的 。
          毋庸置疑使用SQLiteDatabase存储将是您最佳的选择。

    4.数据怎么存

        解决了数据源和数据存到哪的问题,接下来就是怎么存的问题,数据采集操作一个持久操作,不能阻塞UI主线程,那就需要启动一个子线程了,直接让DB里面存,合适吗?采集一个往DB里面存储一个,如果按照1秒采集一次的速度来计算的话,那就一分钟向数据库有60次的读写操作,要知道,在Android的世界里,所有的IO操作都是耗时的操作,怎么办,很简单,先把采集到数据缓存到内存中,缓存到一定程度,一次性全部取出来一把存入库中,问题不就解决了,按照20秒取一次的速度来取的话,一分钟只要存储三次就行了,一分钟就减少了57次对数据库的操作,大大的提升了数据采集的性能问题,当然这只是举个例子,定位的频率,以及数据入库的频率,到具体测试的时候根据实际情况调到最优,分析到了这里,我们也就不难下结论了,毫不含糊先开启一个子线程来采集数据并将数据存入到内存,再开启一个定期任务的子线程负责从内存中取数据,并将数据存入数据库,有一点需要注意下,内存的数据结构我们用ArrayList实为不妥,多线程中有数据同步的问题,所以就只能Vector了,说道这里我们不难发现,这数据采集实现的过程其实就是我们常说的生产者与消费者的问题了

    5.提高进程优先级

        数据采集是持久的操作,如果程序进入后台,过一段时间就很有可能被系统杀死,我们知道android系统的的进程,按照进程的优先级可划分为:前台进程、可见进程间、服务进程、后台进程、空进程,很显然我们需要启动一个Service服务来对数据进行采集和存储的操作,这样如果程序进入了后台,我们将一个后台进程提升为了服务进程,提升了系统的优先级,服务进程被系统杀死的概率将会大大降低。

        在长期的开发实践中证明后台服务进程在某些机型,也有被杀死的可能,需要我们进一步需要进程优先级,怎么办,真正的“黑科技”来了,通过android系统提供的账号同步机制SyncAdapter来实现进程的优先级,SysnAdapter服务工作在独立的进程,由操作系统调度,进程属于系统核心级别,系统不会被杀掉,而使用SyncAdapter的进程优先级本身也会提高,服务关联SyncAdapter后,进程的优先级变为1,仅仅低于前台正在运行的进程,因此大大降低了被系统杀掉的概率。

    6.工作流程

    有了以上的分析,现在不妨画个流程图,以便加深理解

    7.类关系图

    8.代码实现

    画好UML类图后,再去看源码,再也不怕迷路了

    • 8.1 启动服务
     //MainActivity:启动轨迹信息收集服务
      private void startTrackCollectService() {
        Intent intent = new Intent(this, TrackCollectService.class);
        startService(intent);
        bindService(intent, new ServiceConnection() {
          @Override
          public void onServiceConnected(ComponentName name, IBinder service) {
            mTrackCollection = (ITripTrackCollection) service;
          }
    
          @Override
          public void onServiceDisconnected(ComponentName name) {
    
          }
        }, Context.BIND_AUTO_CREATE);
      }
    
    • 8.2 开始采集
      //TripTrackCollection:开始采集数据
     @Override
      public void start() {
        startLocation();
        startCollect();
      }
    
      // 开启定位服务
      private void startLocation() {
        Log.v("MYTAG", "startLocation start...");
        // 初始定位服务
        if(mlocationClient == null){
          mlocationClient = new AMapLocationClient(mContext);
        }
        // 初始化定位参数
        AMapLocationClientOption mLocationOption = new AMapLocationClientOption();
        // 设置定位模式为高精度模式,Battery_Saving为低功耗模式,Device_Sensors是仅设备模式
        mLocationOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);
        // 设置定位间隔,单位毫秒,默认为2000ms
        mLocationOption.setInterval(2000);
        // 设置定位参数
        mlocationClient.setLocationOption(mLocationOption);
    
        mLocationOption.setOnceLocation(false);// 是否定位一次
        // 此方法为每隔固定时间会发起一次定位请求,为了减少电量消耗或网络流量消耗,
        // 注意设置合适的定位时间的间隔(最小间隔支持为1000ms),并且在合适时间调用stopLocation()方法来取消定位请求
        // 在定位结束后,在合适的生命周期调用onDestroy()方法
        // 在单次定位情况下,定位无论成功与否,都无需调用stopLocation()方法移除请求,定位sdk内部会移除
        // 启动定位
        // 设置定位监听
        mlocationClient.setLocationListener(new AMapLocationListener() {
          @Override
          public void onLocationChanged(final AMapLocation amapLocation) {
            if (amapLocation != null && amapLocation.getErrorCode() == 0) {
              // 定位成功回调信息,设置相关消息
              // amapLocation.getLocationType();// 获取当前定位结果来源,如网络定位结果,详见定位类型表
              // amapLocation.getLatitude();// 获取纬度
              // amapLocation.getLongitude();// 获取经度
              // amapLocation.getAccuracy();// 获取精度信息
              if (mAMapLocationListener != null) {
                mAMapLocationListener.onLocationChanged(amapLocation);
              }
              if (mVectorThread == null) {
                mVectorThread = Executors.newSingleThreadExecutor();
              }
              Log.d("MYTAG",
                  "lat:" + amapLocation.getLatitude() + "lon:" + amapLocation.getLongitude());
              // 避免阻塞UI主线程,开启一个单独线程来存入内存
              mVectorThread.execute(new Runnable() {
                @Override
                public void run() {
                  mLocations
                      .add(new LocationInfo(amapLocation.getLatitude(), amapLocation.getLongitude()));
                }
              });
            } else {
              // 显示错误信息ErrCode是错误码,errInfo是错误信息,详见错误码表。
              Log.d("MYTAG", "location Error, ErrCode:" + amapLocation.getErrorCode() + ", errInfo:"
                  + amapLocation.getErrorInfo());
              if(isshowerror){
                isshowerror = false;
                Toast.makeText(mContext, amapLocation.getErrorInfo(), Toast.LENGTH_LONG).show();
              }
            }
          }
        });
        mlocationClient.startLocation();
      }
    
      // 开启数据入库线程,二十秒秒中入一次库
      private void startCollect() {
        Log.v("MYTAG", "startCollect start...");
        if (mDataBaseThread == null) {
          mDataBaseThread = Executors.newSingleThreadScheduledExecutor();
        }
        mDataBaseThread.scheduleWithFixedDelay(new Runnable() {
          @Override
          public void run() {
            // 取出缓存数据
            StringBuffer stringBuffer = new StringBuffer();
            for (int i = 0; i < mLocations.size(); i++) {
              LocationInfo locationInfo = mLocations.get(i);
              stringBuffer.append(locationInfo.getLat()).append(",").append(locationInfo.getLon())
                  .append("|");
            }
            // 取完之后清空数据
            mLocations.clear();
            String trackid = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
            TripDBHelper.getInstance(mContext).addTrack(trackid, trackid, stringBuffer.toString());
          }
        }, 1000 * 20, 1000 * 20, TimeUnit.MILLISECONDS);
      }
    
    
    • 8.3 停止采集
     //停止采集
      @Override
      public void stop() {
        Log.v("MYTAG", "stop start...");
        if (mlocationClient != null) {
          mlocationClient.stopLocation();
          mlocationClient = null;
        }
        // 关闭Vector线程
        if (mVectorThread != null) {
          mVectorThread.shutdownNow();
          mVectorThread = null;
        }
        // 关闭SaveDabase线程
        if (mDataBaseThread != null) {
          mDataBaseThread.shutdownNow();
          mDataBaseThread = null;
        }
        // 定期任务关闭后,需要把最后的数据同步到数据库
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < mLocations.size(); i++) {
          LocationInfo locationInfo = mLocations.get(i);
          stringBuffer.append(locationInfo.getLat()).append(",").append(locationInfo.getLon())
              .append("|");
        }
        // 取完之后清空数据
        mLocations.clear();
        String trackid = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        TripDBHelper.getInstance(mContext).addTrack(trackid, trackid, stringBuffer.toString());
      }
    
    • 8.4 轨迹展示
    //MainActivity:轨迹展示
      private void showTrack(List<LatLng> list) {
        if (list == null || list.size() == 0) {
          return;
        }
    
        final LatLngBounds.Builder mBuilder = new LatLngBounds.Builder();
        PolylineOptions polylineOptions = new PolylineOptions()
            .setCustomTexture(BitmapDescriptorFactory.fromResource(R.mipmap.ic_tour_track))
            .addAll(list);
        if (mMap != null) {
          mMap.clear();
          mMap.addPolyline(polylineOptions);
        }
        for (int i = 0; i < list.size(); i++) {
          mBuilder.include(list.get(i));
        }
    
        new Handler().postDelayed(new Runnable() {
          @Override
          public void run() {
            CameraUpdate cameraUpdate;
            // 判断,区域点计算出来,的两个点相同,这样地图视角发生改变,SDK5.0.0会出现异常白屏(定位到海上了)
            if (mBuilder != null && mMap != null) {
              LatLng northeast = mBuilder.build().northeast;
              if (northeast != null && northeast.equals(mBuilder.build().southwest)) {
                cameraUpdate = CameraUpdateFactory.newLatLng(mBuilder.build().southwest);
              } else {
                cameraUpdate = CameraUpdateFactory.newLatLngBounds(mBuilder.build(), 20);
              }
              mMap.animateCamera(cameraUpdate);
            }
          }
    
        }, 500);
      }
    

    源码下载地址:https://download.csdn.net/download/geduo_83/10841480

    特别说明

    由于时间关系通过SyncAdapter服务来提升数据采集服务系统进程优化级的功能,暂未实现,在后续版本将会逐步完善。

    问题反馈

    在使用中有任何问题,欢迎反馈给我,可以用以下联系方式跟我交流

    关于作者

      var geduo_83 = {
        nickName  : "geduo_83",
        site : "http://www.weibo.com/geduo83"
      }
    展开全文
  • AI运动项目践行运动数字化的理念,为运动人口的上翻提供了重要支撑,迈出了阿里体育端智能运动领域的第步,为用户带来了更加有趣的新颖玩法。上线以来,项目受到了广泛关注。 作者 | 其声 来源 | 阿里技术...

    简介: 过去一年,阿里体育技术团队在端智能方面不断探索,特别在运动健康场景下实现了实践落地和业务赋能,这就是AI运动项目。AI运动项目践行运动数字化的理念,为运动人口的上翻提供了重要支撑,迈出了阿里体育端智能运动领域的第一步,为用户带来了更加有趣的新颖玩法。上线以来,项目受到了广泛关注。

    image.png

    作者 | 其声
    来源 | 阿里技术公众号

    一 背景

    过去一年,阿里体育技术团队在端智能方面不断探索,特别地,在运动健康场景下实现了实践落地和业务赋能,这就是AI运动项目。AI运动项目践行运动数字化的理念,为运动人口的上翻提供了重要支撑,迈出了阿里体育端智能运动领域的第一步,为用户带来了更加有趣的新颖玩法。上线以来,项目受到了广泛关注。

    2020年因新冠疫情,传统的线下运动受到限制,居家运动逐渐成为新趋势。基于阿里巴巴强大的技术沉淀,阿里体育团队顺应线上运动的迫切需要,开发出基于AI识别的智能运动,为用户提供了简便、好玩的新型居家运动方式。只需一部手机和3-4平米的场地,就可以开展AI运动。运动时,用户打开乐动力APP,将手机固定在场地一侧,适当设置手机角度,根据应用的自动语音提示调整身体与手机距离,直到人体完全位于识别框内,即可开始运动。

    1.gif

    二 端智能实践

    经过⼀年的探索和完善,从验证DEMO到囊括多种动作、支持能力迁移的AI运动智能平台,阿⾥体育建立了系统化的客户端运动智能体系。端智能运动体系基于阿里深度推理引擎在手机端进行推理,识别⼈体姿态和动作,分析人体姿态、运动轨迹、动作角度等,给出实时反馈并进行动作纠正,通过能力的模块化组合,现已支持十多种运动动作和数十种玩法,实现了运动与AI的有机整合,让用户的线上运动变得上手简单而又充满趣味。

    三 技术支持

    端智能运动的基本技术思路是运用MNN推理引擎进行推理和姿态识别。即

    • 实时检测图像及视频中的人体轮廓,定位人体14个关键骨骼点,包括头、肩、脚等重点关节部位。
    • 基于这些关键点信息,连点成线、连线形成动作,可以分析人体姿态、动作角度和运动轨迹。
    • 通过动作姿态匹配,检测用户运动动作,实现动作的计时与计数。同时,实时检测分析动作标准化程度,给出状态反馈,纠正用户动作,实现互动,提高交互体验。

    image.png

    传统运动方式下,用户在运动时可以及时得到现场辅助人员(教练员、考官或亲友)的实时提醒和帮助。端智能运动方式下,用户在做动作时只能与手机应用进行交互。交互的能力和识别水平会受到推理模型能力、运动场景复杂度、运动匹配识别算法等一系列因素的影响。在端智能运动能力的探索和落地过程中,会遇到一些新的问题或者难题,如人机方位匹配、骨骼点识别丢点、点误识别、二维失真、用户移动、手机晃动、场景噪声等。这些问题不一一赘述,仅选取几个有代表性的问题进行分享:

    • 动作的有效性判断及关键算法设计,以提高动作匹配精度,这是智能运动能力的基础。
    • 在保证识别效果的前提下,采取有效措施,降低移动终端的资源消耗,以提升用户体验,主要表现是费电和发热。
    • 采取更加灵活的方式,减轻移动端测试的人力和时间消耗,提高开发和测试效率,为团队的交付保障提供有力支撑。

    提升识别精度

    智能运动带给用户的最直观、最基础的感受就是动作计数准确性。如果动作识别计数不准,用户使用APP的积极性就会打消,参与性就不高。为此,我们要首先解决计数准不准的问题。

    智能运动计数的基本原理是,把一个完整动作分解成若干个小步骤,然后对每个步骤触发识别和判断,全部步骤遍历后,对整个动作进行有效性确认。如果有效,计数加1;反之就重复上述过程。简言之,智能运动识别与计数是一个状态机。将一个运动动作离散化,抽象成N个状态机,{s(0),s(1),s(2),...,s(n-1)},状态机按照一定的顺序依次进行检测,全部检测到意味用户完成了该动作,对计数加1;若某个状态未被检测到,触发对应反馈信息,重置状态机进入新的循环。每一个状态机对应着一定的触发条件,通过实时骨骼点坐标与状态的循环匹配性检测,获取一个动作匹配结果。

    不难看出,动作识别精度与动作匹配算法紧密相关,算法匹配效果好,识别精度就越高。为提高动作识别精度,可以选取影响匹配算法的因素作为切入点和突破口,骨骼点、状态机、匹配等。相应的解决办法为:

    • 提高骨骼点稳定性,确保状态匹配结果精度。
    • 选择骨骼点稳定、易识别、具有代表性的动作作为状态机。
    • 帧率要能够覆盖一个动作的所有状态机。

    下面将举例进行说明。

    骨骼点识别准确度对动作匹配有着重要影响。如下图所示:测试对象左手臂骨骼点识别出现错误。如果径直进行匹配,显然会得到错误的结果。针对这种情况,应当利用好用户的历史动作信息,在动作匹配算法上对动作匹配进行纠正。

    image.png

    还有一种情况,用户已经完成某种动作的全部动作,如下图中的开合跳,由于采样帧率低,无法捕获和识别全部开合跳运动过程中的全部姿态,造成某个状态匹配不成功,最终导致开合跳动作匹配错误。对于低帧率问题,可从模型和输入源两个方面着手。对于模型来说,在不影响动作识别精度情况下,采用精简模型,减少推理耗时。对不同的终端设备,采用不同分辨率的输入源,降低原始数据处理操作耗时。

    image.png

    降低性能消耗

    受物理条件影响,手机端算力和存储空间有限。此外,深度学习推理本身包含大量的运算,资源消耗大。如果直接在端上进行深度学习推理,再考虑上手机端自身业务(如摄像头、录制视频、动画效果)的资源消耗,CPU和内存开销就显著增长,直观表现是手机发热明显,电量消耗很快。智能运动在端智能上落地时,要特别考虑降低性能消耗,这对于提升用户体验来说至关重要。

    降低整体性能消耗,要追根溯源,从降低单帧消耗处着手。单帧处理可以划分为三个阶段:分别是推理前、推理和推理后。

    image.png

    这三个阶段分别起着不同的作用。推理前阶段主要完成格式转换,将摄像头获取的流数据转换为推理需要的数据格式,如YUV格式、RGBA格式。推理阶段主要完成计算输出骨骼点坐标。对输入的帧数据,经过推理引擎,执行一系列算法,输出推理结果,如姿态识别是将输入图片的RGBA数据转换成骨骼点坐标数据。推理后阶段主要完成展示,进行渲染操作和业务相关操作,如UI展示、动画效果展示。

    相应地,可对上述三个阶段分别进行优化。其中,推理过程中的优化由阿里深度推理引擎MNN负责,这里不作讨论。对于推理前阶段的数据转换,应减少不必要的中间转换环节,直接将摄像头流数据转换成为需要的格式。如推理使用RGBA裸数据,就直接将摄像头流数据转换为RGBA格式。对于推理后阶段,应根据承载的平台选择合适的渲染方案,降低渲染消耗。对于iOS平台,可直接采用Metal进行渲染提效。

    提高测试效率

    AI智能运动是阿里体育团队在体育数字化上的一次大胆尝试。在应用开发特别是测试环节中,投入相当的人力、设备及时间,不断完善应用功能、优化应用性能、提升用户体验。此外,AI运动识别的效果测试受环境因素的影响较大,如光线、背景、距离、人物在摄像头中的成像大小等。这就对测试方式提出了考验。

    以传统测试方案为例:一般是真人、实地、实时动作,测试人员手动记录结果再事后分析,如下图所示。

    image.png

    不难想象,AI智能运动所运行的手机有着不同的品牌、型号、系统版本和性能参数,AI智能运动的用户可能处于不同的使用环境,若采用传统的测试方式,对不同因素进行测试覆盖,对测试人员、测试时间提出了很大的挑战,测试的一致性与精度也难以保证。具体原因如下:

    • 人工成本较高:一次测试需多名同学配合,耗时耗力。
    • 测试环境较单一:无法应对线上复杂多样的环境。
    • 测试结果量化难。无法对模型的精度、算法的效率、动动匹配准确度、精度提升度、性能消耗等量化评估。
    • 问题定位难。事后分析排查,无法复现定位线上客诉问题。

    传统的测试方法难以为继,为克服上述困难,阿里体育技术团队开发了一套AI运动自动测试工具,专门用于解决AI智能项目测试难题,实现了线上问题的快捷定位与回归,并对模型算法精度实现量化评估。

    自动测试工具的解决思路是:批量解析视频集,模拟真实场景,获取骨骼点数据,进行业务结果测试,自动生成测试报告。具体技术方案如下图所示:

    image.png

    采用新的测试工具后,显著地降低了人工成本、提高了测试效率。具体测试效果如下:

    2.gif

    3.gif

    需要注意的是,测试工具的效果与测试样本的数量相关,样本越丰富,测试精度越好。

    四 业务结果

    阿里体育智能运动现已支持数十种运动动作,开发出丰富的AI训练课程,同时通过运动能力的模块化组合,支持未来不断拓展新的动作。

    自AI智能运动诞生以来,乐动力APP陆续上线了直臂开合跳、俯卧撑等上肢动作,臀桥、深蹲等下肢动作以及跳绳、开合跳等全身动作等多种运动形式,使得用户可以不受时间和场地限制,随时随地和朋友一起参与到AI运动,提升了APP的用户吸引力和趣味性。此外,AI训练课程创新引进明星资源,推进全年52周每周7天不间断的“明星陪练”课,以明星带动用户养成运动习惯、快乐运动、爱上运动。阿里体育团队也将不断地根据用户需要打造更多的运动玩法,丰富产品功能,形成阿里体育端智能的独特业务品牌和创新产品特色。

    4.gif

    5.gif

    6.gif

    7.gif

    原文链接
    本文为阿里云原创内容,未经允许不得转载。

    展开全文
  • 最佳运动类APP-Keep原型设计与欣赏

    千次阅读 2018-11-23 09:56:27
    运动类APP是大家手机中必备的款软件。如果说谁手机里没有任何涉及运动类APP,那只能说真的与时代脱轨了。近些年随着物质生活条件的改善,人们开始越来越重视自己的身体,所以也越来越多的人会进行身体锻炼。 尤其...

    运动类APP是大家手机中必备的一款软件。如果说谁手机里没有任何涉及运动类APP,那只能说真的与时代脱轨了。近些年随着物质生活条件的改善,人们开始越来越重视自己的身体,所以也越来越多的人会进行身体锻炼。

    尤其是像我们这种设计师,每天的工作就是坐在电脑前做产品定位分析、用户调研、业务需求分析、目标用户画像、用户需求分析、原型设计,交互设计文档等等。日常的运动更是必不可少。当然,今天不是来和大家说运动的重要性。接下来要做的是带领大家一起来分析和欣赏当今最佳一款运动类APP-Keep的设计,谁叫我这么热爱工作,每天沉迷于工作不能自拔呢,哈哈。希望对交互设计感兴趣的设计师朋友们有所用处。

     

    首先,和大家来简单介绍一下这款最佳运动类APP-Keep。Keep是一款具有社交属性的运动健身类产品。用户可以充分利用碎片化的时间,随时随地选择适合自己的健身课程进行真人同步训练。从2015 年 2 月上线至今,注册用户量已过1亿。在中国手机应用运动类APP指数排名第一(数据取自艾媒北极星)。那到底是什么样的设计造就了如此好的用户体验,从而吸引大量用户注册使用呢。接下来和大家一起一探究竟。

    1.产品定位

    基于健身教学视频的运动类APP,并融合社区、活动挑战、商城、饮食等功能模块,通过提供用户互动及运动装备购买,力求打造线上健身平台的闭环。

    2.用户分析

    ①性别占比

     

    根据艾媒北极星给出的统计数据来看,在使用用户中,男士比例为33.91%,女士比例为66.09%。

    ②年龄占比

     

    根据艾媒北极星给出的统计数据来看,用户年龄集中在30岁以下。

    3.特色功能介绍

    相比其他的运动类APP,在Keep中有一系列形式多样的健身计划、还通过推荐健身搭配饮食来满足用户的其他需求,让用户在健身过程中享受乐趣。

    真人同步训练,记录训练:以视频全程语音督导,自动同步进度。每次训练都会预留休息时间,不需要用户去想,只需跟着视频语音来锻炼,对于用户来说,具有很强的冲击力和趣味性。

    发布个人动态:分享健身成果,拍照记录每一天的变化,分享给好友相互鼓励,让健身不再孤独。

    徽章:通过训练可获得相应等级徽章并一键分享到不同渠道,让用户产生攀比心理,制造目标,促使用户去完成任务获得徽章。

    课程表:针对不同人群,制定不同锻炼任务,以日历的方式显示每天的任务,每次到锻炼时间会及时提醒。切实做到量体裁衣。

    4. 信息架构分析

    5. 原型设计分析及实现技巧

    提到原型设计,原型工具就必不可少了。有人说,我可以使用手绘啊,这个在此不多做评价,有兴

    趣的可以看看我之前的文章(纸上原型设计 VS 桌面原型工具设计,你更喜欢谁?)。当然,个人

    更推荐使用原型设计工具。目前市场上的原型设计工具琳琅满目,在这里,根据个人多年设计经

    验,推荐大家使用国产Mockplus做原型设计。其上手之快,操作之便捷,效率之高,也只有你身体

    力行后才能够体会了。

    废话不多说,先来尝鲜小编以Mockplus制作的运动类APP-Keep的原型。

    成品原型图:

     

    原型在线演示地址:

    https://run.mockplus.cn/tTUqShHAXPfczBUr/index.html

    原型微信扫码演示地址:

    原型设计解读及实现技巧:

    从界面中,我们可以看出,此APP中有4个底部导航菜单(运动、发现、社区、我)。在运动、社区这两个页面的布局均是通过点击标签组切换至不同页面。此外,在单独页面中,内容的布局很多都是一致的。这些在Mockplus中均有封装好的组件和图标,可快速设计出原型。

    ① 底部导航菜单栏设计

    使用“带文字图标”组件,直接在图标库中填充对应图标。布局好之后,直接将其添加至个人的组件库,其余页面直接复用。

    ②菜单内的页面切换

    直接使用标签组配合内容面板,轻松拖拽鼠标即可实现页面切换。(详情见:http://doc.mockplus.cn/?p=387

    ③页面内内容布局设计

    内容页的布局均是一样的,我们只需做好一个,然后直接将其转换为快速格子,快速实现重复布局。而里面的内容及图片,可直接使用自带的填充功能。(详情见:http://doc.mockplus.cn/?p=2811

    运动类APP在市场上还有很多,如小米运动,悦动圈,咕咚等。对交互设计感兴趣的朋友可以选择几款APP使用Mockplus做出交互原型。相信可以为你后期快速将产品构想变为可视界面奠定坚实基础,一起加油吧,设计圈的骚年们!

    展开全文
  • SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Calendar c = Calendar....//过去七c.setTime(new Date());c.add(Calendar.DATE, - 7);Date d = c.getTime();String day = format...
  • 运动时间

    2016-10-05 20:12:01
    有研究表明对于跑步者来说,你跑得最好时间一天当中你的体温最高时。而对于大部分人来说,这个时间会是4PM-7PM。在这个时间段内大部分人跑得更快、更久、更稳定。(注意:如果是跑步运动员,因为大
  • 一天作息时间

    2011-03-06 20:16:00
    一天作息时间
  • 2022年,送给程序员的运动健身知识

    千次阅读 2022-01-03 00:06:31
    经常加班的程序员,没时间喜欢运动,发现身体一天不如一天了。 NO NO NO! 今天就给各位来介绍一些运动科普和运动法方式推荐,帮助各位程序员有个更好的身体。 一般,程序员身体出现问题比较多的就是体能,心肺功能...
  • 运动改变大脑

    千次阅读 2019-06-25 21:17:41
    ,什么时间运动,对大脑的效果最好? 2007年德国科学家发现,和运动前相比,人们在运动后学习词汇的速度提高了20%。 运动优化大脑的作用发生在运动后。 因为一旦运动结束,血液几乎立刻回流至大脑,...
  • 类似于我们学习新的自然技能,用户可以通过改变神经表征来学会控制脑机接口,这过程就是神经适应。从闭环控制的角度出发,我们通过解码器引导脑机接口去适配神经适应。在侵入式运动脑机接口中可以记录多个尺度...
  • 小米运动数据导出并做个性化统计

    千次阅读 2019-09-21 16:41:36
    小米运动数据是没有对外提供接口的,它本身提供的数据统计有不足之处,比如体重数据,在一天中不同时间体重数据时不一样的,把所有数据混合在一起看,是很难得出体重变化趋势的。所以,最好是能够获取到这些数据,...
  • 反之,运动强度较小、持续时间长的运动项目,每分需氧量少,但运动的总氧量却大。 (二) 攝氧量 单位时间内,机体攝取并被实际消耗或利用的氧量称为攝氧量(oxygen uptake)。有时把攝氧量也称为吸氧量(o...
  • 开始运动后,最先消耗的能量是ATP(三磷酸腺苷Adenosine-triphosphate),那些无氧运动,如100米短跑等运动主要使用ATP供能。等到ATP消耗完,接着就会用到储存在肝脏的肝糖,脂肪也逐渐参与供能。跑步20分钟后,脂肪...
  • 深度学习去运动模糊----《DeblurGAN》

    万次阅读 2018-12-12 21:37:44
    前言现实生活中,大多数图片是模糊不清的,...拍照时手抖,或者画面中的物体运动都会让画面模糊,女友辛辛苦苦摆好的各种Pose也将淹没在各种模糊的线条中,是时候要有种新的算法解救水深火热中的你了。这不,去...
  • 下面是上周的作业的要求: 基本功能: 完成个初步的太阳系程序。 场景中有三个球体,个表示太阳,个表示地球,个表示月亮; 地球不停地绕太阳旋转,月亮绕地球旋转。... 如何让物体不停运动呢?vo...
  • 运动目标跟踪()--搜索算法预测模型之KF,EKF,UKF

    万次阅读 多人点赞 2016-06-25 16:33:00
    这里先总体介绍下,原文转自: ...任何传感器,激光也好,视觉也好,整个SLAM系统也好,要解决的问题只有个...换句话说,状态估计问题,也就是“如何最好地使用传感器数据”。可以说,SLAM是状态估计的个特例。   1.
  • 运动生理学 绪论 第节 生命的基本特征 生命体的生命现象主要表现为以下五个方面的基本特征:新陈代谢、兴奋性、应激性、适应性和生殖 、 新陈代谢:是生物体自我更新的最基本的生命活动过程。新陈代谢包括...
  • 点击上方“3D视觉工坊”,选择“星标”干货第一时间送达今天给大家分享篇最新文章,VDO-SLAM :种动态目标感知的视觉SLAM系统,原文名称 VDO-SLAM :A Visual ...
  • 布朗运动、伊藤引理、BS公式

    千次阅读 多人点赞 2020-03-03 17:47:02
    现在,考虑给标准布朗运动加上个仅和时间 t 有关的漂移项 μt,以及个尺度参数 σ,便得到个带漂移的布朗运动(Brownian motion with drift),记作 X(t) = μt + σB(t)。它在任意长度t内的分布满足均值为 μ...
  • 随着当代老年人健康消费需求的持续升级以及越来越多的60后“新老人”的出现,公园散步、打太极拳、跳广场舞等流行的传统运动方式已经越来越难满足现在自我意识觉醒、消费品质升级、追求个性发展的新老年人群!...
  • 晚上出题,看了两三道题,果断选了A题(无限运动传感器节点设计),赛前备赛情况: ADS1292已调通 已完成模拟器的心电信号LCD显示和上位机显示,能够实现稳定的心率测量(使用心电信号模拟器) 接入人体测试时,存在...
  • 这款手表主要面向95后Z世代年轻人群体,定位运动潮流内核,潮流的外观造型和丰富的运动功能上吸引了不少年轻人关注的目光,同时也吸引了小编尝鲜的好奇之心。潮酷外观+丰富表盘 自由定制年轻生活华为Watch GT2e手表...
  • 一天之内有多少次时针分针秒针回重合?分别是什么时候,说出计算方法。 学C#前后不到一个月 今天去面试第一个题目就是这个 不评最佳答案,看重处理方式 纯为交流 更多 分享到: 相关主题推荐: 面试 c# 相关...
  • 其中有4应该进行1小时左右的中等强度运动,而另外两应该进行45分钟左右高强度运动。进行高强度运动时,身体会被迫进入无氧代谢状态。 运动能产生大量的神经元,而环境优化的刺激则有助于神经元的存活 不要在高...
  • 运动模糊的图像修复调研

    千次阅读 多人点赞 2019-01-21 10:17:59
    在用摄像机获取景物图像时,如果在相机曝光期间景物和摄像机之间存在相对运动,例如用照相机拍摄快速运动的物体,或者从行驶中的汽车上拍摄外面静止不动的景物时,拍得的照片都可能存在模糊的现象,这种由于相对运动...
  • python+appium+mobile phone爬取微信运动数据,顺便数据分析波好友们的运动情况 前言 微信运动只能看到前30的数据,超过的会过期而拿不到,我也是现在才知道的。本来还想拿多一点数据的哈哈。 ...
  • 男性一天尿几次算正常

    千次阅读 2014-02-17 16:24:36
     男性一天尿几次正常?男人一天几次小便算正常  吃喝拉撒别看成低俗,可和健康关系最密切.通常喝完一大杯水后,在15分钟内上厕所的男性为7.08%,女性为5.51%;30分钟内男性为22.49%,女性为11.76%;1小时内男性为19....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 40,677
精华内容 16,270
关键字:

一天运动最佳时间