diff --git a/app/src/main/java/com/best/deskclock/data/SettingsDAO.java b/app/src/main/java/com/best/deskclock/data/SettingsDAO.java index b06ff353b..be9c53ed6 100644 --- a/app/src/main/java/com/best/deskclock/data/SettingsDAO.java +++ b/app/src/main/java/com/best/deskclock/data/SettingsDAO.java @@ -484,6 +484,14 @@ public final class SettingsDAO { return Integer.parseInt(string) * 60; } + /** + * @return the timer creation view style. + */ + public static String getTimerCreationViewStyle(SharedPreferences prefs) { + // Default value must match the one in res/xml/settings_timer.xml + return prefs.getString(KEY_TIMER_CREATION_VIEW_STYLE, DEFAULT_TIMER_CREATION_VIEW_STYLE); + } + /** * @return {@code true} if the timer background must be transparent. {@code false} otherwise. */ diff --git a/app/src/main/java/com/best/deskclock/settings/PreferencesDefaultValues.java b/app/src/main/java/com/best/deskclock/settings/PreferencesDefaultValues.java index b69127714..8899361ee 100644 --- a/app/src/main/java/com/best/deskclock/settings/PreferencesDefaultValues.java +++ b/app/src/main/java/com/best/deskclock/settings/PreferencesDefaultValues.java @@ -102,6 +102,8 @@ public class PreferencesDefaultValues { } // Timer + public static final String DEFAULT_TIMER_CREATION_VIEW_STYLE = "keypad"; + public static final String TIMER_CREATION_VIEW_SPINNER_STYLE = "spinner"; public static final String DEFAULT_TIMER_AUTO_SILENCE = "30"; public static final String DEFAULT_TIMER_CRESCENDO_DURATION = "0"; public static final boolean DEFAULT_TIMER_VIBRATE = false; diff --git a/app/src/main/java/com/best/deskclock/settings/PreferencesKeys.java b/app/src/main/java/com/best/deskclock/settings/PreferencesKeys.java index dcb773c23..8d599aba5 100644 --- a/app/src/main/java/com/best/deskclock/settings/PreferencesKeys.java +++ b/app/src/main/java/com/best/deskclock/settings/PreferencesKeys.java @@ -106,6 +106,7 @@ public class PreferencesKeys { public static final String KEY_PREVIEW_ALARM = "key_preview_alarm"; // Timer + public static final String KEY_TIMER_CREATION_VIEW_STYLE = "key_timer_creation_view_style"; public static final String KEY_TIMER_RINGTONE = "key_timer_ringtone"; public static final String KEY_TIMER_AUTO_SILENCE = "key_timer_auto_silence"; public static final String KEY_TIMER_CRESCENDO_DURATION = "key_timer_crescendo_duration"; diff --git a/app/src/main/java/com/best/deskclock/settings/TimerSettingsFragment.java b/app/src/main/java/com/best/deskclock/settings/TimerSettingsFragment.java index aafceb9f1..3c0b4dd9a 100644 --- a/app/src/main/java/com/best/deskclock/settings/TimerSettingsFragment.java +++ b/app/src/main/java/com/best/deskclock/settings/TimerSettingsFragment.java @@ -7,6 +7,7 @@ import static com.best.deskclock.settings.PreferencesKeys.KEY_DEFAULT_TIME_TO_AD import static com.best.deskclock.settings.PreferencesKeys.KEY_DISPLAY_WARNING_BEFORE_DELETING_TIMER; import static com.best.deskclock.settings.PreferencesKeys.KEY_SORT_TIMER; import static com.best.deskclock.settings.PreferencesKeys.KEY_TIMER_AUTO_SILENCE; +import static com.best.deskclock.settings.PreferencesKeys.KEY_TIMER_CREATION_VIEW_STYLE; import static com.best.deskclock.settings.PreferencesKeys.KEY_TIMER_CRESCENDO_DURATION; import static com.best.deskclock.settings.PreferencesKeys.KEY_TIMER_FLIP_ACTION; import static com.best.deskclock.settings.PreferencesKeys.KEY_TIMER_POWER_BUTTON_ACTION; @@ -40,6 +41,7 @@ public class TimerSettingsFragment extends ScreenFragment ListPreference mTimerCrescendoPref; ListPreference mSortTimerPref; ListPreference mDefaultMinutesToAddToTimerPref; + ListPreference mTimerCreationViewStylePref; Preference mTimerRingtonePref; Preference mTimerVibratePref; SwitchPreferenceCompat mTimerVolumeButtonsActionPref; @@ -74,6 +76,7 @@ public class TimerSettingsFragment extends ScreenFragment mDefaultMinutesToAddToTimerPref = findPreference(KEY_DEFAULT_TIME_TO_ADD_TO_TIMER); mTransparentBackgroundPref = findPreference(KEY_TRANSPARENT_BACKGROUND_FOR_EXPIRED_TIMER); mDisplayWarningBeforeDeletingTimerPref = findPreference(KEY_DISPLAY_WARNING_BEFORE_DELETING_TIMER); + mTimerCreationViewStylePref = findPreference(KEY_TIMER_CREATION_VIEW_STYLE); setupPreferences(); } @@ -90,7 +93,8 @@ public class TimerSettingsFragment extends ScreenFragment switch (pref.getKey()) { case KEY_TIMER_RINGTONE -> mTimerRingtonePref.setSummary(DataModel.getDataModel().getTimerRingtoneTitle()); - case KEY_TIMER_AUTO_SILENCE, KEY_TIMER_CRESCENDO_DURATION, KEY_DEFAULT_TIME_TO_ADD_TO_TIMER -> { + case KEY_TIMER_AUTO_SILENCE, KEY_TIMER_CRESCENDO_DURATION, + KEY_DEFAULT_TIME_TO_ADD_TO_TIMER, KEY_TIMER_CREATION_VIEW_STYLE -> { final ListPreference preference = (ListPreference) pref; final int index = preference.findIndexOfValue((String) newValue); preference.setSummary(preference.getEntries()[index]); @@ -148,6 +152,9 @@ public class TimerSettingsFragment extends ScreenFragment mTimerPowerButtonActionPref.setOnPreferenceChangeListener(this); + mTimerCreationViewStylePref.setOnPreferenceChangeListener(this); + mTimerCreationViewStylePref.setSummary(mTimerCreationViewStylePref.getEntry()); + SensorManager sensorManager = (SensorManager) requireActivity().getSystemService(Context.SENSOR_SERVICE); if (sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) == null) { mTimerFlipActionPref.setChecked(false); diff --git a/app/src/main/java/com/best/deskclock/timer/CustomTimerSpinnerSetupView.java b/app/src/main/java/com/best/deskclock/timer/CustomTimerSpinnerSetupView.java new file mode 100644 index 000000000..1c142e427 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/timer/CustomTimerSpinnerSetupView.java @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-only + +package com.best.deskclock.timer; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.NumberPicker; + +import androidx.annotation.Nullable; + +import com.best.deskclock.R; + +import java.util.concurrent.TimeUnit; + +/** + * Custom component to display a time selection view using spinners used when creating timers. + */ +public class CustomTimerSpinnerSetupView extends LinearLayout { + NumberPicker mHourPicker; + NumberPicker mMinutePicker; + NumberPicker mSecondPicker; + + @Nullable + OnValueChangeListener mOnValueChangeListener; + + public CustomTimerSpinnerSetupView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + View rootView = inflate(context, R.layout.timer_spinner_setup_view, this); + mHourPicker = rootView.findViewById(R.id.hour); + mMinutePicker = rootView.findViewById(R.id.minute); + mSecondPicker = rootView.findViewById(R.id.second); + + setupCustomSpinnerDurationPicker(); + } + + private void setupCustomSpinnerDurationPicker() { + mHourPicker.setMinValue(0); + mHourPicker.setMaxValue(24); + + mMinutePicker.setMinValue(0); + mMinutePicker.setMaxValue(59); + + mSecondPicker.setMinValue(0); + mSecondPicker.setMaxValue(59); + + mHourPicker.setOnValueChangedListener((_picker, _oldVal, _newVal) -> { + if (mOnValueChangeListener != null) mOnValueChangeListener.onValueChange(getValue()); + }); + + mMinutePicker.setOnValueChangedListener((_picker, _oldVal, _newVal) -> { + if (mOnValueChangeListener != null) mOnValueChangeListener.onValueChange(getValue()); + }); + + mSecondPicker.setOnValueChangedListener((_picker, _oldVal, _newVal) -> { + if (mOnValueChangeListener != null) mOnValueChangeListener.onValueChange(getValue()); + }); + } + + public void setValue(long valueMillis) { + long hours = TimeUnit.MILLISECONDS.toHours(valueMillis); + long minutes = TimeUnit.MILLISECONDS.toMinutes(valueMillis) % 60; + long seconds = TimeUnit.MILLISECONDS.toSeconds(valueMillis) % 60; + setValue(new DurationObject((int) hours, (int) minutes, (int) seconds)); + } + + public void setValue(DurationObject value) { + mHourPicker.setValue(value.hour()); + mMinutePicker.setValue(value.minute()); + mSecondPicker.setValue(value.second()); + } + + public void reset() { + setValue(new DurationObject(0, 0, 0)); + } + + public DurationObject getValue() { + return new DurationObject(mHourPicker.getValue(), mMinutePicker.getValue(), mSecondPicker.getValue()); + } + + public void setOnChangeListener(OnValueChangeListener onValueChangeListener) { + this.mOnValueChangeListener = onValueChangeListener; + } + + public interface OnValueChangeListener { + void onValueChange(DurationObject duration); + } + + public record DurationObject(int hour, int minute, int second) { + public long toMillis() { + return (((hour * 60L) + minute) * 60 + second) * 1000; + } + } + +} diff --git a/app/src/main/java/com/best/deskclock/timer/TimerFragment.java b/app/src/main/java/com/best/deskclock/timer/TimerFragment.java index 705628a1b..482cbbd21 100644 --- a/app/src/main/java/com/best/deskclock/timer/TimerFragment.java +++ b/app/src/main/java/com/best/deskclock/timer/TimerFragment.java @@ -13,6 +13,7 @@ import static android.view.View.TRANSLATION_Y; import static android.view.View.VISIBLE; import static com.best.deskclock.DeskClockApplication.getDefaultSharedPreferences; +import static com.best.deskclock.settings.PreferencesDefaultValues.TIMER_CREATION_VIEW_SPINNER_STYLE; import static com.best.deskclock.uidata.UiDataModel.Tab.TIMERS; import android.animation.Animator; @@ -68,6 +69,7 @@ public final class TimerFragment extends DeskClockFragment { private RecyclerView mRecyclerView; private Serializable mTimerSetupState; private TimerSetupView mCreateTimerView; + private CustomTimerSpinnerSetupView mCreateTimerSpinnerView; private TimerAdapter mAdapter; private View mTimersView; private View mCurrentView; @@ -114,6 +116,7 @@ public final class TimerFragment extends DeskClockFragment { mRecyclerView = view.findViewById(R.id.recycler_view); mTimersView = view.findViewById(R.id.timer_view); mCreateTimerView = view.findViewById(R.id.timer_setup); + mCreateTimerSpinnerView = view.findViewById(R.id.timer_spinner_setup); mIsTablet = ThemeUtils.isTablet(); mIsLandscape = ThemeUtils.isLandscape(); @@ -127,6 +130,8 @@ public final class TimerFragment extends DeskClockFragment { mRecyclerView.setClipToPadding(false); mCreateTimerView.setFabContainer(this); + mCreateTimerSpinnerView.setOnChangeListener((durationObject) -> + updateFab(FAB_SHRINK_AND_EXPAND)); DataModel.getDataModel().addTimerListener(mAdapter); DataModel.getDataModel().addTimerListener(mTimerWatcher); @@ -187,7 +192,7 @@ public final class TimerFragment extends DeskClockFragment { getViewLifecycleOwner(), new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { - if (isTabSelected() && mCurrentView == mCreateTimerView && hasTimers()) { + if (isTabSelected() && mCurrentView != mTimersView && hasTimers()) { animateToView(mTimersView, false); } else { setEnabled(false); @@ -218,7 +223,7 @@ public final class TimerFragment extends DeskClockFragment { super.onSaveInstanceState(outState); // If the timer creation view is visible, store the input for later restoration. - if (mCurrentView == mCreateTimerView) { + if (mCurrentView != mTimersView) { mTimerSetupState = mCreateTimerView.getState(); outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState); } @@ -230,8 +235,8 @@ public final class TimerFragment extends DeskClockFragment { fab.setImageResource(R.drawable.ic_add); fab.setContentDescription(mContext.getString(R.string.timer_add_timer)); fab.setVisibility(VISIBLE); - } else if (mCurrentView == mCreateTimerView) { - if (mCreateTimerView.hasValidInput()) { + } else if (mCurrentView == getTimerCreationView()) { + if (hasValidInput()) { fab.setImageResource(R.drawable.ic_fab_play); fab.setContentDescription(mContext.getString(R.string.timer_start)); fab.setVisibility(VISIBLE); @@ -243,6 +248,34 @@ public final class TimerFragment extends DeskClockFragment { } } + private boolean isSpinnerCreationView() { + return SettingsDAO.getTimerCreationViewStyle(mPrefs).equals(TIMER_CREATION_VIEW_SPINNER_STYLE); + } + + private boolean hasValidInput() { + if (isSpinnerCreationView()) { + return mCreateTimerSpinnerView.getValue().toMillis() != 0; + } else { + return mCreateTimerView.hasValidInput(); + } + } + + private long getTimeInMillis() { + if (isSpinnerCreationView()) { + return mCreateTimerSpinnerView.getValue().toMillis(); + } else { + return mCreateTimerView.getTimeInMillis(); + } + } + + private View getTimerCreationView() { + if (isSpinnerCreationView()) { + return mCreateTimerSpinnerView; + } else { + return mCreateTimerView; + } + } + @Override public void onUpdateFab(@NonNull ImageView fab) { updateFab(fab); @@ -262,7 +295,7 @@ public final class TimerFragment extends DeskClockFragment { left.setVisibility(INVISIBLE); right.setVisibility(INVISIBLE); - } else if (mCurrentView == mCreateTimerView) { + } else if (mCurrentView == getTimerCreationView()) { right.setVisibility(INVISIBLE); left.setClickable(true); @@ -272,6 +305,7 @@ public final class TimerFragment extends DeskClockFragment { left.setVisibility(hasTimers() ? VISIBLE : INVISIBLE); left.setOnClickListener(v -> { mCreateTimerView.reset(); + mCreateTimerSpinnerView.reset(); animateToView(mTimersView, false); left.announceForAccessibility(mContext.getString(R.string.timer_canceled)); Utils.setVibrationTime(mContext, 10); @@ -282,12 +316,12 @@ public final class TimerFragment extends DeskClockFragment { @Override public void onFabClick(@NonNull ImageView fab) { if (mCurrentView == mTimersView) { - animateToView(mCreateTimerView, true); - } else if (mCurrentView == mCreateTimerView) { + animateToView(getTimerCreationView(), true); + } else if (mCurrentView == getTimerCreationView()) { mCreatingTimer = true; try { // Create the new timer. - final long timerLength = mCreateTimerView.getTimeInMillis(); + final long timerLength = getTimeInMillis(); String defaultTimeToAddToTimer = String.valueOf(SettingsDAO.getDefaultTimeToAddToTimer(mPrefs)); final Timer timer = DataModel.getDataModel().addTimer(timerLength, "", defaultTimeToAddToTimer, false); @@ -323,10 +357,14 @@ public final class TimerFragment extends DeskClockFragment { // Show the creation view; hide the timer view. mTimersView.setVisibility(GONE); - mCreateTimerView.setVisibility(VISIBLE); + + // Reset all possible time picker views to be hidden in order to only show one of them later + mCreateTimerView.setVisibility(GONE); + mCreateTimerSpinnerView.setVisibility(GONE); // Record the fact that the create view is visible. - mCurrentView = mCreateTimerView; + mCurrentView = getTimerCreationView(); + mCurrentView.setVisibility(VISIBLE); // Update the fab and buttons. updateFab(updateTypes); @@ -342,6 +380,7 @@ public final class TimerFragment extends DeskClockFragment { // Show the timer view; hide the creation view. mTimersView.setVisibility(VISIBLE); mCreateTimerView.setVisibility(GONE); + mCreateTimerSpinnerView.setVisibility(GONE); // Record the fact that the create view is visible. mCurrentView = mTimersView; @@ -366,7 +405,7 @@ public final class TimerFragment extends DeskClockFragment { if (toTimers) { mTimersView.setVisibility(VISIBLE); } else { - mCreateTimerView.setVisibility(VISIBLE); + getTimerCreationView().setVisibility(VISIBLE); } // Avoid double-taps by enabling/disabling the set of buttons active on the new view. updateFab(BUTTONS_DISABLE); @@ -415,6 +454,7 @@ public final class TimerFragment extends DeskClockFragment { // Reset the state of the create view. mCreateTimerView.reset(); + mCreateTimerSpinnerView.reset(); } else { showCreateTimerView(FAB_AND_BUTTONS_EXPAND); } @@ -437,8 +477,10 @@ public final class TimerFragment extends DeskClockFragment { super.onAnimationEnd(animation); mTimersView.setTranslationY(0f); mCreateTimerView.setTranslationY(0f); + mCreateTimerSpinnerView.setTranslationY(0f); mTimersView.setAlpha(1f); mCreateTimerView.setAlpha(1f); + mCreateTimerSpinnerView.setAlpha(1f); } }); @@ -543,7 +585,7 @@ public final class TimerFragment extends DeskClockFragment { updateFab(FAB_AND_BUTTONS_IMMEDIATE); if (mCurrentView == mTimersView && mAdapter.getItemCount() == 0) { - animateToView(mCreateTimerView, true); + animateToView(getTimerCreationView(), true); } // Required to adjust the layout for tablets that use either a GridLayoutManager or a LinearLayoutManager. diff --git a/app/src/main/res/layout/timer_fragment.xml b/app/src/main/res/layout/timer_fragment.xml index 131f6cfed..b8f3d7429 100644 --- a/app/src/main/res/layout/timer_fragment.xml +++ b/app/src/main/res/layout/timer_fragment.xml @@ -29,4 +29,11 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/timer_spinner_setup_view.xml b/app/src/main/res/layout/timer_spinner_setup_view.xml new file mode 100644 index 000000000..31f087051 --- /dev/null +++ b/app/src/main/res/layout/timer_spinner_setup_view.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 5b8952f49..14c06cd56 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -407,6 +407,18 @@ spinner + + + @string/timer_creation_view_style_keypad + @string/clock_style_spinner + + + + keypad + spinner + + @string/sort_timer_manually diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb2791971..8330caa8e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1112,6 +1112,10 @@ Timers Set default ringtone, volume and timer sorting + + Timer creation view style + + Keypad Timer vibrate diff --git a/app/src/main/res/xml/settings_timer.xml b/app/src/main/res/xml/settings_timer.xml index e1ab0f5e8..a2d6c3154 100644 --- a/app/src/main/res/xml/settings_timer.xml +++ b/app/src/main/res/xml/settings_timer.xml @@ -14,6 +14,16 @@ app:iconSpaceReserved="false" tools:layout="@layout/settings_preference_category_layout"> + +