Android 工位运动小助手
背景
在社会日益发展的今天,我们大部分人在工位上长时间的办公,从而忽略了站起来活动一下的必要性。时长活动一下有益于身心健康,同时也可以缓解工作的疲劳。基于以上情况,我开发了一个定时提醒用户运动的工具类app.
功能介绍
下面我们来具体看看,这个工具具体的功能吧
第一张图开始设置任务的间隔时间,第二张图是任务准备执行,第三张图是任务已经在执行,第四张图是任务完成了第一次进入到下一次的周期任务。第五,第六张图显示的是通知提醒用户起来活动一下。这个工具可以让你 设置任意时间的周期,然后每n min 后就会提醒你该起来活动一下了。那么具体是怎么实现这个功能的呢?
实现方法
我们使用workManager构建一个周期性的任务,设置一个具体的时间间隔,通过service在需要的时候启动这个任务,就可以让这个任务运行,通过notification,从而提醒用户起来活动一下。
具体实现代码
package com.fly.heat.service
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.fly.heat.R
import com.fly.heat.constant.Config.STOP_SERVICE
import com.fly.heat.constant.Config.TASK_DEFAULT_TIME
import com.fly.heat.constant.Config.TASK_INTERVAL
import com.fly.heat.task.ActivityReminderWorker
import com.fly.heat.ui.RemindAc
import com.fly.heat.util.MMKVHelper
import java.util.concurrent.TimeUnit
class ForegroundService : Service() {
private lateinit var workManager: WorkManager
companion object {
const val NOTIFICATION_ID = 2
const val CHANNEL_ID = "ForegroundServiceChannel"
const val CHANNEL_NAME = "Foreground Service Channel"
const val DESCRIPTION = "Channel for Foreground Service"
}
override fun onCreate() {
super.onCreate()
createNotificationChannel()
workManager = WorkManager.getInstance(this)
}
@RequiresApi(Build.VERSION_CODES.Q)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == STOP_SERVICE) {
stopServiceAndCancelTasks()
return START_NOT_STICKY
}
startForegroundService()
scheduleReminder()
return START_STICKY
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = DESCRIPTION
}
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun startForegroundService() {
val notificationIntent = Intent(this, RemindAc::class.java)
var pendingIntent: PendingIntent? = null
pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
} else {
PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(this.getString(R.string.app_name))
.setContentText(
getString(R.string.remind_content,MMKVHelper.getInstance().getLong(TASK_INTERVAL,TASK_DEFAULT_TIME))
)
.setSmallIcon(R.mipmap.logo)
.setContentIntent(pendingIntent)
.build()
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
}
//停止这个服务并取消所有工作请求
private fun stopServiceAndCancelTasks() {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
removeNotification()
}
private fun removeNotification() {
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancelAll()
}
override fun onDestroy() {
super.onDestroy()
// 取消所有工作请求
workManager.cancelAllWork()
}
private fun scheduleReminder() {
val taskInterval = MMKVHelper.getInstance().getLong(TASK_INTERVAL, TASK_DEFAULT_TIME)
val inputData = Data.Builder()
.putString("message", getString(R.string.please_stand_up))
.build()
val periodicWorkRequest = PeriodicWorkRequestBuilder<ActivityReminderWorker>(
taskInterval, TimeUnit.MINUTES
).setInputData(inputData)
.build()
workManager.enqueue(periodicWorkRequest)
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
}
这个ForegroundService是用来启动前台通知的,同时让服务运行,便于任务在后台运行时间变长
package com.fly.heat.task
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.text.format.DateFormat
import android.util.Log
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.fly.heat.R
import com.fly.heat.ui.RemindAc
import com.fly.heat.mi_step.StepUtil
import java.util.Calendar
class ActivityReminderWorker(context: Context, private var workerParams: WorkerParameters) : Worker(context, workerParams) {
companion object {
var notificationId = 1
var channelId = "activity_reminder_channel"
var chanelName = "Activity Reminder Channel"
}
override fun doWork(): Result {
val message = workerParams.inputData.getString("message") ?: "默认消息"
Log.d("ActivityReminderWorker", "doWork: $message")
// 执行提醒逻辑
showCustomNotification()
return Result.success()
}
private fun showStep(remoteViews: RemoteViews){
remoteViews.setTextViewText(R.id.step, "步数:${StepUtil.getTodayStepsCount(applicationContext)}")
}
private fun showTime(remoteViews: RemoteViews) {
val currentTime = Calendar.getInstance().time
val formattedTime = DateFormat.format("HH:mm:ss", currentTime).toString()
remoteViews.setTextViewText(R.id.time, formattedTime)
}
private fun showCustomNotification() {
val notificationLayout = RemoteViews(applicationContext.packageName, R.layout.notification_small)
val notificationLayoutExpanded = RemoteViews(applicationContext.packageName, R.layout.notification_large)
val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
showTime(notificationLayout)
showStep(notificationLayout)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
chanelName,
NotificationManager.IMPORTANCE_HIGH
).apply {
// 设置通道的默认声音
val soundUri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
setSound(soundUri, null)
// 设置震动模式
enableVibration(false)
vibrationPattern = longArrayOf(0, 1000, 500, 1000) // 震动模式:0ms延迟,1000ms震动,5
}
notificationManager.createNotificationChannel(channel)
}
val intent = Intent(applicationContext, RemindAc::class.java)
val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val notification = NotificationCompat.Builder(applicationContext, channelId)
// .setContentTitle("活动提醒")
// .setContentText("起来活动一下吧!")
.setSmallIcon(R.mipmap.sport)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
// .setStyle(NotificationCompat.DecoratedCustomViewStyle())
.setCustomContentView(notificationLayout)
// .setCustomBigContentView(notificationLayoutExpanded)
.build()
notificationManager.notify(notificationId, notification)
}
}
这个ActivityReminderWorker是wokeManager具体的任务操作,这里显示一个notification来提醒用户。
package com.fly.heat.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.Button
import android.widget.Chronometer
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.example.customprogressview.ProgressCircleView
import com.fly.heat.service.ForegroundService
import com.fly.heat.R
import com.fly.heat.constant.Config
import com.fly.heat.constant.Config.STOP_SERVICE
import com.fly.heat.constant.TaskStatus
import com.fly.heat.util.MMKVHelper
class RemindAc : AppCompatActivity() {
private lateinit var chronometer: Chronometer //任务倒计时
private var countDownTimer: CountDownTimer? = null
private var btnStart: Button? = null //执行按钮
private var taskStatus = TaskStatus.READY//记录服务是否正在运行
private var tvStatus: TextView? = null //显示任务状态
private lateinit var waterView: ProgressCircleView //动画的进度显示条
private val handler = Handler(Looper.getMainLooper())
private var currentProgress = 0 //动画的当前进度
private var taskInterval = Config.TASK_DEFAULT_TIME //任务时间间隔
init {
taskInterval = MMKVHelper.getInstance().getLong(
Config.TASK_INTERVAL,
Config.TASK_DEFAULT_TIME
)
}
companion object {
fun start(context: Context) {
val intent = Intent(context, RemindAc::class.java)
context.startActivity(intent)
}
}
private lateinit var animalRunnable:Runnable;
//开始动画
private fun startProgressAnimation(duration:Long) {
val delayTime = 600*duration//延时时间 ms
animalRunnable = object : Runnable {
override fun run() {
if (currentProgress < 100) {
currentProgress += 1
waterView.setProgress(currentProgress)
handler.postDelayed(this, delayTime)
}else {
resetProgressAnimation()//重置动画
}
}
}
handler.postDelayed(animalRunnable,delayTime)
}
//停止进度动画
private fun stopProgressAnimation(){
handler.removeCallbacks(animalRunnable)
currentProgress = 0
waterView.setProgress(currentProgress)
}
//重置进度动画
private fun resetProgressAnimation() {
stopProgressAnimation()
startProgressAnimation(taskInterval)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.ac_remind)
chronometer = findViewById(R.id.chronometer)
btnStart = findViewById(R.id.btn_remind)
tvStatus = findViewById(R.id.tv_task_status)
btnStart?.setOnClickListener {
if(taskStatus == TaskStatus.READY){
startTask()
}else if(taskStatus == TaskStatus.RUNNING){
finishTask()
}
}
waterView = findViewById(R.id.waterView)
}
//开始任务
private fun startTask() {
if (taskStatus == TaskStatus.RUNNING) {
Toast.makeText(this, getString(R.string.task_running), Toast.LENGTH_SHORT).show()
return
}else if(taskStatus == TaskStatus.READY){
taskStatus = TaskStatus.RUNNING
tvStatus?.text = getString(R.string.task_running)
startForegroundService();//开启前台任务
startCountdown()//开始倒计时
runningButton()//运行按钮可用
startProgressAnimation(taskInterval)//开始动画
}
}
//启动前台服务
private fun startForegroundService() {
val intent = Intent(this, ForegroundService::class.java)
startService(intent)
}
//结束服务并且运行任务
private fun stopForegroundService() {
val intent = Intent(this, ForegroundService::class.java).apply {
action = STOP_SERVICE
}
startService(intent)
}
private fun finishTask() {
if (taskStatus == TaskStatus.READY) {
Toast.makeText(this, getString(R.string.task_not_running), Toast.LENGTH_SHORT).show()
return
}else if(taskStatus == TaskStatus.RUNNING){
taskStatus = TaskStatus.READY
Toast.makeText(this, getString(R.string.task_finish), Toast.LENGTH_SHORT).show()
stopCountDown();//结束定时器
stopForegroundService()//结束服务
tvStatus?.text = getString(R.string.task_finish)
readyButton()//重置按钮
stopProgressAnimation()//停止进度动画
}
}
//开始任务不可用
private fun readyButton() {
btnStart?.apply {
isEnabled = true
isClickable = true
background = ContextCompat.getDrawable(context, R.drawable.round_red)
text = getString(R.string.start_task)
}
}
//开始任务可用
private fun runningButton() {
btnStart?.apply {
isEnabled = true
isClickable = true
background = ContextCompat.getDrawable(context, R.drawable.round_green)
text = getString(R.string.finish_task)
}
}
//停止计时器
private fun stopCountDown(){
chronometer.stop()
countDownTimer?.cancel()
chronometer.text = getString(R.string.start_time)
}
//开始倒计时
private fun startCountdown() {
// 15分钟倒计时,单位为毫秒
val duration = taskInterval * 60 * 1000L
countDownTimer = object : CountDownTimer(duration, 1000) {
override fun onTick(millisUntilFinished: Long) {
val minutes = millisUntilFinished / 1000 / 60
val seconds = (millisUntilFinished / 1000) % 60
chronometer.text = String.format("d:d", minutes, seconds)
}
override fun onFinish() {
chronometer.text = getString(R.string.start_time)
startCountdown()//结束后重新开始倒计时
resetProgressAnimation()//重置动画
}
}.start()
}
override fun onDestroy() {
super.onDestroy()
countDownTimer?.cancel()
}
}
这个RemindAc对应的是任务运行的app界面,这里会绘制ui,执行按钮的响应事件,开启任务执行的进度的动画,让用户清晰的看到自己任务的执行情况。比如我的时间周期是30min,那么用户从用户开始任务后,每隔30min就可以收到提醒,这就可以让我们知道需要起来活动一下了。假如你想终止任务,那么只需要结束任务哭就可以终止任务了。
package com.fly.heat.ui
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.Button
import android.widget.NumberPicker
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.example.customprogressview.ProgressCircleView
import com.fly.heat.R
import com.fly.heat.constant.Config.TASK_INTERVAL
import com.fly.heat.util.MMKVHelper
class TaskSettingAc : AppCompatActivity() {
private var btn_sumbit: Button? = null
private var numberPick: NumberPicker? = null
private var tv_time: TextView? = null
private var time: Long = 0
companion object {
const val MIN = 15
const val MAX = 120
}
private fun unableButton() {
btn_sumbit?.isEnabled = false
}
private fun enableButton() {
btn_sumbit?.isEnabled = true
}
private fun requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this,
"android.permission.POST_NOTIFICATIONS"
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf("android.permission.POST_NOTIFICATIONS"),
1
)
unableButton()
} else {
enableButton()
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 1) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
runOnUiThread {
// 用户授予了通知权限
Toast.makeText(this, "已获得通知权限", Toast.LENGTH_SHORT).show()
enableButton()
}
} else {
runOnUiThread {
// 用户拒绝了通知权限
Toast.makeText(this, "用户拒绝了通知权限", Toast.LENGTH_SHORT).show()
unableButton()
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.ac_task_setting)
initView()
requestNotificationPermission()
}
private fun initView() {
tv_time = findViewById(R.id.tv_time)
numberPick = findViewById(R.id.picker)
btn_sumbit = findViewById(R.id.submit)
initNumberPicker()
}
private fun initNumberPicker() {
numberPick?.apply {
minValue = MIN
maxValue = MAX
wrapSelectorWheel = false
setOnValueChangedListener { _, _, newVal ->
time = newVal.toLong()
tv_time?.text = String.format(getString(R.string.your_choose), time)
}
}
}
fun submit(view: View) {
if (time < 15) {
Toast.makeText(this, getString(R.string.choose_time), Toast.LENGTH_SHORT).show()
return
} else {
MMKVHelper.getInstance().putLong(TASK_INTERVAL, time)
RemindAc.start(this)
}
}
}
这个TaskSettingAc是用来设置任务的执行时间的,这就可以很灵活的控制自己需要执行任务的时间。
最后总结
技术层面:采用kotlin+notification+service+workManager的方式
生活层面:提醒在办公室坐着的我们,每隔一段时间需要起来活动一下,有益于我们的身体健康。
作者:生如夏花爱学习22966
来源:juejin.cn/post/7429606384704995382
来源:juejin.cn/post/7429606384704995382