Skip to content

Commit

Permalink
Use Material 3 date and time pickers, with some style tweaks
Browse files Browse the repository at this point in the history
FIXES: #53
  • Loading branch information
albertvaka committed Apr 1, 2024
1 parent 3c36163 commit f5432de
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 35 deletions.
28 changes: 14 additions & 14 deletions app/src/main/java/org/kde/bettercounter/ui/ChartHolder.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.kde.bettercounter.ui

import android.content.Context
import android.view.Gravity
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import org.kde.bettercounter.R
Expand All @@ -19,7 +19,7 @@ import java.util.Date
import java.util.Locale

class ChartHolder(
private val context: Context,
private val activity: AppCompatActivity,
private val binding: FragmentChartBinding,
) : RecyclerView.ViewHolder(binding.root) {

Expand All @@ -36,9 +36,9 @@ class ChartHolder(
Interval.LIFETIME -> throw IllegalStateException("Interval not valid as a chart display interval")
}
val dateString = dateFormat.format(rangeStart.time)
binding.chartName.text = context.resources.getQuantityString(R.plurals.chart_title, entries.size, dateString, entries.size)
binding.chartName.text = activity.resources.getQuantityString(R.plurals.chart_title, entries.size, dateString, entries.size)
binding.chartName.setOnClickListener { view ->
val popupMenu = PopupMenu(context, view, Gravity.END)
val popupMenu = PopupMenu(activity, view, Gravity.END)
popupMenu.menuInflater.inflate(R.menu.popup_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem ->
menuItem.isChecked = true
Expand All @@ -62,22 +62,22 @@ class ChartHolder(
popupMenu.show()
}
binding.chartName.setOnLongClickListener {
showDatePicker(context, rangeStart, onDateChange)
showDatePicker(activity, rangeStart, onDateChange)
true
}

// Only show a goal line if the displayed interval is larger than the counter's
val goalLine = computeGoalLine(counter, interval)

// Chart
binding.chart.setDataBucketized(entries, rangeStart, interval, counter.color.toColorForChart(context), goalLine)
binding.chart.setDataBucketized(entries, rangeStart, interval, counter.color.toColorForChart(activity), goalLine)

// Stats
val periodAverage = getPeriodAverageString(counter, entries, rangeStart, rangeEnd)
val lifetimeAverage = getLifetimeAverageString(counter)
binding.chartAverage.text = context.getString(R.string.stats_averages, periodAverage, lifetimeAverage)
binding.chartAverage.text = activity.getString(R.string.stats_averages, periodAverage, lifetimeAverage)
if (binding.chartAverage.lineCount > 1) {
binding.chartAverage.text = context.getString(R.string.stats_averages_multiline, periodAverage, lifetimeAverage)
binding.chartAverage.text = activity.getString(R.string.stats_averages_multiline, periodAverage, lifetimeAverage)
}
}

Expand All @@ -97,7 +97,7 @@ class ChartHolder(

private fun getLifetimeAverageString(counter: CounterSummary): String {
if (counter.totalCount == 0) {
return context.getString(R.string.stats_average_n_a)
return activity.getString(R.string.stats_average_n_a)
}

val beginRange = counter.leastRecent!!
Expand All @@ -111,7 +111,7 @@ class ChartHolder(

private fun getPeriodAverageString(counter: CounterSummary, intervalEntries: List<Entry>, rangeStart: Calendar, rangeEnd: Calendar): String {
if (intervalEntries.isEmpty()) {
return context.getString(R.string.stats_average_n_a)
return activity.getString(R.string.stats_average_n_a)
}

// Hack so to use the end of this interval and not at the beginning of the next,
Expand All @@ -135,19 +135,19 @@ class ChartHolder(
val days = ChronoUnit.DAYS.count(startDate, endDate)
val avgPerDay = count.toFloat() / days
return if (avgPerDay > 1) {
context.getString(R.string.stats_average_per_day, avgPerDay)
activity.getString(R.string.stats_average_per_day, avgPerDay)
} else {
context.getString(R.string.stats_average_every_days, 1 / avgPerDay)
activity.getString(R.string.stats_average_every_days, 1 / avgPerDay)
}
}

private fun getAverageStringPerHour(count: Int, startDate: Date, endDate: Date): String {
val hours = ChronoUnit.HOURS.count(startDate, endDate)
val avgPerHour = count.toFloat() / hours
return if (avgPerHour > 1) {
context.getString(R.string.stats_average_per_hour, avgPerHour)
activity.getString(R.string.stats_average_per_hour, avgPerHour)
} else {
context.getString(R.string.stats_average_every_hours, 1 / avgPerHour)
activity.getString(R.string.stats_average_every_hours, 1 / avgPerHour)
}
}
}
53 changes: 35 additions & 18 deletions app/src/main/java/org/kde/bettercounter/ui/DateTimePicker.kt
Original file line number Diff line number Diff line change
@@ -1,30 +1,47 @@
package org.kde.bettercounter.ui

import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Context
import android.text.format.DateFormat
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import java.util.Calendar
import java.util.Date

fun showDateTimePicker(context: Context, initialDateTime: Calendar, callback: (Calendar) -> Unit) {
fun showDateTimePicker(activity: AppCompatActivity, initialDateTime: Calendar, callback: (Calendar) -> Unit) {
val initialHour = initialDateTime.get(Calendar.HOUR_OF_DAY)
val initialMinute = initialDateTime.get(Calendar.MINUTE)
val use24HourClock = DateFormat.is24HourFormat(context)
showDatePicker(context, initialDateTime) { cal ->
TimePickerDialog(context, { _, hour, minute ->
cal.set(Calendar.MINUTE, minute)
cal.set(Calendar.HOUR_OF_DAY, hour)
callback(cal)
}, initialHour, initialMinute, use24HourClock).show()
}}
val timeFormat = if (DateFormat.is24HourFormat(activity)) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H
showDatePicker(activity, initialDateTime) { cal ->
MaterialTimePicker.Builder()
.setTimeFormat(timeFormat)
.setHour(initialHour)
.setMinute(initialMinute)
.setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
.build().apply {
addOnPositiveButtonClickListener {
cal.set(Calendar.MINUTE, minute)
cal.set(Calendar.HOUR_OF_DAY, hour)
callback(cal)
}
}.show(activity.supportFragmentManager, "timePicker")
}
}

fun showDatePicker(context: Context, initialDateTime: Calendar, callback: (Calendar) -> Unit) {
val initialYear = initialDateTime.get(Calendar.YEAR)
val initialMonth = initialDateTime.get(Calendar.MONTH)
val initialDay = initialDateTime.get(Calendar.DAY_OF_MONTH)
DatePickerDialog(context, { _, year, month, day ->
val cal = Calendar.getInstance()
cal.set(year, month, day)
callback(cal)
}, initialYear, initialMonth, initialDay).show()
fun showDatePicker(activity: AppCompatActivity, initialDateTime: Calendar, callback: (Calendar) -> Unit) {
MaterialDatePicker.Builder.datePicker()
.setSelection(initialDateTime.timeInMillis)
.build().apply {
addOnPositiveButtonClickListener {
val cal = Calendar.getInstance()
cal.timeInMillis = it
callback(cal)
}
}
.show(activity.supportFragmentManager, "datePicker")
}
8 changes: 5 additions & 3 deletions app/src/main/java/org/kde/bettercounter/ui/EntryViewHolder.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.kde.bettercounter.ui

import android.app.Activity
import android.content.Context
import android.view.Gravity
import android.view.HapticFeedbackConstants
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import io.github.douglasjunior.androidSimpleTooltip.SimpleTooltip
Expand All @@ -14,7 +16,7 @@ import org.kde.bettercounter.persistence.Tutorial
import java.util.Calendar

class EntryViewHolder(
private val context: Context,
private val activity: AppCompatActivity,
val binding: FragmentEntryBinding,
private var viewModel: ViewModel,
private val touchHelper: ItemTouchHelper,
Expand All @@ -27,7 +29,7 @@ class EntryViewHolder(
viewModel.incrementCounter(counter.name)
if (!viewModel.isTutorialShown(Tutorial.PICKDATE)) {
viewModel.setTutorialShown(Tutorial.PICKDATE)
SimpleTooltip.Builder(context)
SimpleTooltip.Builder(activity)
.anchorView(binding.increaseButton)
.text(R.string.tutorial_pickdate)
.gravity(Gravity.BOTTOM)
Expand All @@ -38,7 +40,7 @@ class EntryViewHolder(
}
}
binding.increaseButton.setOnLongClickListener {
showDateTimePicker(context, Calendar.getInstance()) { pickedDateTime ->
showDateTimePicker(activity, Calendar.getInstance()) { pickedDateTime ->
viewModel.incrementCounter(counter.name, pickedDateTime.time)
}
true
Expand Down
36 changes: 36 additions & 0 deletions app/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,61 @@
<!-- Overflow menu backgound -->
<item name="colorSurfaceContainer">@color/colorDarkBackground</item>

<!-- TimePicker and DatePicker background -->
<item name="colorSurfaceContainerHigh">@color/colorLightBackground</item>

<!-- FAB -->
<item name="colorPrimaryContainer">@color/colorAccent</item>
<item name="colorOnPrimaryContainer">@color/colorLightBackground</item>

<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>

<item name="materialTimePickerTheme">@style/ThemeOverlay.App.MaterialTimePicker</item>
<item name="materialCalendarTheme">@style/ThemeOverlay.App.MaterialCalendar</item>
<item name="materialAlertDialogTheme">@style/ThemeOverlay.App.MaterialAlertDialog</item>
</style>

<!-- MaterialDialog (used in CounterSettingsDialog and the import/export progress dialogs) -->
<style name="ThemeOverlay.App.MaterialAlertDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
<item name="alertDialogStyle">@style/MaterialAlertDialog.App</item>
<item name="android:background">@color/colorLightBackground</item>
<item name="materialAlertDialogTitleTextStyle">@style/DialogTitle</item>
</style>
<style name="MaterialAlertDialog.App" parent="MaterialAlertDialog.Material3">
<item name="shapeAppearance">@style/ShapeAppearance.App.MediumComponent</item>
<item name="shapeAppearanceOverlay">@null</item>
</style>

<!-- MaterialCalendar (DatePicker) -->
<style name="ThemeOverlay.App.MaterialCalendar" parent="ThemeOverlay.Material3.MaterialCalendar">
<item name="materialCalendarStyle">@style/MaterialCalendar.App</item>
<item name="materialCalendarHeaderTitle">@style/DialogTitle</item>

</style>
<style name="MaterialCalendar.App" parent="Widget.Material3.MaterialCalendar">
<item name="shapeAppearance">@style/ShapeAppearance.App.MediumComponent</item>
<item name="shapeAppearanceOverlay">@null</item>
</style>

<!-- MaterialTimePicker -->
<!-- Note: I inherit from the Material 2 theme (MaterialComponents.TimePicker) instead of the
Material 3 one (Material3.MaterialTimePicker) because the AM/PM selector looks nicer. -->
<style name="ThemeOverlay.App.MaterialTimePicker" parent="ThemeOverlay.MaterialComponents.TimePicker">
<item name="materialTimePickerStyle">@style/MaterialTimePicker.App</item>
<item name="materialTimePickerTitleStyle">@style/DialogTitle</item>
</style>
<style name="MaterialTimePicker.App" parent="Widget.Material3.MaterialTimePicker">
<item name="shapeAppearance">@style/ShapeAppearance.App.MediumComponent</item>
<item name="shapeAppearanceOverlay">@null</item>
</style>

<!-- Make titles look the same for all dialogs -->
<style name="DialogTitle" parent="@style/MaterialAlertDialog.Material3.Title.Text">
<item name="android:textAppearance">@style/TextAppearance.AppCompat.Title</item>
</style>

<!-- Reduce corner radius from the Material 3 default to something less pronounced -->
<style name="ShapeAppearance.App.MediumComponent" parent="ShapeAppearance.Material3.MediumComponent">
<item name="cornerSize">8dp</item>
</style>
Expand Down

0 comments on commit f5432de

Please sign in to comment.