AlarmManager(闹钟服务)

使用 AlarmManager (闹钟服务)

AlarmManager 是 Android 提供的一个系统服务类,用于调度定时任务或重复任务(如设置闹钟、定时通知)。它允许开发者在指定时间或周期性地触发 PendingIntent,适合执行后台操作,如发送通知、启动服务或广播。结合 Android 动画合集的背景,AlarmManager 可用于触发动画效果(如定时播放动画或音效),增强用户交互体验。本文将详细介绍 AlarmManager 的功能、使用方法、权限要求及注意事项,重点展示如何设置单次和重复闹钟,并结合动画实现交互反馈。

AlarmManager 的作用与原理

  • 作用AlarmManager 提供定时或周期性任务调度,支持在特定时间或间隔触发 PendingIntent(可启动 Activity、服务或广播)。它适用于需要精确或近似定时执行的任务,如提醒、通知或数据同步。
  • 原理AlarmManager 与 Android 系统时钟和电源管理交互,调度任务即使应用未运行也能执行(通过唤醒设备)。它支持多种闹钟类型(如 RTC、ELAPSED_REALTIME),并与 Doze 模式和权限限制(如 API 31+)紧密相关。
  • 应用场景
  • 定时触发通知(如每日提醒动画)。
  • 周期性执行任务(如同步数据后更新动画)。
  • 结合 VibratorSoundPool 实现闹钟效果。
  • 在特定时间播放动画或音效(如日程提醒)。

权限要求

使用 AlarmManager 的某些功能需要以下权限,在 AndroidManifest.xml 中声明:

<!-- 精确闹钟(API 31+) -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- 接收广播(如果使用 BroadcastReceiver) -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
  • 动态权限(API 31+):
  • SCHEDULE_EXACT_ALARM:设置精确闹钟(API 31+)。
  • 使用 ActivityCompat.requestPermissions 请求。
  • 普通权限(无需动态请求):
  • RECEIVE_BOOT_COMPLETED:设备重启后重新注册闹钟。
  • 注意:API 23+(Android 6.0)引入 Doze 模式,影响非精确闹钟;API 31+(Android 12)对精确闹钟需用户授权。

获取 AlarmManager

通过 Context 获取 AlarmManager 实例:

import android.app.AlarmManager;
import android.content.Context;

AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

常用功能与方法

以下是 AlarmManager 的核心方法:

方法/功能描述API 级别用法示例
set(int type, long triggerAtMillis, PendingIntent operation)设置单次闹钟(API 19 弃用)。1+ (API 19 弃用)alarmManager.set(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
setExact(int type, long triggerAtMillis, PendingIntent operation)设置精确单次闹钟。19+alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
setRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)设置重复闹钟(API 19 弃用)。1+ (API 19 弃用)alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, triggerTime, interval, pendingIntent);
setInexactRepeating(int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)设置非精确重复闹钟(节省电量)。19+alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, triggerTime, interval, pendingIntent);
setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)设置精确闹钟,允许在 Doze 模式下触发。23+alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
cancel(PendingIntent operation)取消指定闹钟。1+alarmManager.cancel(pendingIntent);
canScheduleExactAlarms()检查是否可设置精确闹钟(API 31+)。31+boolean canSchedule = alarmManager.canScheduleExactAlarms();
  • 闹钟类型
  • RTC_WAKEUP:基于实时时钟(RTC),唤醒设备。
  • RTC:基于 RTC,不唤醒设备。
  • ELAPSED_REALTIME_WAKEUP:基于开机时间,唤醒设备。
  • ELAPSED_REALTIME:基于开机时间,不唤醒设备。
  • 时间参数
  • triggerAtMillis:触发时间(System.currentTimeMillis()SystemClock.elapsedRealtime())。
  • intervalMillis:重复间隔(如 AlarmManager.INTERVAL_DAY)。

完整示例(定时闹钟 + 动画 + 通知)

以下是一个使用 AlarmManager 设置定时闹钟的示例,触发通知并结合按钮动画效果。

import android.app.AlarmManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.widget.Button;
import android.widget.Toast;
import android.animation.ObjectAnimator;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;

public class AlarmAnimationActivity extends AppCompatActivity {
    private AlarmManager alarmManager;
    private Button setAlarmButton;
    private ObjectAnimator scaleAnimator;

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

        setAlarmButton = findViewById(R.id.set_alarm_button);
        alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

        // 初始化动画
        scaleAnimator = ObjectAnimator.ofFloat(setAlarmButton, "scaleX", 1f, 1.2f, 1f);
        scaleAnimator.setDuration(200);

        // 创建通知渠道(API 26+)
        createNotificationChannel();

        // 设置闹钟按钮
        setAlarmButton.setOnClickListener(v -> {
            setAlarm();
            scaleAnimator.start();
            Toast.makeText(this, "Alarm set for 10 seconds from now", Toast.LENGTH_SHORT).show();
        });
    }

    private void setAlarm() {
        // 创建 PendingIntent
        Intent intent = new Intent(this, AlarmReceiver.class);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

        // 设置闹钟(10秒后触发)
        long triggerTime = SystemClock.elapsedRealtime() + 10_000; // 10秒后
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            if (alarmManager.canScheduleExactAlarms()) {
                alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime, pendingIntent);
            } else {
                Toast.makeText(this, "Exact alarms not permitted", Toast.LENGTH_SHORT).show();
            }
        } else {
            alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime, pendingIntent);
        }
    }

    private void createNotificationChannel() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                "ALARM_CHANNEL",
                "Alarm Notifications",
                NotificationManager.IMPORTANCE_HIGH
            );
            NotificationManager manager = getSystemService(NotificationManager.class);
            manager.createNotificationChannel(channel);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        scaleAnimator.cancel();
    }

    // 广播接收器处理闹钟触发
    public static class AlarmReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "ALARM_CHANNEL")
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setContentTitle("Alarm Triggered")
                .setContentText("Your alarm has gone off!")
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setAutoCancel(true);

            NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
            manager.notify(1, builder.build());
        }
    }
}
  • 布局 XML(res/layout/activity_alarm_animation.xml):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">
    <Button
        android:id="@+id/set_alarm_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Set Alarm" />
</LinearLayout>
  • AndroidManifest.xml(添加权限和广播接收器):
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application>
    <receiver android:name=".AlarmAnimationActivity$AlarmReceiver"
              android:exported="false" />
</application>
  • 说明
  • 权限:声明 SCHEDULE_EXACT_ALARM 权限,API 31+ 检查 canScheduleExactAlarms()
  • 闹钟:设置 10 秒后触发的单次闹钟,使用 setExactAndAllowWhileIdle(API 23+)以兼容 Doze 模式。
  • 通知:闹钟触发时通过 BroadcastReceiver 发送通知。
  • 动画:点击按钮时触发缩放动画(scaleX)。
  • 生命周期onDestroy 取消动画。

设置重复闹钟

实现每日重复闹钟:

long triggerTime = System.currentTimeMillis() + 10_000; // 10秒后首次触发
long interval = AlarmManager.INTERVAL_DAY; // 每天重复
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, triggerTime, interval, pendingIntent);

取消闹钟

取消指定闹钟:

alarmManager.cancel(pendingIntent);

结合 Vibrator 和 SoundPool

结合 VibratorSoundPool(前文介绍)增强闹钟效果:

public static class AlarmReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            vibrator.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE));
        } else {
            vibrator.vibrate(200);
        }

        SoundPool soundPool = new SoundPool.Builder().setMaxStreams(1).build();
        int soundId = soundPool.load(context, R.raw.alarm_sound, 1);
        soundPool.setOnLoadCompleteListener((pool, sampleId, status) -> {
            pool.play(sampleId, 1.0f, 1.0f, 1, 0, 1.0f);
        });

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "ALARM_CHANNEL")
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle("Alarm with Vibration")
            .setContentText("Your alarm is ringing!")
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setAutoCancel(true);

        NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        manager.notify(1, builder.build());
    }
}

优缺点

  • 优点
  • 支持精确和非精确定时任务。
  • 可唤醒设备执行操作(RTC_WAKEUP)。
  • 兼容 Doze 模式(setExactAndAllowWhileIdle)。
  • 缺点
  • API 31+ 精确闹钟需用户授权。
  • Doze 模式和电池优化可能延迟非精确闹钟。
  • 依赖 PendingIntent,需正确管理。
  • 替代方案
  • WorkManager:适合周期性或延迟任务,兼容 Doze 模式。
  • JobScheduler:API 21+,适合后台任务。
  • Foreground Service:需持续运行的任务。
  • Firebase Cloud Messaging:云端触发的通知。

注意事项

  • 权限:API 31+ 需检查 canScheduleExactAlarms() 并引导用户授权。
  • Doze 模式:API 23+ 的 Doze 和 App Standby 可能延迟非精确闹钟,使用 gars
    使用 setExactAndAllowWhileIdle
  • 设备重启:设备重启后闹钟会丢失,需在 RECEIVE_BOOT_COMPLETED 广播中重新注册。
  • PendingIntent:确保使用 FLAG_IMMUTABLE(API 23+)或 FLAG_UPDATE_CURRENT
  • 调试:通过 Log 检查闹钟触发时间和 PendingIntent 执行情况。
  • 电池优化:避免在受限应用中运行(如电池优化白名单)。

扩展:Jetpack Compose 中的实现

在 Jetpack Compose 中,AlarmManager 可结合 Composable 实现闹钟设置和动画:

import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat

@Composable
fun AlarmScreen() {
    val context = LocalContext.current
    val alarmManager = remember { context.getSystemService(Context.ALARM_SERVICE) as AlarmManager }
    var isAlarmSet by remember { mutableStateOf(false) }
    val scale by animateFloatAsState(
        targetValue = if (isAlarmSet) 1.2f else 1f,
        animationSpec = tween(200)
    )

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Button(
            onClick = {
                val intent = Intent(context, AlarmAnimationActivity.AlarmReceiver::class.java)
                val pendingIntent = PendingIntent.getBroadcast(
                    context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
                )
                val triggerTime = SystemClock.elapsedRealtime() + 10_000 // 10秒后
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
                    // Handle permission
                } else {
                    alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime, pendingIntent)
                    isAlarmSet = true
                }
            },
            modifier = Modifier.scale(scale)
        ) {
            Text("Set Alarm")
        }
    }

    DisposableEffect(Unit) {
        onDispose {
            // Clean up if needed
        }
    }
}
  • 说明
  • 使用 remember 缓存 AlarmManager
  • animateFloatAsState 实现按钮缩放动画。
  • 设置 10 秒后触发的闹钟。
  • DisposableEffect 可用于清理资源(本例无需额外清理)。

示例:结合振动和通知

AlarmReceiver 中添加振动效果:

public static class AlarmReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            vibrator.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE));
        } else {
            vibrator.vibrate(200);
        }

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "ALARM_CHANNEL")
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle("Alarm Triggered")
            .setContentText("Time's up with vibration!")
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setAutoCancel(true);

        NotificationManagerCompat.from(context).notify(1, builder.build());
    }
}

If you need more advanced features (e.g., repeating alarms with custom intervals, integration with WorkManager for complex scheduling, or Canvas-based alarm visualizations) or want to explore other parts of the Android animation collection (e.g., transition animations, Lottie), please let me know!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注