10、实战:通话熄屏功能(上):监听电话状态(TelephonyManager)、结合距离传感器实现通话中自动熄屏。

各位同学,欢迎来到实战环节。

前面几章我们把距离传感器的原理、API 调用、数据校准都讲透了。说实话,光讲理论容易让人犯困。今天咱们直接上手一个真实场景——通话熄屏

这个功能大家每天都在用:打电话时手机贴到耳朵上,屏幕自动黑掉;拿下来,屏幕又亮了。听起来简单,但里面坑不少。我当年第一次做这个功能时,就被各种边缘情况折腾得够呛。

10.1 整体思路:先监听电话,再管传感器

要实现通话熄屏,核心逻辑分两步:

  1. 知道什么时候在通话——用 TelephonyManager 监听电话状态。
  2. 知道手机是否贴近耳朵——用距离传感器判断。

说白了,就是电话一接通,开始看距离传感器;电话一挂断,停止监听传感器。

嗯,这里要注意:顺序不能反。必须先确认电话状态,再操作传感器。否则会出现「明明没在通话,传感器一遮就黑屏」的尴尬情况。

核心流程图:

注册电话状态监听 判断电话状态 是否 通话中? 注册距离传感器 开始监听距离变化 注销距离传感器 释放资源 持续监听电话状态变化

10.2 第一步:用 TelephonyManager 监听电话状态

TelephonyManager 是 Android 系统提供的电话管理服务。通过它,我们能拿到当前的通话状态。

我个人习惯先注册一个 PhoneStateListener,然后在回调里处理三种状态:

  • IDLE:空闲,没有通话。
  • RINGING:来电响铃中。
  • OFFHOOK:通话中(包括拨出和接听)。

来看代码:

// 获取 TelephonyManager 实例
TelephonyManager telephonyManager = 
    (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

// 创建电话状态监听器
PhoneStateListener phoneStateListener = new PhoneStateListener() {
    @Override
    public void onCallStateChanged(int state, String phoneNumber) {
        switch (state) {
            case TelephonyManager.CALL_STATE_IDLE:
                // 空闲状态,注销传感器
                unregisterProximitySensor();
                break;
            case TelephonyManager.CALL_STATE_RINGING:
                // 来电响铃,暂时不处理传感器
                break;
            case TelephonyManager.CALL_STATE_OFFHOOK:
                // 通话中,注册传感器
                registerProximitySensor();
                break;
        }
    }
};

// 开始监听
telephonyManager.listen(phoneStateListener, 
    PhoneStateListener.LISTEN_CALL_STATE);

小提示:记得在 Activity 或 Service 销毁时取消监听,否则会造成内存泄漏。

取消方式:telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);

10.3 第二步:结合距离传感器实现熄屏

电话状态监听搞定了,接下来就是传感器部分。

我在项目中遇到过一个问题:传感器数据变化太快,导致屏幕频繁亮灭。你想想看,打电话时手机稍微晃一下,屏幕就跟着闪,用户体验极差。

解决办法是加一个防抖阈值。传感器值变化后,不要立刻操作屏幕,而是等一小段时间(比如 200ms)再执行。

核心代码:

private SensorEventListener proximityListener = new SensorEventListener() {
    @Override
    public void onSensorChanged(SensorEvent event) {
        float distance = event.values[0];
        float maxRange = event.sensor.getMaximumRange();
        
        // 判断是否贴近
        boolean isNear = distance < maxRange / 2;
        
        // 防抖处理:延迟 200ms 执行
        handler.removeCallbacks(updateScreenTask);
        handler.postDelayed(updateScreenTask, 200);
    }
    
    private Runnable updateScreenTask = new Runnable() {
        @Override
        public void run() {
            if (isNear) {
                // 熄屏
                turnScreenOff();
            } else {
                // 亮屏
                turnScreenOn();
            }
        }
    };
};

private void registerProximitySensor() {
    SensorManager sensorManager = 
        (SensorManager) getSystemService(Context.SENSOR_SERVICE);
    Sensor proximitySensor = 
        sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
    sensorManager.registerListener(proximityListener, 
        proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
}

private void unregisterProximitySensor() {
    SensorManager sensorManager = 
        (SensorManager) getSystemService(Context.SENSOR_SERVICE);
    sensorManager.unregisterListener(proximityListener);
    // 恢复屏幕
    turnScreenOn();
}

警告:熄屏操作需要 WAKE_LOCK 权限,并且要使用 PowerManagergoToSleep() 方法。

亮屏则用 wakeUp() 方法。注意这两个方法都需要 android.permission.DEVICE_POWER 权限,系统应用才能调用。

普通应用建议使用 FLAG_KEEP_SCREEN_ONPROXIMITY_SCREEN_OFF_WAKE_LOCK 来实现类似效果。

10.4 避坑指南:我曾经踩过的三个坑

做这个功能时,我踩过不少坑。挑三个最典型的分享给大家:

现象 原因 解决方案
坑一 通话结束屏幕不亮 挂断时没有注销传感器 在 IDLE 状态强制亮屏
坑二 屏幕频繁闪烁 传感器数据抖动 加 200ms 防抖延迟
坑三 某些手机传感器不工作 硬件差异或驱动问题 加空值判断和降级策略

我曾经在坑一上折腾了一整天。当时测试发现,通话结束后屏幕一直黑着,按电源键也没反应。后来才发现是 unregisterProximitySensor() 里忘了调用 turnScreenOn()。嗯,这种低级错误,犯过一次就再也不会忘了。

10.5 完整代码结构

最后,我把整个逻辑封装成一个工具类,方便大家直接使用:

public class CallScreenManager {
    private Context context;
    private TelephonyManager telephonyManager;
    private SensorManager sensorManager;
    private Sensor proximitySensor;
    private PowerManager powerManager;
    private Handler handler = new Handler();
    
    // 电话状态监听
    private PhoneStateListener phoneStateListener = new PhoneStateListener() {
        @Override
        public void onCallStateChanged(int state, String phoneNumber) {
            switch (state) {
                case TelephonyManager.CALL_STATE_IDLE:
                    unregisterProximity();
                    wakeScreen();
                    break;
                case TelephonyManager.CALL_STATE_OFFHOOK:
                    registerProximity();
                    break;
            }
        }
    };
    
    // 传感器监听
    private SensorEventListener proximityListener = new SensorEventListener() {
        @Override
        public void onSensorChanged(SensorEvent event) {
            float distance = event.values[0];
            float maxRange = proximitySensor.getMaximumRange();
            boolean isNear = distance < maxRange / 2;
            
            handler.removeCallbacksAndMessages(null);
            handler.postDelayed(() -> {
                if (isNear) {
                    sleepScreen();
                } else {
                    wakeScreen();
                }
            }, 200);
        }
        
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {}
    };
    
    // 省略构造方法和具体实现...
}

好了,这一章的内容就到这里。代码逻辑其实不复杂,关键是把「电话状态」和「传感器状态」这两个维度协调好。下一章我们会继续深入,聊聊熄屏时的电源管理优化,以及如何应对各种奇葩机型的兼容性问题。


公众号:蓝海资料掘金营,微信deep3321