Skip to content

Commit

Permalink
RingingAlarmの作成
Browse files Browse the repository at this point in the history
アラームの基本機能である、指定時刻に通知を送りアラームを鳴らす機能を作成。
view と model の2ファイルに分けて作成し、その画面に遷移するためのボタンをmainに作成。
パッケージはそれぞれ>flutter pub add ~~~で入れる必要があった。
  • Loading branch information
MrMocchy committed Jun 10, 2022
1 parent 35904d1 commit af28f70
Show file tree
Hide file tree
Showing 7 changed files with 420 additions and 1 deletion.
Binary file not shown.
20 changes: 19 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'model/ringing_alarm_model.dart';
import 'view/ringing_alarm_view.dart';

void main() {
void main() async {
setupRingingAlarm();
runApp(const MyApp());
}

Expand Down Expand Up @@ -102,10 +105,25 @@ class _MyHomePageState extends State<MyHomePage> {
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
FloatingActionButton(
//RingingAlarmPageに飛ぶボタン
heroTag: 'ringing_alarm_page_button',
onPressed: () async {
// "push"で新規画面に遷移
await Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return const RingingAlarmTestPage(
title: 'Ringing Alarm Test Page');
}),
);
},
child: const Icon(Icons.alarm),
),
],
),
),
floatingActionButton: FloatingActionButton(
heroTag: 'increment_button',
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
Expand Down
161 changes: 161 additions & 0 deletions lib/model/ringing_alarm_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:intl/intl.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;

/// RingingAlarm の初期化をする。mainで呼ぶ。
void setupRingingAlarm() {
_setupTimeZone();
}

// タイムゾーンを設定する
Future<void> _setupTimeZone() async {
tz.initializeTimeZones();
var tokyo = tz.getLocation('Asia/Tokyo');
tz.setLocalLocation(tokyo);
}

///ミリ秒まで表示するフォーマッター
///- 返り値は hh:mm:ss.x の形の String
String formatTime(DateTime dt) {
return "${DateFormat.Hms().format(dt)}.${dt.millisecond.toString().padRight(2, '0').substring(0, 1)}";
}

///アラームの管理をするクラス
class RingingAlarm {
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
Timer? _timer; // タイマーオブジェクト
DateTime _time = DateTime.utc(0, 0, 0)
.add(const Duration(seconds: 10)); // タイマーで管理している時間。10秒をカウントダウンする設定
bool _isTimerActive = false; // バックグラウンドに遷移した際にタイマーが起動中かどうか
DateTime? _pausedTime; // バックグラウンドに遷移した時間
int? _notificationId; // 通知ID
bool _notificationOnceFlag = false; //連続して通知ができないようにするフラグ

DateTime get displayTime => _time;

///コンストラクタ
RingingAlarm();

/// アプリがバックグラウンドに遷移した際のハンドラ
void handleOnPaused() {
if (_timer != null && _timer!.isActive) {
_isTimerActive = true;
_timer!.cancel(); // タイマーを停止する
}
_pausedTime = DateTime.now(); // バックグラウンドに遷移した時間を記録
if (_isTimerActive) {
if (_notificationOnceFlag) {
_notificationOnceFlag = false;
if (_time
.difference(DateTime.utc(0, 0, 0))
.compareTo(const Duration(seconds: 1)) >
0) {
//タイマーの停止直後に再度通知を作りたがるので間隔を制限
_notificationId = _scheduleLocalNotification(
_time.difference(DateTime.utc(0, 0, 0))); // ローカル通知をスケジュール登録
}
}
}
}

// フォアグラウンドに復帰した時のハンドラ
void handleOnResumed() {
if (_isTimerActive == false) return; // タイマーが動いてなければ何もしない
Duration backgroundDuration =
DateTime.now().difference(_pausedTime!); // バックグラウンドでの経過時間
// バックグラウンドでの経過時間が終了予定を超えていた場合(この場合は通知実行済みのはず)
if (_time.difference(DateTime.utc(0, 0, 0)).compareTo(backgroundDuration) <
0) {
_time = DateTime.utc(0, 0, 0); // 時間をリセットする
} else {
_time = _time.add(-backgroundDuration); // バックグラウンド経過時間分時間を進める
startTimer(); // タイマーを再開する
}
}

//鳴っている通知を消す(正確には最新の通知)
void resetNotification() {
if (_notificationId != null) {
flutterLocalNotificationsPlugin.cancel(_notificationId!); // 通知をキャンセル
}
_isTimerActive = false; // リセット
_notificationId = null; // リセット
_pausedTime = null;
_notificationOnceFlag = false;
}

// タイマーを開始する
void startTimer() {
_notificationOnceFlag = true;
_timer = Timer.periodic(const Duration(milliseconds: 100), (Timer timer) {
_time = _time.add(const Duration(milliseconds: -100));
_handleTimeIsOver();
}); // 1秒ずつ時間を減らす
}

// タイマーを初期化してから開始する
void restartTimer() {
if (_timer != null) _timer!.cancel();
_time = DateTime.utc(0, 0, 0);
_time = _time.add(const Duration(seconds: 10));
startTimer();
}

// 時間がゼロになったらタイマーを止める
void _handleTimeIsOver() {
if (_timer != null &&
_timer!.isActive &&
//_time != null &&
_time == DateTime.utc(0, 0, 0)) {
_timer!.cancel();
_notificationOnceFlag = false;
}
}

//タイマーを止める
void cancelTimer() {
if (_timer != null && _timer!.isActive) {
_timer!.cancel();
}
}

/// タイマー終了をローカル通知。実質メイン
int _scheduleLocalNotification(Duration duration) {
flutterLocalNotificationsPlugin.initialize(
const InitializationSettings(
android:
AndroidInitializationSettings('@mipmap/ic_launcher'), //通知アイコン
iOS: IOSInitializationSettings()),
);
int notificationId =
DateTime.now().hashCode; //現在時刻から生成しているが、通知を管理するIDを指定できる
DateTime tzDT = tz.TZDateTime.now(tz.local).add(duration);
flutterLocalNotificationsPlugin.zonedSchedule(
notificationId, //通知のID
'Time is over', //通知のタイトル
'Wake up! It\'s ${formatTime(tzDT)}', //通知の本文
tz.TZDateTime.now(tz.local).add(duration), //通知の予約時間
const NotificationDetails(
android: AndroidNotificationDetails(
'your channel id', 'your channel name',
channelDescription: 'your channel description',
importance: Importance.high,
priority: Priority.high,
//https://esffects.net/361.html からとってきた音源。
//alarm2022\android\app\src\main\res\raw\simple_alarm_music.mp3 に配置。
sound:
RawResourceAndroidNotificationSound('simple_alarm_music'),
fullScreenIntent: true),
iOS: IOSNotificationDetails()),
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
androidAllowWhileIdle: true);
debugPrint("■■■Scheduled for ${formatTime(tzDT)}");
debugPrint("■■■ from ${formatTime(tz.TZDateTime.now(tz.local))}");
return notificationId;
}
}
121 changes: 121 additions & 0 deletions lib/view/ringing_alarm_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'dart:async';
import '../model/ringing_alarm_model.dart';

//新しく作り直すと考えTestとつけている
class RingingAlarmTestPage extends StatefulWidget {
const RingingAlarmTestPage({Key? key, this.title}) : super(key: key);
final String? title;
@override
// ignore: library_private_types_in_public_api
_RingingAlarmTestPageState createState() => _RingingAlarmTestPageState();
}

class _RingingAlarmTestPageState extends State<RingingAlarmTestPage>
with WidgetsBindingObserver {
//modelの方の実体
final RingingAlarm _ringingAlarm = RingingAlarm();
//現在時刻表示用のTimer
Timer? _clockTimer;

//ページ遷移時にlogがバグったので。
//参照:https://stackoverflow.com/questions/49340116/setstate-called-after-dispose
@override
void setState(fn) {
if (mounted) {
super.setState(fn);
}
}

/// 初期化処理
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
//現在時刻表示用だが画面の更新処理も担っている
_clockTimer =
Timer.periodic(const Duration(milliseconds: 100), (Timer clockTimer) {
setState(() {});
});
}

/// ライフサイクルが変更された際に呼び出される関数をoverrideして、変更を検知
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
// バックグラウンドに遷移した時
setState(() {
_ringingAlarm.handleOnPaused();
});
} else if (state == AppLifecycleState.resumed) {
// フォアグラウンドに復帰した時
setState(() {
_ringingAlarm.handleOnResumed();
});
}
super.didChangeAppLifecycleState(state);
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(
//現在時刻表示
"Now : ${formatTime(DateTime.now())}",
),
Text(
//タイマー表示
formatTime(_ringingAlarm.displayTime),
style: Theme.of(context).textTheme.headline2,
),
Row(
//各種ボタンの配置
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
//前の画面に戻る
heroTag: 'back_button',
onPressed: () {
if (_clockTimer != null) _clockTimer!.cancel();
Navigator.of(context).pop();
},
child: const Text("Back"),
),
FloatingActionButton(
//タイマーの一時停止
heroTag: 'stop_button',
onPressed: () {
_ringingAlarm.cancelTimer();
},
child: const Text("Stop"),
),
FloatingActionButton(
//タイマーのカウントダウン再開
heroTag: 'start_button',
onPressed: () {
_ringingAlarm.startTimer();
},
child: const Text("Start"),
),
FloatingActionButton(
//タイマーの時間をリセットしてスタート
heroTag: 'restart_button',
onPressed: () {
_ringingAlarm.restartTimer();
},
child: const Text("Restart"),
),
FloatingActionButton(
//鳴ってるアラームを止める
heroTag: 'reset_button',
onPressed: () {
_ringingAlarm.resetNotification();
},
child: const Text("Reset"),
),
],
)
]));
}
}
2 changes: 2 additions & 0 deletions macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation

import flutter_local_notifications

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
}
Loading

0 comments on commit af28f70

Please sign in to comment.