Android转屏流程与优化(Google转屏算法)

Android转屏流程与优化(Google转屏算法)

1. 转屏慢与整体分析思路

我们用手机转屏的时候,会发现有时候转屏特别慢,快的和慢的会相差1倍,例如快的转屏700ms,慢的转屏会1.2s以上.

目前分析思路是:
1、gsenor上报数据是否有延迟?
2、gsensor上报的数据准确后,google的算法需要多长时间才能转换为方向改变?
3、上层处理转屏时间是多长时间?即冻屏时间是多长
4、转屏动画的处理时长?

第一个问题属于sensor部分,我们暂时不讨论。
第二个问题属于Google转屏算法部分,也是我们本次将要重点讨论的内容。
第三方问题属于framework和view layer界面层级部分,这部分属于项目问题,不讨论
第四个问题,这个很好理解,默认android转屏动画都是400ms,大家可以根据项目要求自定义修改,这个目前市面上的很多大厂家都有做优化,修改也比较简单,此处也不过多阐述。

2. Google转屏算法

为了能够优化该类问题,大家就不得不去研究一下Google的转屏算法的实现,只有了解其实现原理才能谈的上如何去优化.
代码路径=>
WindowOrientationListener.java (frameworks\base\services\core\java\com\android\server\policy)
这部分算法google很少改动,目前看android 6.0, android 7.0, android 8.0/8.1几乎都没怎么改动过。

内容的话看懂了就还是比较简单的,逻辑都只在一个文件中。

2.1 数据来源

转屏识别方向是通过gsensor重力传感器来实现的,那么数据肯定是从gsensor重力传感器传上来的,这里略过,我们之间到google开始识别部分,看看传递上来给上层识别的数据是什么?

    private static final int ACCELEROMETER_DATA_X = 0;
    private static final int ACCELEROMETER_DATA_Y = 1;
    private static final int ACCELEROMETER_DATA_Z = 2;

    //sensor数据改变会传递SensorEvent就是传感器的事件
    public void onSensorChanged(SensorEvent event) {
        //...
        //此处获取x,y,z三个轴的加速度的数据
        float x = event.values[ACCELEROMETER_DATA_X];
        float y = event.values[ACCELEROMETER_DATA_Y];
        float z = event.values[ACCELEROMETER_DATA_Z];

        //此处获取事件上报时间的数据
        final long now = event.timestamp;

目前Android使用的gsensor数据有3个就是底层传递上来的x、y、z三个坐标轴的加速度方向。
本来gsensor的数据只有一个就是9.8 m/s^2,垂直往下。
但是由于设备(不管是手机还是gsensor)是空间上有3个方向,在一个立体的空间描述一个分类,可以拆分成x、y、z轴的矢量叠加。
至于上报时间这个事件数据是android自身设置,不属于gsensor提供的数据

2.2 识别x,y,z轴

根据sensor传递上来的event事件,由于sensor内部刷新速度可以很快200Hz(5ms一次),需要先进行初步判断和转换

    //当前事件的时间
    final long now = event.timestamp;
    //上一次事件的时间
    final long then = mLastFilteredTimestampNanos;
    //两次事件间隔,单位是ms,一般默认SENSOR_DELAY_NORMAL时,gsensor上报数据是66.666ms左右
    final float timeDeltaMS = (now - then) * 0.000001f;
    //是否需要跳过
    final boolean skipSample;

    //判断是否无效事件,无效重新reset
    if (now < then
            || now > then + MAX_FILTER_DELTA_TIME_NANOS
            || (x == 0 && y == 0 && z == 0)) {
        //清除之前的数据,重新开始
        resetLocked();
        skipSample = true;
    } else {
        //FILTER_TIME_CONSTANT_MS=200ms,timeDeltaMS目前应该是66.666ms左右,alpha= 0.25左右
        //x,y,z渐变正常都是以0.25的速率改变
        final float alpha = timeDeltaMS / (FILTER_TIME_CONSTANT_MS + timeDeltaMS);
        x = alpha * (x - mLastFilteredX) + mLastFilteredX;
        y = alpha * (y - mLastFilteredY) + mLastFilteredY;
        z = alpha * (z - mLastFilteredZ) + mLastFilteredZ;
        //...
        skipSample = false;
    }

x,y,z渐变正常都是以0.25的速率改变。
取部分正常转屏的日志,如下面数据
x = alpha * (x - mLastFilteredX) + mLastFilteredX;
mLastFilteredX是上一个转换后的x=2.6531296,右边的运算x是未转换的x=9.012,左边的x是转换后的x=4.230438,带入上述公式
4.230438 = alpha*(9.012 - 2.6531296) + 2.6531296
alpha = 0.248

//上一次转后的数据
14:20:45.056 Filtered acceleration vector: x=2.6531296, y=9.352881, z=2.019604, magnitude=9.9

//gsensor当前上报的数据
14:20:45.122 Raw acceleration vector: x=9.012, y=-13.896, z=-1.436, magnitude=16.6
//当前gsensor的数据转换后的最新x、y、z轴数据
14:20:45.122 Filtered acceleration vector: x=4.230438, y=3.5860295, z=1.1624464, magnitude=5.6

x,y,z 3轴禁止不动时的值范围是(0-9.8),代表加速度方向,加速度方向是9.8,指向的是垂直往下。
x轴是左边正方向,y轴是竖直向上正方向,z轴是屏幕向上是正方向,
如下图所示

                   y轴            z轴
                   -(0,9.8,0)    -(0,0,9.8) 
                   -           -
                   -         -
                   -       -
                   -     -
x轴(9.8,0,0)--------(x,y,z)(0,0,0)

最终如果没有额外加速度影响,合成的值是大概9.8 m/s^2

2.3 判断x,y,z轴是否有效

x、y、z三轴的矢量怎么转换成一个方向的矢量呢?
在数学算法中求该合成矢量的方法很简单:Math.sqrt(x * x + y * y + z * z),求(x * x + y * y + z * z)方差。(如果是直角三角形,最长边的长度L*L = x * x + y * y => L = Math.sqrt(x * x + y * y)这个大家很好理解,加多一个立体面的z轴其实是一个意思)。

   //求x,y,z轴的方差,也就是距离(0,0,0)点的距离,这就的距离就是加速度,0点代表地面
    final float magnitude = (float) Math.sqrt(x * x + y * y + z * z);
    //NEAR_ZERO_MAGNITUDE=1,加速度小于1的可以忽略
    //在什么情况下会出现这种事情呢,你拿着手机往地上摔的时候这个就是0啦,小心手机别摔坏了~~
    if (magnitude < NEAR_ZERO_MAGNITUDE) {
        if (LOG) {
            Slog.v(TAG, "Ignoring sensor data, magnitude too close to zero.");
        }
        clearPredictedRotationLocked();
    } else {
        //在小于5.8(9.8-4)或者大于13.8(9.8+4),代表手机在加速(往一个方向加速)
        //垂直向下加速是小于9.8,垂直向上加速是大于9.8
        //非常缓慢转屏的话是9.8左右,你没有加速度
        if (isAcceleratingLocked(magnitude)) {
            isAccelerating = true;
            mAccelerationTimestampNanos = now;
        }

1) 当加速度小于1的时候,相当于自由落体,google算法部分会忽略。
2) 此处既然获取了加速度,顺便对加速的逻辑进行的判断,若总的加速度小于5.8(9.8-4)或者大于13.8(9.8+4),代表手机整体的加速度是有较大的变化,简而言之,说明手机有加速了

2.4 计算z轴倾斜角度

magnitude是距离(0,0,0)的长度(代表加速度方向与大小),z代表z轴加速度,Math.asin(z / magnitude)得出的就是偏向z轴的弧度(根据直角三角形的sin、cos、tan、cot是可以知道该夹角的角度的,如果不了解该逻辑的同学可以百度一下,或者翻一翻中学时度的数学书…),转换成角度就是tiltAngle。

    //RADIANS_TO_DEGREES根据弧度换算成角度的的比例,代表每弧度等于多少角度
    private static final float RADIANS_TO_DEGREES = (float) (180 / Math.PI);

    //magnitude是距离(0,0,0)的长度(代表加速度方向与大小),z代表z轴加速度
    final int tiltAngle = (int) Math.round(Math.asin(z / magnitude) * RADIANS_TO_DEGREES);

asin(x)是求反正玄函数,得到的值是弧度,取值x范围是-1<=x<=1
如:
pi=3.14159265359…弧度,或者说是180度
a = sin(pi/6) = sin(30度) = sin(0.5236弧度) = 1/2 = 0.5
b = asin(a) = pi/6 = 0.5236弧度

sin(30度) = 0.5这个大家应该很熟悉吧,不熟悉还是那就话,先翻一下中学数学课本…

2.5 判断是否平躺着,是否有垂直摇摆晃动

1) 判断是否有平躺,条件:z轴角度tiltAngle大于80度,而且时间大于1s
2) 是否有垂直摇摆晃动,条件:取300ms内的数据,z轴角度变化相差超过20度代表有晃动

    //是否有平躺,z轴角度tiltAngle大于80度,而且时间大于1s
    if (isFlatLocked(now)) {
        //设置平躺标签为true
        isFlat = true;
        //设置平躺触发时间
        mFlatTimestampNanos = now;
    }

    //是否有垂直摇摆晃动,取300ms内的数据,z轴角度变化相差超过20度代表有晃动
    if (isSwingingLocked(now, tiltAngle)) {
        //设置摇摆晃动标签为true
        isSwinging = true;
        //设置摇摆晃动触发时间
        mSwingTimestampNanos = now;
    }

2.6 垂直倾斜角度过滤

1) 当垂直倾斜角度在一定范围内会给过滤掉,即该值不会用于方向识别,进入忽略条件tiltAngle<=-40(代表手机盖住了),退出忽略条件是tiltAngle>=-15
2) z倾斜轴角度大于80度的时候,不管是盖住还是朝上放着,都会忽略掉

    private static final int TILT_OVERHEAD_ENTER = -40;
    private static final int TILT_OVERHEAD_EXIT = -15;
    private static final int MAX_TILT = 80;

    //进入忽略条件tiltAngle<=-40(代表手机盖住了
    if (tiltAngle <= TILT_OVERHEAD_ENTER) {
        mOverhead = true;
    //退出忽略条件是tiltAngle>=-15
    } else if (tiltAngle >= TILT_OVERHEAD_EXIT) {
        mOverhead = false;
    }

    //忽略
    if (mOverhead) {
        clearPredictedRotationLocked();
    //z倾斜轴角度大于80度的时候,不管是盖住还是朝上放着,都会忽略掉
    } else if (Math.abs(tiltAngle) > MAX_TILT) {
        clearPredictedRotationLocked();
    }

2.7 识别转屏角度

1) 识别转屏角度orientationAngle,通过反正切得到角度
2) 将转屏角度标准化到0-360度

    //通过反正切得到角度
    int orientationAngle = (int) Math.round(
            -Math.atan2(-x, y) * RADIANS_TO_DEGREES);

    //角度转换成0-360度
    if (orientationAngle < 0) {
        // atan2 返回的是 [-180, 180]; 标准化到 [0, 360]
        orientationAngle += 360;
    }

2.8 构建最接近的方向

构建最接近的方向nearestRotation,
orientationAngle取值是0-360,nearestRotation取值是0-3.
逆时针从竖屏方向旋转分别是:
0(垂直正常手握方向) - 1(左侧横屏) - 2(垂直反方向,手机倒着拿的方向) - 3(右侧横屏)

每个方向对应的真实角度是:
0 => 0
1 => 90
2 => 180
3 => 270

    //如该角度小于某个真实物理方向45度以内的,都算在该物理方向内
    int nearestRotation = (orientationAngle + 45) / 90;
    if (nearestRotation == 4) {
        nearestRotation = 0;
    }

构建后各个角度的最接近方向如下:
0-45&&315-360 => 0
45-135 => 1
135-225 => 2
225-315 => 3

2.9 判断数据是否有效

这里主要是2个逻辑
1) 判断tilt倾斜角度,tiltAngle正值代表屏幕向上,负值代表屏幕向下.如方向0时(正常竖屏)倾斜角度不能超过:屏幕向上不能超过70度,屏幕盖住不能超过-25度
2) 判断旋转方向是否可以接受,此处旋转角度必须接近实际角度(0,90,180,270)的25度以内才算方向改变

    //判断tilt倾斜角度,tiltAngle正值代表屏幕向上,负值代表屏幕向下
    private boolean isTiltAngleAcceptableLocked(int rotation, int tiltAngle) {
        return tiltAngle >= mTiltToleranceConfig[rotation][0]
                && tiltAngle <= mTiltToleranceConfig[rotation][1];
    }

    //上述举个例子,如方向0时(正常竖屏)倾斜角度不能超过:屏幕向上不能超过70度,屏幕盖住不能超过-25度
    private final int[][] mTiltToleranceConfig = new int[][] {
        /* ROTATION_0   */ { -25, 70 }, 
        /* ROTATION_90  */ { -25, 65 },
        /* ROTATION_180 */ { -25, 60 },
        /* ROTATION_270 */ { -25, 65 }
    };

    //isOrientationAngleAcceptableLocked判断方向是否可以接受,
    //旋转角度必须接近实际角度(0,90,180,270)的25度以内才算方向改变
    //rotation是本次最接近的方向 nearestRotation(0,1,2,3)(0,90,180,270),
    //orientationAngle是本身实际的方向(0-360),nearestRotation对应的角度如下:
    //0-45&&315-360 => 0
    //45-135 => 1
    //135-225 => 2
    //225-315 => 3
    private static final int ADJACENT_ORIENTATION_ANGLE_GAP = 45;

    private boolean isOrientationAngleAcceptableLocked(int rotation, int orientationAngle) {
        //mCurrentRotation是上一次的方向,rotation是当前最接近的方向
        final int currentRotation = mCurrentRotation;
        if (currentRotation >= 0) {
            //方向不变化或者,方向往逆时针加90旋转
            if (rotation == currentRotation
                    || rotation == (currentRotation + 1) % 4) {
                //也就是0-360减去22.5度,(0,1,2,3)(-22.5,67.5,157.5,247.5)
                int lowerBound = rotation * 90 - 45
                        + ADJACENT_ORIENTATION_ANGLE_GAP / 2;

                //如果当前最接近的方向是竖直方向(0-45&&315-360)
                if (rotation == 0) {
                    //lowerBound=-22.5,也就是315 <= orientationAngle < 337.5范围内会给丢弃
                    //也就是说从270度(右侧横屏幕)转到0度或360度(竖直方向),会将0的范围缩短为(0-45&&337.5-360)
                    if (orientationAngle >= 315 && orientationAngle < lowerBound + 360) {
                        return false;
                    }
                //如果是其它方向
                } else {
                    //这个是上面得到的,也就是与实际方向相差25度以内的才会给使用
                    //0-45&&337.5-360 => 0
                    //下面的是本次得到
                    //67.5-135 => 1
                    //157.5-225 => 2
                    //247.5-315 => 3
                    //相差25度以上的丢弃
                    if (orientationAngle < lowerBound) {
                        return false;
                    }
                }
            }

            //顺时针旋转,与上面逻辑类似
            if (rotation == currentRotation
                    || rotation == (currentRotation + 3) % 4) {
                //也就是0-360加上22.5度,(0,1,2,3)(22.5,112.5,202.5,292.5)
                int upperBound = rotation * 90 + 45
                        - ADJACENT_ORIENTATION_ANGLE_GAP / 2;
                //如果当前最接近的方向是竖直方向(0-45&&315-360)
                if (rotation == 0) {
                    //也就是说从90度(左侧横屏幕)转到0度或360度(竖直方向),会将0的范围缩短为(22.5-45&&315-360)
                    if (orientationAngle <= 45 && orientationAngle > upperBound) {
                        return false;
                    }
                } else {
                    //这个是上面得到的,也就是与实际方向相差25度以内的才会给使用
                    //22.5-45&&315-360 => 0
                    //下面的是本次得到
                    //45-112.5 => 1
                    //135-202.5 => 2
                    //225-292.5 => 3
                    //相差25度以上的丢弃
                    if (orientationAngle > upperBound) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

2.10 判断是否需要更新方向信息

上面已经获得了方向信息,那是否需要更新还是有逻辑进行控制:
1) 当前角度需要保持40ms以上
2) 从手机平放着拿起需要500ms才会转屏
3) 晃动后300ms内都不能转屏
4) 加速转动的时候500ms都不能转屏
5) 手没有触摸屏幕500ms才能转屏

    //判断当前的方向改变是否需要更新到系统
    private boolean isPredictedRotationAcceptableLocked(long now) {
        //mPredictedRotationTimestampNanos是上一次更新有效数据的时间(方向改变才会设置),
        //在上面的updatePredictedRotationLocked设置过
        //当前角度需要保持40ms以上
        if (now < mPredictedRotationTimestampNanos + PROPOSAL_SETTLE_TIME_NANOS) {
            return false;
        }

        //从手机平放着拿起需要500ms才会转屏
        if (now < mFlatTimestampNanos + PROPOSAL_MIN_TIME_SINCE_FLAT_ENDED_NANOS) {
            return false;
        }

        //晃动后300ms内都不能转屏
        if (now < mSwingTimestampNanos + PROPOSAL_MIN_TIME_SINCE_SWING_ENDED_NANOS) {
            return false;
        }

        //加速转动的时候500ms都不能转屏
        if (now < mAccelerationTimestampNanos
                + PROPOSAL_MIN_TIME_SINCE_ACCELERATION_ENDED_NANOS) {
            return false;
        }

        //手没有触摸屏幕500ms才能转屏
        if (mTouched || now < mTouchEndedTimestampNanos
                + PROPOSAL_MIN_TIME_SINCE_TOUCH_END_NANOS) {
            return false;
        }

        // Looks good!
        return true;
    }

从上述我们可以看到,就算识别了方向,也还需要一段时间才让系统去更新。

3. 优化方案

从google算法,我们可以知道其有2个比较大的缺陷:
1) 通过重力传感器传来的x,y,z轴的加速度合成之后只有一个垂直往下的加速度,如果此时用户在别的方向上有加速度,那么通过反余弦、反正切等计算的角度就会不准确,原始的角度计算有问题,就谈不上识别正确的方向了。而google在旋转角度上,并未用代码进行修正。
2) google拿到方向信息之后,去干扰采取的措施是延迟300-500ms,等彻底没有干扰的时候再进行方向改变,这个方案很保守。由于普通用户正常转屏都会触发抖动和加速(70%左右都会触发),这样就会导致,就算是旋转角度计算没有任何问题,也会由于算法导致300-500ms的延迟。

上述2个缺陷有兴趣的同学可以自己去修正它。

在这里提供一个简单且容易实现的方案:
对第2)点google延迟的缺陷进行修正=>
1) 保留之前N个旋转角度数据(N可以自己根据实际调整)。
2) 增加开始转屏逻辑(给个提示:旋转的时候角度会往一个方向增加/减少,那么单位时间内增加/减少多少角度可以作为一个评判标准;开始和结束的位置逻辑可以做为另一个评判标准)。
3) 增加转屏完成的逻辑(给个提示:转屏完成之后角度变化会比较缓慢或者不动,其会在实际角度周围)。

增加上述逻辑对Google的延迟进行修正,这种做法属于保守修正,可以优化转屏速度大概200-400ms左右。这里不提供实际代码,仅供思路参考。

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值