29、综合实战(上):设计一个智能台灯控制App,根据环境光自动调节台灯亮度(模拟)

终于到了综合实战环节。

前面我们学了光线传感器的各种用法,从最基础的数值读取,到数据滤波,再到灵敏度调节。说实话,这些知识点单独拿出来都不难,但真正把它们组合成一个完整的应用,才是考验功底的时候。

这一章,我们就来做一个智能台灯控制App。它能根据环境光的强弱,自动调节台灯的亮度。当然,我们这里用手机屏幕的亮度来模拟台灯亮度——毕竟你手上没有真的台灯硬件,对吧?

项目需求分析

先理清楚我们要做什么。这个App的核心逻辑其实很简单:

  • 输入:光线传感器读取的环境光强度(lux)
  • 处理:根据光照值映射到对应的亮度百分比
  • 输出:调整手机屏幕亮度(模拟台灯亮度)

听起来是不是有点像PID控制器的简化版?嗯,其实没那么复杂。我们只需要一个映射函数,把0~10000 lux的范围映射到0~255的屏幕亮度值。

核心设计思路:环境光越暗,台灯越亮;环境光越亮,台灯越暗。说白了就是补光逻辑——跟人眼的自然需求是一致的。

UI布局设计

我个人习惯先把UI搭好,再写逻辑。这样调试的时候能直观看到效果。

这个App的界面分三个区域:

  1. 状态显示区:显示当前环境光数值和台灯亮度百分比
  2. 模式切换区:手动模式 vs 自动模式
  3. 模拟台灯区:一个圆形区域,背景色随亮度变化

来看看布局代码:

<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="24dp"
    android:background="#FFF8E1">

    <!-- 环境光数值显示 -->
    <TextView
        android:id="@+id/tv_lux"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="环境光:0 lux"
        android:textSize="20sp"
        android:textColor="#333" />

    <!-- 台灯亮度显示 -->
    <TextView
        android:id="@+id/tv_brightness"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="台灯亮度:0%"
        android:textSize="18sp"
        android:textColor="#555"
        android:layout_marginTop="8dp" />

    <!-- 模式切换 -->
    <Switch
        android:id="@+id/switch_mode"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="自动模式"
        android:checked="true"
        android:layout_marginTop="16dp" />

    <!-- 手动亮度滑块 -->
    <SeekBar
        android:id="@+id/seekbar_manual"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="50"
        android:visibility="gone"
        android:layout_marginTop="16dp" />

    <!-- 模拟台灯区域 -->
    <View
        android:id="@+id/view_lamp"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center"
        android:layout_marginTop="32dp"
        android:background="@drawable/circle_lamp" />

</LinearLayout>

小技巧:手动模式下的SeekBar默认隐藏,只有切换到手动模式时才显示。这样UI更干净,用户不会困惑。

核心逻辑实现

接下来是重头戏——光线传感器监听和亮度映射。

我曾在项目里踩过一个坑:直接拿原始lux值去映射亮度,结果在弱光环境下屏幕疯狂闪烁。为什么呢?因为光线传感器在低光照下数值波动很大,哪怕只是稍微动一下手机,数值就跳几十个单位。

所以这里我们引入一个简单的平滑处理:取最近5次读数的平均值。

public class SmartLampActivity extends AppCompatActivity {

    private SensorManager sensorManager;
    private Sensor lightSensor;

    private TextView tvLux, tvBrightness;
    private Switch switchMode;
    private SeekBar seekBarManual;
    private View viewLamp;

    private float currentLux = 0f;
    private int currentBrightness = 0;

    // 平滑缓冲区
    private float[] luxBuffer = new float[5];
    private int bufferIndex = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initViews();
        initSensor();
    }

    private void initViews() {
        tvLux = findViewById(R.id.tv_lux);
        tvBrightness = findViewById(R.id.tv_brightness);
        switchMode = findViewById(R.id.switch_mode);
        seekBarManual = findViewById(R.id.seekbar_manual);
        viewLamp = findViewById(R.id.view_lamp);

        switchMode.setOnCheckedChangeListener((buttonView, isChecked) -> {
            seekBarManual.setVisibility(isChecked ? View.GONE : View.VISIBLE);
            if (isChecked) {
                // 切回自动模式,立即更新一次
                updateLampBrightness(currentLux);
            }
        });

        seekBarManual.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (!switchMode.isChecked()) {
                    setScreenBrightness(progress);
                    updateLampUI(progress);
                }
            }
            @Override public void onStartTrackingTouch(SeekBar seekBar) {}
            @Override public void onStopTrackingTouch(SeekBar seekBar) {}
        });
    }

    private void initSensor() {
        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
        if (lightSensor == null) {
            tvLux.setText("设备不支持光线传感器");
            return;
        }
        sensorManager.registerListener(lightSensorListener, lightSensor,
                SensorManager.SENSOR_DELAY_NORMAL);
    }

    private final SensorEventListener lightSensorListener = new SensorEventListener() {
        @Override
        public void onSensorChanged(SensorEvent event) {
            float rawLux = event.values[0];
            // 平滑处理
            luxBuffer[bufferIndex % 5] = rawLux;
            bufferIndex++;
            float sum = 0;
            int count = Math.min(bufferIndex, 5);
            for (int i = 0; i < count; i++) {
                sum += luxBuffer[i];
            }
            currentLux = sum / count;

            runOnUiThread(() -> {
                tvLux.setText(String.format("环境光:%.1f lux", currentLux));
                if (switchMode.isChecked()) {
                    updateLampBrightness(currentLux);
                }
            });
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
            // 精度变化时一般不需要处理
        }
    };

    private void updateLampBrightness(float lux) {
        // 映射逻辑:0 lux -> 255亮度,10000+ lux -> 0亮度
        int brightness;
        if (lux <= 0) {
            brightness = 255;
        } else if (lux >= 10000) {
            brightness = 0;
        } else {
            // 线性映射,取反
            brightness = (int) (255 - (lux / 10000f) * 255);
        }

        // 限制范围
        brightness = Math.max(0, Math.min(255, brightness));

        currentBrightness = brightness;
        int percent = (int) (brightness / 255f * 100);

        tvBrightness.setText("台灯亮度:" + percent + "%");
        setScreenBrightness(brightness);
        updateLampUI(brightness);
    }

    private void setScreenBrightness(int brightness) {
        WindowManager.LayoutParams lp = getWindow().getAttributes();
        lp.screenBrightness = brightness / 255f;
        getWindow().setAttributes(lp);
    }

    private void updateLampUI(int brightness) {
        // 亮度越高,背景色越接近白色;越低越接近暗黄色
        int alpha = brightness;
        int color = Color.argb(alpha, 255, 220, 100);
        viewLamp.setBackgroundTintList(ColorStateList.valueOf(color));
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (sensorManager != null) {
            sensorManager.unregisterListener(lightSensorListener);
        }
    }
}

映射逻辑详解

这段代码里最核心的就是updateLampBrightness()方法。我解释一下映射思路:

环境光(lux) 台灯亮度(0~255) 说明
0 255 全黑环境,台灯最亮
500 ~242 微弱光,台灯依然很亮
2000 ~204 室内正常光照,中等亮度
5000 ~127 较亮环境,台灯半亮
10000+ 0 强光环境,台灯关闭

注意:这个映射是线性的,实际产品中可能需要用曲线映射。比如人眼对暗部变化更敏感,所以低光照区域应该用更缓的斜率。不过作为入门教程,线性映射已经够用了。

知识体系结构图

下面这张图概括了整个智能台灯App的架构:

智能台灯控制App架构图 光线传感器 输入:环境光 lux 值 数据处理层 平滑滤波(5次均值) 线性映射:lux → 亮度值(0~255) 输出层 屏幕亮度调节(模拟台灯) UI圆形区域颜色变化 手动模式 SeekBar 控制 自动模式 传感器自动调节

避坑指南

写这个Demo的时候,有几个地方特别容易翻车:

  • 传感器注册时机:一定要在onResume()里注册监听,在onPause()里取消注册。否则App切到后台还在耗电,用户会骂的。
  • 屏幕亮度权限:修改系统屏幕亮度需要WRITE_SETTINGS权限。不过我们这个Demo只修改当前Activity的亮度,不需要系统权限——用WindowManager.LayoutParams.screenBrightness就行。
  • 数值溢出:我见过有人直接写brightness = 255 - lux / 10000 * 255,结果因为整数除法,lux小于10000时结果永远是255。记得用浮点数运算。

扩展思路:如果你手头有Arduino或ESP32,完全可以把这套逻辑移植过去,用PWM控制真实的LED灯。手机端只做显示和控制,硬件端负责执行——这才是真正的智能台灯。

好了,这一章我们完成了智能台灯的核心逻辑。下一章我们会加入更多实用功能,比如亮度曲线优化、日志记录、以及更友好的交互反馈。嗯,先消化这些内容吧。


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