You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android通知中Chronometer与ProgressBar应用关闭后异常问题求助

解决Android自定义通知中Chronometer和ProgressBar在应用被杀后行为异常的问题

问题原因

自定义通知里的Chronometer和ProgressBar由SystemUI进程托管,但它们的更新逻辑默认依赖你的应用进程。当应用被关闭或杀死后,进程终止,系统无法继续维护这两个控件的计时/进度更新,导致行为异常。

解决方案

使用**前台服务(Foreground Service)**托管通知,前台服务拥有更高的系统优先级,不易被杀死,可在后台持续运行并定时更新通知的RemoteView内容,确保Chronometer和ProgressBar正常工作。

步骤1:创建前台服务

创建Foreground Service,负责初始化通知、定时更新Chronometer剩余时间和ProgressBar进度:

class NotificationUpdateService : Service() {
    private lateinit var notificationManager: NotificationManager
    private lateinit var collapsedView: RemoteViews
    private lateinit var expandedNotifView: RemoteViews
    private var notificationId = 123
    private var remainingTime: Long = 0L
    private val handler = Handler(Looper.getMainLooper())
    private val updateRunnable = object : Runnable {
        override fun run() {
            remainingTime -= 1000
            if (remainingTime <= 0) {
                // 倒计时结束,清理通知并停止服务
                stopForeground(STOP_FOREGROUND_REMOVE)
                stopSelf()
                return
            }

            // 手动格式化剩余时间并更新Chronometer文本
            val timeStr = String.format("%tH:%tM:%tS", Date(remainingTime), Date(remainingTime), Date(remainingTime))
            collapsedView.setTextViewText(R.id.myChronometere, timeStr)
            expandedNotifView.setTextViewText(R.id.myChronometere, timeStr)

            // 更新ProgressBar(示例总时长为1小时,需根据实际需求修改)
            val totalDuration = 3600000L
            val progress = ((totalDuration - remainingTime) * 100 / totalDuration).toInt()
            collapsedView.setProgressBar(R.id.myProgressBar, 100, progress, false)
            expandedNotifView.setProgressBar(R.id.myProgressBar, 100, progress, false)

            // 更新通知
            notificationManager.notify(notificationId, buildNotification())
            handler.postDelayed(this, 1000)
        }
    }

    override fun onCreate() {
        super.onCreate()
        notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        // 初始化折叠和展开状态的RemoteView
        collapsedView = RemoteViews(packageName, R.layout.custom_notification_id27_collapsed)
        expandedNotifView = RemoteViews(packageName, R.layout.custom_notification_id27)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 接收传递的参数
        remainingTime = intent?.getLongExtra("remaining_time", 0L) ?: 0L
        val heading = intent?.getStringExtra("heading") ?: ""
        val content = intent?.getStringExtra("content") ?: ""
        val img = intent?.getParcelableExtra<Bitmap>("img")

        // 初始化RemoteView的文本、图片和点击事件
        initRemoteViewContent(heading, content, img, intent)

        // 启动前台服务
        startForeground(notificationId, buildNotification())
        // 开始定时更新
        handler.post(updateRunnable)

        return START_STICKY
    }

    private fun initRemoteViewContent(heading: String, content: String, img: Bitmap?, intent: Intent?) {
        // 设置展开视图内容
        expandedNotifView.setTextViewText(R.id.notification_content_title3, heading)
        expandedNotifView.setTextViewText(R.id.notification_content_text3, content)
        img?.let { expandedNotifView.setImageViewBitmap(R.id.productImageView_large, it) }
        expandedNotifView.setViewVisibility(R.id.productImageView_large, View.VISIBLE)
        expandedNotifView.setViewVisibility(R.id.rlFlipper, View.GONE)
        expandedNotifView.setViewVisibility(R.id.viewFlipperLarge, View.GONE)

        // 设置图片点击事件
        val imgIntent = GcmIntentService.setSlotIntentTemplate(
            this,
            intent?.getStringExtra("uuid") ?: "",
            intent?.getStringExtra("link") ?: "",
            GcmIntentService.CTA_ACTION_TEMPLATE_27,
            intent?.getStringExtra("tId") ?: "",
            "",
            "0",
            intent?.getStringExtra("fullImg") ?: ""
        )
        imgIntent.putExtra("notificationId", notificationId)
        val contentIntentImg = PendingIntent.getActivity(this, notificationId, imgIntent, PendingIntent.FLAG_IMMUTABLE)
        expandedNotifView.setOnClickPendingIntent(R.id.productImageView_large, contentIntentImg)

        // 设置前后导航点击事件
        val clickIntentNext = intent?.getParcelableExtra<Intent>("clickIntentNext") ?: Intent()
        val contentIntentNext = PendingIntent.getBroadcast(this, notificationId, clickIntentNext, PendingIntent.FLAG_IMMUTABLE)
        expandedNotifView.setOnClickPendingIntent(R.id.imgNext, contentIntentNext)
        expandedNotifView.setViewVisibility(R.id.imgNext, View.VISIBLE)
        collapsedView.setViewVisibility(R.id.imgNext, View.VISIBLE)

        val clickIntentPrev = intent?.getParcelableExtra<Intent>("clickIntentPrev") ?: Intent()
        val contentIntentPrev = PendingIntent.getBroadcast(this, notificationId, clickIntentPrev, PendingIntent.FLAG_IMMUTABLE)
        expandedNotifView.setOnClickPendingIntent(R.id.imgPrev, contentIntentPrev)
        expandedNotifView.setViewVisibility(R.id.imgPrev, View.VISIBLE)
        collapsedView.setViewVisibility(R.id.imgPrev, View.VISIBLE)

        // 设置折叠视图内容
        collapsedView.setTextViewText(R.id.notification_content_title3, heading)
        collapsedView.setTextViewText(R.id.notification_content_text3, content)
        collapsedView.setViewVisibility(R.id.downArrowImg, View.VISIBLE)
    }

    private fun buildNotification(): Notification {
        return NotificationCompat.Builder(this, getString(R.string.default_notification_channel_id))
            .setSmallIcon(R.drawable.material_sd_notificationicon)
            .setColor(resources.getColor(R.color.notificationb_bg_color))
            .setCustomContentView(collapsedView)
            .setCustomBigContentView(expandedNotifView)
            .setPriority(NotificationCompat.PRIORITY_MAX)
            .setOnlyAlertOnce(true)
            .setOngoing(true)
            .build()
    }

    override fun onDestroy() {
        super.onDestroy()
        handler.removeCallbacks(updateRunnable)
    }

    override fun onBind(intent: Intent?): IBinder? = null
}

步骤2:启动前台服务

在原创建通知的位置,替换为启动前台服务并传递必要参数:

// 保留原有剩余时间计算逻辑
val date = format.parse(exptime)
val elapsed = now.time - date.time
val remainingTime = startTime - elapsed

// 构造启动服务的Intent
val serviceIntent = Intent(context, NotificationUpdateService::class.java).apply {
    putExtra("remaining_time", remainingTime)
    putExtra("heading", heading)
    putExtra("content", content)
    putExtra("img", img)
    putExtra("uuid", uuid)
    putExtra("link", link)
    putExtra("tId", tId)
    putExtra("fullImg", fullImg)
    putExtra("clickIntentNext", clickIntentNext)
    putExtra("clickIntentPrev", clickIntentPrev)
}

// 适配Android版本启动前台服务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    context.startForegroundService(serviceIntent)
} else {
    context.startService(serviceIntent)
}

步骤3:声明权限和服务

在AndroidManifest.xml中添加权限并注册服务:

<!-- 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 12及以上需要通知权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!-- 注册服务 -->
<service
    android:name=".NotificationUpdateService"
    android:exported="false" />

关键说明

  • 放弃依赖RemoteView中Chronometer的自动计时,改为手动计算剩余时间并更新文本,避免系统进程无法维护计时的问题。
  • 前台服务持续在后台运行,定时更新通知内容,确保应用被杀后控件仍能正常工作。
  • START_STICKY参数确保服务被系统杀死后会尝试重启,提升稳定性。

内容的提问来源于stack exchange,提问作者Discom 9

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 15:54:51