diff --git a/app.apk b/app.apk index b30ac02..7127eae 100644 Binary files a/app.apk and b/app.apk differ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b79e8dc..5691207 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "com.example.shiftalarm" minSdk = 26 targetSdk = 35 - versionCode = 104 - versionName = "1.0.4" + versionCode = 105 + versionName = "1.0.5" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/example/shiftalarm/AlarmActivity.kt b/app/src/main/java/com/example/shiftalarm/AlarmActivity.kt index 4fc5df2..968ffd6 100644 --- a/app/src/main/java/com/example/shiftalarm/AlarmActivity.kt +++ b/app/src/main/java/com/example/shiftalarm/AlarmActivity.kt @@ -185,50 +185,68 @@ class AlarmActivity : AppCompatActivity() { } private fun setupControls() { + val snoozeInterval = intent.getIntExtra("EXTRA_SNOOZE_INTERVAL", 5) + binding.tvSnoozeText.text = "다시 울림 (${snoozeInterval}분)" + binding.btnSnooze.setOnClickListener { handleSnooze() } - // Swipe-to-dismiss for Stop Button + // Omni-directional Swipe & Tap to Dismiss (Samsung One UI Clock Style) var startX = 0f + var startY = 0f + var isMoved = false val dismissBtn = binding.btnDismiss - val maxSwipe = dpToPx(100f).toFloat() + val maxSwipe = dpToPx(80f).toFloat() dismissBtn.setOnTouchListener { v, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { startX = event.rawX + startY = event.rawY + isMoved = false v.animate().cancel() + v.scaleX = 0.96f + v.scaleY = 0.96f true } MotionEvent.ACTION_MOVE -> { val dx = event.rawX - startX - val clampedDx = if (dx > 0) dx.coerceAtMost(maxSwipe) else dx.coerceAtLeast(-maxSwipe) - v.translationX = clampedDx + val dy = event.rawY - startY + val distance = Math.hypot(dx.toDouble(), dy.toDouble()).toFloat() - // Visual feedback: scale up when near trigger - val ratio = abs(clampedDx) / maxSwipe - v.scaleX = 1f + (ratio * 0.15f) - v.scaleY = 1f + (ratio * 0.15f) - v.alpha = 1f - (ratio * 0.3f) + if (distance > 10f) { + isMoved = true + } + + val clampedDx = dx.coerceIn(-maxSwipe, maxSwipe) + val clampedDy = dy.coerceIn(-maxSwipe, maxSwipe) + v.translationX = clampedDx + v.translationY = clampedDy + + val ratio = (distance / maxSwipe).coerceAtMost(1f) + v.scaleX = 0.96f + (ratio * 0.18f) + v.scaleY = 0.96f + (ratio * 0.18f) + binding.pulseCircle.alpha = 0.4f + (ratio * 0.6f) true } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { val dx = event.rawX - startX - if (abs(dx) > maxSwipe * 0.8f) { - // Trigger Dismiss - @Suppress("DEPRECATION") - (getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.vibrate(50) - Toast.makeText(this, "알람 해제 완료", Toast.LENGTH_SHORT).show() - stopAlarm(); finish() + val dy = event.rawY - startY + val distance = Math.hypot(dx.toDouble(), dy.toDouble()).toFloat() + + // Trigger dismiss if swiped past threshold or tapped + if (!isMoved || distance > maxSwipe * 0.5f) { + triggerDismiss() } else { - // Reset + // Spring back reset v.animate() .translationX(0f) + .translationY(0f) .scaleX(1f) .scaleY(1f) - .alpha(1f) - .setDuration(300) + .setDuration(250) + .setInterpolator(android.view.animation.OvershootInterpolator(1.5f)) .start() } true @@ -237,17 +255,35 @@ class AlarmActivity : AppCompatActivity() { } } - // Pulse logic with enhanced glow + // Dual Wave Breathing Animation (Samsung Ambient Glow) fun startPulse() { - binding.pulseCircle.scaleX = 0.85f; binding.pulseCircle.scaleY = 0.85f; binding.pulseCircle.alpha = 0.5f + binding.pulseCircle.scaleX = 0.8f + binding.pulseCircle.scaleY = 0.8f + binding.pulseCircle.alpha = 0.3f + + binding.ambientRing.scaleX = 0.9f + binding.ambientRing.scaleY = 0.9f + binding.ambientRing.alpha = 0.5f + binding.pulseCircle.animate() - .scaleX(1.3f).scaleY(1.3f).alpha(1.0f) - .setDuration(1500) - .withEndAction { + .scaleX(1.35f).scaleY(1.35f).alpha(0.8f) + .setDuration(1600) + .withEndAction { binding.pulseCircle.animate() - .scaleX(0.85f).scaleY(0.85f).alpha(0.5f) - .setDuration(1500) - .withEndAction { if(!isFinishing) startPulse() } + .scaleX(0.8f).scaleY(0.8f).alpha(0.3f) + .setDuration(1600) + .withEndAction { if (!isFinishing) startPulse() } + .start() + } + .start() + + binding.ambientRing.animate() + .scaleX(1.15f).scaleY(1.15f).alpha(0.85f) + .setDuration(1600) + .withEndAction { + binding.ambientRing.animate() + .scaleX(0.9f).scaleY(0.9f).alpha(0.5f) + .setDuration(1600) .start() } .start() @@ -255,13 +291,35 @@ class AlarmActivity : AppCompatActivity() { startPulse() } + private fun triggerDismiss() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + (getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.vibrate( + VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE) + ) + } else { + @Suppress("DEPRECATION") + (getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.vibrate(50) + } + Toast.makeText(this, "알람이 해제되었습니다.", Toast.LENGTH_SHORT).show() + stopAlarm() + finish() + } + private fun handleSnooze() { - @Suppress("DEPRECATION") - (getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.vibrate(50) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + (getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.vibrate( + VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE) + ) + } else { + @Suppress("DEPRECATION") + (getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator)?.vibrate(50) + } val snoozeRepeat = intent.getIntExtra("EXTRA_SNOOZE_REPEAT", 3) val text = if (snoozeRepeat == 99) "다시 울림 설정됨" else "다시 울림 (${snoozeRepeat}회 남음)" Toast.makeText(this, text, Toast.LENGTH_SHORT).show() - snoozeAlarm(); stopAlarm(); finish() + snoozeAlarm() + stopAlarm() + finish() } private fun dpToPx(dp: Float): Int { diff --git a/app/src/main/java/com/example/shiftalarm/CalendarAdapter.kt b/app/src/main/java/com/example/shiftalarm/CalendarAdapter.kt index 503b991..3444109 100644 --- a/app/src/main/java/com/example/shiftalarm/CalendarAdapter.kt +++ b/app/src/main/java/com/example/shiftalarm/CalendarAdapter.kt @@ -35,7 +35,6 @@ class CalendarAdapter( val tvShiftSmall: TextView = view.findViewById(R.id.tvShiftSmall) val shiftChar: TextView = view.findViewById(R.id.shiftChar) val holidayNameSmall: TextView = view.findViewById(R.id.holidayNameSmall) - val memoIndicator: ImageView = view.findViewById(R.id.memoIndicator) val tvTide: TextView = view.findViewById(R.id.tvTide) val memoContent: TextView = view.findViewById(R.id.memoContent) } @@ -231,10 +230,8 @@ class CalendarAdapter( if (hasMemo) { holder.memoContent.visibility = View.VISIBLE holder.memoContent.text = item.memoContent - holder.memoIndicator.visibility = View.VISIBLE } else { holder.memoContent.visibility = View.GONE - holder.memoIndicator.visibility = View.GONE } // Today Border or Highlight diff --git a/app/src/main/java/com/example/shiftalarm/MainActivity.kt b/app/src/main/java/com/example/shiftalarm/MainActivity.kt index de4768e..fa1df9c 100644 --- a/app/src/main/java/com/example/shiftalarm/MainActivity.kt +++ b/app/src/main/java/com/example/shiftalarm/MainActivity.kt @@ -31,6 +31,9 @@ import java.time.YearMonth import java.time.format.DateTimeFormatter import java.util.concurrent.TimeUnit import androidx.recyclerview.widget.GridLayoutManager +import android.graphics.Bitmap +import android.graphics.Canvas +import android.util.Log import kotlin.math.abs class MainActivity : AppCompatActivity() { @@ -41,6 +44,8 @@ class MainActivity : AppCompatActivity() { private var currentViewMonth: YearMonth = YearMonth.now(ShiftCalculator.SEOUL_ZONE) private var currentViewTeam: String = "A" private lateinit var gestureDetector: GestureDetector + private var calendarAdapter: CalendarAdapter? = null + private var isAnimatingMonth = false override fun onConfigurationChanged(newConfig: android.content.res.Configuration) { super.onConfigurationChanged(newConfig) @@ -261,6 +266,7 @@ class MainActivity : AppCompatActivity() { private fun setupCalendar() { binding.calendarGrid.layoutManager = GridLayoutManager(this, 7) + binding.calendarGrid.itemAnimator = null updateCalendar() } @@ -300,22 +306,45 @@ class MainActivity : AppCompatActivity() { val days = generateDaysForMonthWithData(currentViewMonth, currentViewTeam, factory, overrides, memos) - val adapter = CalendarAdapter(days, object : CalendarAdapter.OnDayClickListener { - override fun onDayClick(date: LocalDate, currentShift: String) { - showDaySettingsDialog(date, currentShift) + val availableHeight = binding.calendarGrid.height + val calcRowHeight = if (availableHeight > 0) availableHeight / 5 else 0 + + if (calendarAdapter == null || binding.calendarGrid.adapter == null) { + val newAdapter = CalendarAdapter(days, object : CalendarAdapter.OnDayClickListener { + override fun onDayClick(date: LocalDate, currentShift: String) { + showDaySettingsDialog(date, currentShift) + } + }, binding.cbShowHolidays.isChecked) + if (calcRowHeight > 0) { + newAdapter.rowHeight = calcRowHeight } - }, binding.cbShowHolidays.isChecked) - - binding.calendarGrid.adapter = adapter + calendarAdapter = newAdapter + binding.calendarGrid.adapter = newAdapter + } else { + calendarAdapter?.let { ad -> + ad.days = days + ad.showHolidays = binding.cbShowHolidays.isChecked + if (calcRowHeight > 0) { + ad.rowHeight = calcRowHeight + } + ad.notifyDataSetChanged() + } + } + binding.monthTitle.text = currentViewMonth.format(DateTimeFormatter.ofPattern("yyyy년 MM월")) - // Dynamic row height: exactly 5 rows visible, 6-row months scroll - binding.calendarGrid.post { - val availableHeight = binding.calendarGrid.height - if (availableHeight > 0) { - val calcRowHeight = availableHeight / 5 - adapter.rowHeight = calcRowHeight - adapter.notifyDataSetChanged() + if (calcRowHeight == 0) { + binding.calendarGrid.post { + val h = binding.calendarGrid.height + if (h > 0) { + val rH = h / 5 + calendarAdapter?.let { + if (it.rowHeight != rH) { + it.rowHeight = rH + it.notifyDataSetChanged() + } + } + } } } @@ -717,7 +746,9 @@ class MainActivity : AppCompatActivity() { } private fun animateMonthTransition(direction: Int) { + if (isAnimatingMonth) return val card = binding.calendarCard + val snapshot = binding.calendarOverlaySnapshot val width = card.width.toFloat() if (width == 0f) { @@ -730,33 +761,61 @@ class MainActivity : AppCompatActivity() { return } - val fluidInterpolator = androidx.core.view.animation.PathInterpolatorCompat.create(0.22f, 1.0f, 0.36f, 1.0f) + isAnimatingMonth = true + // 1. Capture bitmap snapshot of current calendar card + try { + val bitmap = Bitmap.createBitmap(card.width, card.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + card.draw(canvas) + snapshot.setImageBitmap(bitmap) + snapshot.visibility = android.view.View.VISIBLE + snapshot.alpha = 1.0f + snapshot.scaleX = 1.0f + snapshot.scaleY = 1.0f + snapshot.translationX = 0f + } catch (e: Exception) { + Log.w("MainActivity", "Failed to capture snapshot for month transition", e) + } + + // 2. Update to new month immediately + currentViewMonth = if (direction > 0) { + currentViewMonth.plusMonths(1) + } else { + currentViewMonth.minusMonths(1) + } + updateCalendar() + + // 3. Prepare incoming new month card on top + card.translationX = if (direction > 0) width else -width + card.alpha = 1.0f + card.cardElevation = dpToPx(8f).toFloat() + + val fluidInterpolator = androidx.core.view.animation.PathInterpolatorCompat.create(0.22f, 1.0f, 0.36f, 1.0f) + val duration = 280L + + // 4. Slide top new month card smoothly into position card.animate() - .translationX(if (direction > 0) -width else width) - .alpha(0.8f) - .setDuration(160) + .translationX(0f) + .setDuration(duration) .setInterpolator(fluidInterpolator) .withEndAction { - currentViewMonth = if (direction > 0) { - currentViewMonth.plusMonths(1) - } else { - currentViewMonth.minusMonths(1) - } - updateCalendar() - - // Incoming slide animation from opposite side - card.translationX = if (direction > 0) width else -width - card.alpha = 0.8f - - card.animate() - .translationX(0f) - .alpha(1f) - .setDuration(220) - .setInterpolator(fluidInterpolator) - .start() + card.cardElevation = 0f + snapshot.visibility = android.view.View.GONE + snapshot.setImageDrawable(null) + isAnimatingMonth = false } .start() + + // 5. Parallax fade & scale down bottom snapshot card + snapshot.animate() + .scaleX(0.94f) + .scaleY(0.94f) + .alpha(0.35f) + .translationX(if (direction > 0) -width * 0.22f else width * 0.22f) + .setDuration(duration) + .setInterpolator(fluidInterpolator) + .start() } private fun formatRemainingDays(days: Float): String { diff --git a/app/src/main/res/drawable/bg_dismiss_samsung.xml b/app/src/main/res/drawable/bg_dismiss_samsung.xml new file mode 100755 index 0000000..bc1bfd7 --- /dev/null +++ b/app/src/main/res/drawable/bg_dismiss_samsung.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_pulse_wave_samsung.xml b/app/src/main/res/drawable/bg_pulse_wave_samsung.xml new file mode 100755 index 0000000..eb1637f --- /dev/null +++ b/app/src/main/res/drawable/bg_pulse_wave_samsung.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/bg_snooze_hub_samsung.xml b/app/src/main/res/drawable/bg_snooze_hub_samsung.xml new file mode 100755 index 0000000..01b99de --- /dev/null +++ b/app/src/main/res/drawable/bg_snooze_hub_samsung.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_alarm.xml b/app/src/main/res/layout/activity_alarm.xml index c6cceb7..af96a6b 100644 --- a/app/src/main/res/layout/activity_alarm.xml +++ b/app/src/main/res/layout/activity_alarm.xml @@ -1,110 +1,164 @@ - - + + android:orientation="horizontal" + android:gravity="center" + android:background="@drawable/bg_snooze_hub_samsung" + android:paddingHorizontal="16dp" + android:paddingVertical="6dp" + android:layout_marginTop="64dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"> - - - + + android:text="[주간] 07:00 근무 알람" + android:textSize="14sp" + android:textStyle="bold" + android:textColor="#FFFFFF" /> - + + + + + + + + android:background="@drawable/bg_snooze_hub_samsung" + android:paddingHorizontal="36dp" + android:paddingVertical="16dp" + android:clickable="true" + android:focusable="true" + android:layout_marginBottom="40dp" + app:layout_constraintBottom_toTopOf="@id/dismissContainer" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"> + + - - - - - - - - - - - + android:text="다시 울림 (5분)" + android:textSize="18sp" + android:textStyle="bold" + android:textColor="#FFFFFF" /> + + - + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1acdaa7..0c6f770 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -58,26 +58,39 @@ - - + - + + android:scaleType="fitXY" + android:visibility="gone" /> + + + + + - - - diff --git a/scratch/release.py b/scratch/release.py index f1bb25c..a9d6649 100755 --- a/scratch/release.py +++ b/scratch/release.py @@ -6,21 +6,22 @@ import shutil token = "e3b515eaa0a6683c921ca3bf718e281ed30a6075" owner_repo = "sanjeok77/ShiftRing" -tag = "v1.0.4" -title = "v1.0.4 - 날짜 옆 근무 뱃지 완벽한 원형/글자 크기 정밀 보정 & 설정 권한 아이콘/색상 시각화 개선" -body = """## 🚀 ShiftRing v1.0.4 릴리즈 +tag = "v1.0.5" +title = "v1.0.5 - 메모 아이콘 정리, 삼성 One UI 8.5/9.0 알람 화면 리디자인, 달력 초기 깜빡임 해소 및 카드 스택 레이어 전환 애니메이션 적용" +body = """## 🚀 ShiftRing v1.0.5 릴리즈 ### 🌟 주요 변경 및 개선 사항 -1. **날짜 옆 근무 뱃지 원형 왜곡 방지 및 글자 가독성 정밀 보정**: - - 메모가 있는 날짜의 상단 근무 뱃지(`tvShiftSmall`)를 정원(18dp × 18dp)으로 고정하여 찌그러짐 현상 완벽 방지 - - 폰트 패딩 제거(`includeFontPadding=false`) 및 텍스트 크기 정밀 조정으로 '주/석/야/휴/월/연/교' 및 '주맞/야맞/반월' 텍스트가 잘림 없이 100% 선명하게 표시 -2. **앱 안정성 설정 (필수) 항목별 아이콘 및 테마 색상 개편**: - - 배터리 최적화 제외: 배터리 전용 아이콘(`ic_battery`) + 에메랄드 그린(`#34C759`) - - 정확한 알람 허용: 정밀 시계 아이콘(`ic_alarm`) + One UI 블루(`#007AFF`) - - 전체화면 알림 허용: 전체화면 인텐트 전용 아이콘(`ic_fullscreen_intent`) + 비비드 퍼플(`#AF52DE`) - - 시스템 전체 권한 설정: 시스템 톱니바퀴 아이콘(`ic_settings`) + 앰버 오렌지(`#FF9500`) - - 각 권한의 성격과 상태를 한눈에 직관적으로 구분할 수 있도록 시각 계층 대폭 강화 -3. **Android 14/15 호환 및 정식 서명키(release.jks) 적용**""" +1. **하루 메모 불필요한 하단 아이콘 제거 (`item_day.xml`, `CalendarAdapter.kt`)**: + - 메모 텍스트가 이미 상단/중앙에 4줄로 표시되므로 중복되던 하단 메모/수정 아이콘을 깔끔하게 제거하여 셀 공간 최적화 +2. **알람 울림/해제 화면 삼성 One UI 8.5/9.0 최신 디자인 전면 개편 (`activity_alarm.xml`, `AlarmActivity.kt`)**: + - 상단 근무 라벨 뱃지 + 대형 현대적 시계 타이포그래피 + 날짜 정보 일원화 + - 중앙 와이드 글래스 캡슐 허브 다시 울림(스누즈) 버튼 탑재 + - 하단 듀얼 펄스 웨이브 앰비언트 글로우 및 옴니 다이렉션 스와이프/탭 해제 컨트롤 적용 +3. **앱 초기 실행 시 달력 화면 깜빡임(Flicker) 완벽 해소 (`MainActivity.kt`)**: + - `RecyclerView` 불필요한 기본 애니메이터 제거 및 어댑터/행 높이 재계산 로직 최적화로 초기 진입 시 잔상 없이 즉각 로딩 +4. **달력 월간 이동 시 카드 스택(Card Stack) 레이어 전환 애니메이션 구현 (`MainActivity.kt`, `activity_main.xml`)**: + - 이전 달 화면이 하단에 베이스로 자연스럽게 깔리고(0.94x 축소 및 0.35 알파 페이드), 새 달 카드가 상단에서 부드럽게 감속 슬라이드 오버되는 끊김 없는 유체 모션 적용 +5. **Android 14/15 호환 및 정식 서명키(release.jks) 적용**""" apk_path = "app/build/outputs/apk/release/app-release.apk" if not os.path.exists(apk_path): @@ -58,10 +59,10 @@ print("Updated app.apk at project root.") # Update version.json v_info = { - "versionCode": 104, - "versionName": "1.0.4", + "versionCode": 105, + "versionName": "1.0.5", "apkUrl": direct_url, - "changelog": "v1.0.4: 날짜 옆 근무 뱃지 원형 정밀 보정 & 글자 가독성 개선, 앱 안정성 설정 아이콘 및 색상 시각화", + "changelog": "v1.0.5: 메모 아이콘 정리, 삼성 최신 알람 화면 개편, 달력 초기 깜빡임 해소, 카드 스택 레이어 전환 애니메이션 적용", "forceUpdate": False } with open("version.json", "w", encoding="utf-8") as f: @@ -73,7 +74,7 @@ print(f"Updated version.json with apkUrl: {direct_url}") git_path = r"C:\Users\work\AppData\Roaming\MobaXterm\slash\mx86_64b\bin" os.environ["PATH"] = git_path + os.pathsep + os.environ.get("PATH", "") -commit_msg = "v1.0.4 - 날짜 옆 근무 뱃지 원형 정밀 보정, 앱 안정성 설정 아이콘 및 색상 개편" +commit_msg = "v1.0.5 - 메모 아이콘 정리, 삼성 One UI 최신 알람 화면 개편, 달력 깜빡임 해소 및 카드 스택 전환 애니메이션 적용" subprocess.run(["git", "add", "."], check=True) subprocess.run(["git", "commit", "-m", commit_msg], check=True) subprocess.run(["git", "push", "origin", "main"], check=True) diff --git a/version.json b/version.json index 1243657..b82ea0f 100644 --- a/version.json +++ b/version.json @@ -1,7 +1,7 @@ { - "versionCode": 104, - "versionName": "1.0.4", - "apkUrl": "https://git.webpluss.net/attachments/017b75f6-07a5-468f-8694-83345c9692e3", - "changelog": "v1.0.4: 날짜 옆 근무 뱃지 원형 정밀 보정 & 글자 가독성 개선, 앱 안정성 설정 아이콘 및 색상 시각화", + "versionCode": 105, + "versionName": "1.0.5", + "apkUrl": "https://git.webpluss.net/attachments/b03485a6-cae4-471d-81c0-4f6288d3aa9f", + "changelog": "v1.0.5: 메모 아이콘 정리, 삼성 최신 알람 화면 개편, 달력 초기 깜빡임 해소, 카드 스택 레이어 전환 애니메이션 적용", "forceUpdate": false } \ No newline at end of file