diff --git a/app/src/main/java/com/best/deskclock/timer/TimerItem.java b/app/src/main/java/com/best/deskclock/timer/TimerItem.java index 8b8a8e195..fb0f3ec87 100644 --- a/app/src/main/java/com/best/deskclock/timer/TimerItem.java +++ b/app/src/main/java/com/best/deskclock/timer/TimerItem.java @@ -15,8 +15,6 @@ import android.graphics.Color; import android.graphics.Typeface; import android.os.SystemClock; import android.util.AttributeSet; -import android.view.ViewGroup; -import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.TextView; @@ -28,6 +26,7 @@ import com.best.deskclock.data.DataModel; import com.best.deskclock.data.Timer; import com.best.deskclock.utils.ThemeUtils; +import com.best.deskclock.widget.CircleButtonsLayout; import com.google.android.material.button.MaterialButton; import com.google.android.material.color.MaterialColors; @@ -38,10 +37,12 @@ import java.util.Locale; */ public class TimerItem extends ConstraintLayout { + Context mContext; + /** * The container of TimerCircleView and TimerTextController */ - private FrameLayout mCircleContainer; + private CircleButtonsLayout mCircleContainer; /** * Formats and displays the text in the timer. @@ -59,6 +60,9 @@ public class TimerItem extends ConstraintLayout { /** A button that resets the timer. */ private ImageButton mResetButton; + /** A button that edits a new duration to the timer. */ + private ImageButton mTimerEditNewDurationButton; + /** A button that adds time to the timer. */ private MaterialButton mAddTimeButton; @@ -82,7 +86,6 @@ public class TimerItem extends ConstraintLayout { private boolean mIsTablet; private boolean mIsPortrait; - private boolean mIsLandscape; public TimerItem(Context context) { this(context, null); @@ -96,29 +99,22 @@ public class TimerItem extends ConstraintLayout { protected void onFinishInflate() { super.onFinishInflate(); + mContext = getContext(); mIsTablet = ThemeUtils.isTablet(); mIsPortrait = ThemeUtils.isPortrait(); - mIsLandscape = ThemeUtils.isLandscape(); - // To avoid creating a layout specifically for tablets, adjust the layout width and height here - if (mIsTablet && mIsLandscape) { - final ViewGroup.LayoutParams params = getLayoutParams(); - params.width = LayoutParams.MATCH_PARENT; - params.height = LayoutParams.WRAP_CONTENT; - setLayoutParams(params); - } - - setBackground(ThemeUtils.cardBackground(getContext())); + setBackground(ThemeUtils.cardBackground(mContext)); mLabelView = findViewById(R.id.timer_label); mResetButton = findViewById(R.id.reset); mAddTimeButton = findViewById(R.id.timer_add_time_button); mPlayPauseButton = findViewById(R.id.play_pause); mCircleContainer = findViewById(R.id.circle_container); - mCircleView = mCircleContainer.findViewById(R.id.timer_time); - // Displays the remaining time or time since expiration. Timer text serves as a virtual start/stop button. - mTimerText = mCircleContainer.findViewById(R.id.timer_time_text); - final int colorAccent = MaterialColors.getColor(getContext(), + mCircleView = findViewById(R.id.timer_time); + // Displays the remaining time or time since expiration. + // Timer text serves as a virtual start/stop button. + mTimerText = findViewById(R.id.timer_time_text); + final int colorAccent = MaterialColors.getColor(mContext, com.google.android.material.R.attr.colorPrimary, Color.BLACK); final int textColorPrimary = mTimerText.getCurrentTextColor(); final ColorStateList timeTextColor = new ColorStateList( @@ -126,32 +122,12 @@ public class TimerItem extends ConstraintLayout { new int[]{textColorPrimary, colorAccent}); mTimerText.setTextColor(timeTextColor); mTimerTextController = new TimerTextController(mTimerText); - - // Necessary to avoid the null pointer exception, - // as only the timer_item layout for portrait mode has these attributes + mTimerEditNewDurationButton = findViewById(R.id.timer_edit_new_duration_button); + // Needed to avoid the null pointer exception, as only phones in portrait mode with + // multiple timers have this text if (isPortraitPhoneWithMultipleTimers()) { mTimerTotalDurationText = findViewById(R.id.timer_total_duration); } - - // The size of the Play/Pause and add time buttons are reduced for phones in landscape mode - // due to the size of the timers unlike tablets - if (!mIsTablet && mIsLandscape) { - mAddTimeButton.setIncludeFontPadding(false); - mAddTimeButton.setMinHeight(0); - mAddTimeButton.setMinimumHeight(0); - mAddTimeButton.setMinWidth(0); - mAddTimeButton.setMinimumWidth(0); - mAddTimeButton.setPadding(ThemeUtils.convertDpToPixels(10, getContext()), mAddTimeButton.getPaddingTop(), - ThemeUtils.convertDpToPixels(10, getContext()), mAddTimeButton.getPaddingBottom()); - - mPlayPauseButton.setIncludeFontPadding(false); - mPlayPauseButton.setMinHeight(0); - mPlayPauseButton.setMinimumHeight(0); - mPlayPauseButton.setMinWidth(0); - mPlayPauseButton.setMinimumWidth(0); - mPlayPauseButton.setPadding(ThemeUtils.convertDpToPixels(20, getContext()), mPlayPauseButton.getPaddingTop(), - ThemeUtils.convertDpToPixels(20, getContext()), mPlayPauseButton.getPaddingBottom()); - } } /** @@ -163,8 +139,7 @@ public class TimerItem extends ConstraintLayout { mTimerTextController.setTimeString(timer.getRemainingTime()); if (mCircleView != null) { - final boolean hideCircle = ((timer.isExpired() || timer.isMissed()) && blinkOff) - || (!mIsTablet && mIsLandscape); + final boolean hideCircle = ((timer.isExpired() || timer.isMissed()) && blinkOff); mCircleView.setVisibility(hideCircle ? INVISIBLE : VISIBLE); @@ -187,6 +162,7 @@ public class TimerItem extends ConstraintLayout { // Initialize the time. mTimerTextController.setTimeString(timer.getRemainingTime()); + // Initialize text for timer total duration if (isPortraitPhoneWithMultipleTimers() && mTimerTotalDurationText != null) { mTimerTotalDurationText.setText(timer.getTotalDuration()); } @@ -203,54 +179,86 @@ public class TimerItem extends ConstraintLayout { } // Initialize the circle - // (the circle is hidden for landscape phones because there is not enough space) if (mCircleView != null) { - final boolean hideCircle = !mIsTablet && mIsLandscape; - - mCircleView.setVisibility(hideCircle ? INVISIBLE : VISIBLE); - - if (!hideCircle) { - mCircleView.update(timer); - } + mCircleView.update(timer); } + // Initialize the alpha value of the time text color mTimerText.setAlpha(1f); - // Initialize the time value to add to timer in the "timer_add_time_button" + // Initialize the time value to add to timer in the "Add time" button String buttonTime = timer.getButtonTime(); long totalSeconds = Long.parseLong(buttonTime); long buttonTimeMinutes = (totalSeconds) / 60; long buttonTimeSeconds = totalSeconds % 60; - String buttonTimeFormatted = String.format( Locale.getDefault(), buttonTimeMinutes < 10 ? "%d:%02d" : "%02d:%02d", buttonTimeMinutes, buttonTimeSeconds); - mAddTimeButton.setText(getContext().getString(R.string.timer_add_custom_time, buttonTimeFormatted)); + mAddTimeButton.setText(mContext.getString(R.string.timer_add_custom_time, buttonTimeFormatted)); String buttonContentDescription = buttonTimeSeconds == 0 - ? getContext().getString(R.string.timer_add_custom_time_description, String.valueOf(buttonTimeMinutes)) - : getContext().getString(R.string.timer_add_custom_time_with_seconds_description, + ? mContext.getString(R.string.timer_add_custom_time_description, String.valueOf(buttonTimeMinutes)) + : mContext.getString(R.string.timer_add_custom_time_with_seconds_description, String.valueOf(buttonTimeMinutes), String.valueOf(buttonTimeSeconds)); mAddTimeButton.setContentDescription(buttonContentDescription); + // For tablets in portrait mode with single timer, adjust the size of the "Add time" and + // "Play/Pause" buttons + final ConstraintLayout.LayoutParams addTimeButtonParams = + (ConstraintLayout.LayoutParams) mAddTimeButton.getLayoutParams(); + final ConstraintLayout.LayoutParams playPauseButtonParams = + (ConstraintLayout.LayoutParams) mPlayPauseButton.getLayoutParams(); + + if (mIsTablet && mIsPortrait && DataModel.getDataModel().getTimers().size() == 1) { + addTimeButtonParams.matchConstraintMaxHeight = ThemeUtils.convertDpToPixels(200, mContext); + playPauseButtonParams.matchConstraintMaxHeight = ThemeUtils.convertDpToPixels(200, mContext); + } + // Initialize some potentially expensive areas of the user interface only on state changes. if (timer.getState() != mLastState) { - final Context context = getContext(); - final String resetDesc = context.getString(R.string.reset); + final String resetDesc = mContext.getString(R.string.reset); + mResetButton.setVisibility(VISIBLE); mResetButton.setContentDescription(resetDesc); mAddTimeButton.setVisibility(VISIBLE); mLastState = timer.getState(); - // If the timer is reset, add a top margin so that the "Play" button is not stuck to the "Delete" button. + // For phones in portrait mode, when there are multiple timers and they are reset, + // adjust the constraints, margins and paddings of the "Play/Pause" button to have + // a suitable reduced view if (isPortraitPhoneWithMultipleTimers()) { - final ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) mPlayPauseButton.getLayoutParams(); - params.topMargin = ThemeUtils.convertDpToPixels(timer.getState().equals(Timer.State.RESET) ? 10 : 0, context); - mPlayPauseButton.setLayoutParams(params); + if (mLastState.equals(Timer.State.RESET)) { + playPauseButtonParams.width = LayoutParams.WRAP_CONTENT; + + playPauseButtonParams.startToStart = ConstraintLayout.LayoutParams.UNSET; + playPauseButtonParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID; + playPauseButtonParams.topToBottom = mLabelView.getId(); + playPauseButtonParams.bottomToBottom = ConstraintLayout.LayoutParams.UNSET; + + playPauseButtonParams.rightMargin = ThemeUtils.convertDpToPixels(12, mContext); + playPauseButtonParams.topMargin = ThemeUtils.convertDpToPixels(10, mContext); + + mPlayPauseButton.setPadding(0, 0, 0, 0); + } else { + int playPauseButtonPadding = ThemeUtils.convertDpToPixels(24, mContext); + + playPauseButtonParams.width = 0; + + playPauseButtonParams.startToStart = mAddTimeButton.getId(); + playPauseButtonParams.endToEnd = mAddTimeButton.getId(); + playPauseButtonParams.topToBottom = ConstraintLayout.LayoutParams.UNSET; + playPauseButtonParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; + + playPauseButtonParams.rightMargin = 0; + playPauseButtonParams.topMargin = 0; + + mPlayPauseButton.setPadding(playPauseButtonPadding, playPauseButtonPadding, + playPauseButtonPadding, playPauseButtonPadding); + } } switch (mLastState) { @@ -258,7 +266,11 @@ public class TimerItem extends ConstraintLayout { mResetButton.setVisibility(GONE); mResetButton.setContentDescription(null); mAddTimeButton.setVisibility(INVISIBLE); - mPlayPauseButton.setIcon(AppCompatResources.getDrawable(context, R.drawable.ic_fab_play)); + mPlayPauseButton.setIcon(AppCompatResources.getDrawable(mContext, R.drawable.ic_fab_play)); + + if (isTabletOrLandscapePhone() || isPhoneWithSingleTimer()) { + mTimerEditNewDurationButton.setVisibility(VISIBLE); + } if (isPortraitPhoneWithMultipleTimers()) { mCircleContainer.setVisibility(GONE); @@ -267,7 +279,12 @@ public class TimerItem extends ConstraintLayout { } case PAUSED -> { - mPlayPauseButton.setIcon(AppCompatResources.getDrawable(context, R.drawable.ic_fab_play)); + mPlayPauseButton.setIcon(AppCompatResources.getDrawable(mContext, R.drawable.ic_fab_play)); + + if (isTabletOrLandscapePhone() || isPhoneWithSingleTimer()) { + mTimerEditNewDurationButton.setVisibility(GONE); + } + if (isPortraitPhoneWithMultipleTimers()) { mCircleContainer.setVisibility(VISIBLE); mTimerTotalDurationText.setVisibility(GONE); @@ -275,7 +292,11 @@ public class TimerItem extends ConstraintLayout { } case RUNNING -> { - mPlayPauseButton.setIcon(AppCompatResources.getDrawable(context, R.drawable.ic_fab_pause)); + mPlayPauseButton.setIcon(AppCompatResources.getDrawable(mContext, R.drawable.ic_fab_pause)); + + if (isTabletOrLandscapePhone() || isPhoneWithSingleTimer()) { + mTimerEditNewDurationButton.setVisibility(GONE); + } if (isPortraitPhoneWithMultipleTimers()) { mCircleContainer.setVisibility(VISIBLE); @@ -284,14 +305,38 @@ public class TimerItem extends ConstraintLayout { } case EXPIRED, MISSED -> { + mPlayPauseButton.setIcon(AppCompatResources.getDrawable(mContext, R.drawable.ic_fab_stop)); + + if (isTabletOrLandscapePhone() || isPhoneWithSingleTimer()) { + mTimerEditNewDurationButton.setVisibility(GONE); + } + mResetButton.setVisibility(GONE); - mPlayPauseButton.setIcon(AppCompatResources.getDrawable(context, R.drawable.ic_fab_stop)); } } } } + /** + * @return {@code true} if the device is a phone in portrait mode with multiple timers displayed. + * {@code false} otherwise. + */ private boolean isPortraitPhoneWithMultipleTimers() { return !mIsTablet && mIsPortrait && DataModel.getDataModel().getTimers().size() > 1; } + + /** + * @return {@code true} if the device is a tablet or phone in landscape mode. + * {@code false} otherwise. + */ + private boolean isTabletOrLandscapePhone() { + return (mIsTablet || !mIsPortrait); + } + + /** + * @return {@code true} if the device is a phone with a single timer displayed. + */ + private boolean isPhoneWithSingleTimer() { + return !mIsTablet && DataModel.getDataModel().getTimers().size() == 1; + } } \ No newline at end of file diff --git a/app/src/main/java/com/best/deskclock/timer/TimerViewHolder.java b/app/src/main/java/com/best/deskclock/timer/TimerViewHolder.java index 4a100b69c..421cffe7e 100644 --- a/app/src/main/java/com/best/deskclock/timer/TimerViewHolder.java +++ b/app/src/main/java/com/best/deskclock/timer/TimerViewHolder.java @@ -44,6 +44,7 @@ public class TimerViewHolder extends RecyclerView.ViewHolder { View timerLabel = view.findViewById(R.id.timer_label); View resetButton = view.findViewById(R.id.reset); View timerTotalDuration = view.findViewById(R.id.timer_total_duration); + View timerEditNewDurationButton = view.findViewById(R.id.timer_edit_new_duration_button); View addTimeButton = view.findViewById(R.id.timer_add_time_button); View circleContainer = view.findViewById(R.id.circle_container); View timerTimeText = view.findViewById(R.id.timer_time_text); @@ -61,15 +62,6 @@ public class TimerViewHolder extends RecyclerView.ViewHolder { } }; - View.OnLongClickListener setNewDurationListener = v -> { - if (!getTimer().isReset()) { - return false; - } - - mTimerClickHandler.onDurationClicked(getTimer()); - return true; - }; - timerLabel.setOnClickListener(v -> mTimerClickHandler.onEditLabelClicked(getTimer())); resetButton.setOnClickListener(v -> { @@ -96,23 +88,37 @@ public class TimerViewHolder extends RecyclerView.ViewHolder { return true; }); + // Only possible for portrait mode phones with multiple timers if (timerTotalDuration != null) { - timerTotalDuration.setOnLongClickListener(setNewDurationListener); + timerTotalDuration.setOnClickListener(v -> { + if (!getTimer().isReset()) { + return; + } + + mTimerClickHandler.onDurationClicked(getTimer()); + }); } - // If we click on the circular container when the phones (only) are in landscape mode, - // indicating a title for the timers is not possible so in this case we click on the - // time to start the timer. - // Long press on the time displays the dialog to set a new timer duration. - if (!ThemeUtils.isTablet() && ThemeUtils.isLandscape()) { - timerTimeText.setOnLongClickListener(setNewDurationListener); - timerTimeText.setOnClickListener(playPauseListener); - } else { - circleContainer.setOnLongClickListener(setNewDurationListener); + // Only possible for tablets, landscape phones or when there is only one timer + if (timerEditNewDurationButton != null) { + timerEditNewDurationButton.setOnClickListener(v -> { + if (!getTimer().isReset()) { + return; + } + + mTimerClickHandler.onDurationClicked(getTimer()); + }); + } + + if (circleContainer != null) { circleContainer.setOnClickListener(playPauseListener); circleContainer.setOnTouchListener(new Utils.CircleTouchListener()); } + if (!ThemeUtils.isTablet() && ThemeUtils.isLandscape()) { + timerTimeText.setOnClickListener(playPauseListener); + } + playPauseButton.setOnClickListener(playPauseListener); deleteButton.setOnClickListener(v -> { diff --git a/app/src/main/java/com/best/deskclock/widget/CircleButtonsLayout.java b/app/src/main/java/com/best/deskclock/widget/CircleButtonsLayout.java new file mode 100644 index 000000000..179d227d9 --- /dev/null +++ b/app/src/main/java/com/best/deskclock/widget/CircleButtonsLayout.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * modified + * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only + */ + +package com.best.deskclock.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import com.best.deskclock.R; +import com.best.deskclock.utils.ThemeUtils; + +/** + * This class adjusts the location of the reset button. + */ +public class CircleButtonsLayout extends FrameLayout { + + private final float mDiamOffset; + + @SuppressWarnings("unused") + public CircleButtonsLayout(Context context) { + this(context, null); + } + + public CircleButtonsLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + final float strokeSize = ThemeUtils.convertDpToPixels(6, context); + mDiamOffset = strokeSize * 2; + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // We must call onMeasure both before and after re-measuring our views because the circle + // may not always be drawn here yet. The first onMeasure will force the circle to be drawn, + // and the second will force our re-measurements to take effect. + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + remeasureViews(); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + protected void remeasureViews() { + View mCircleView = findViewById(R.id.timer_time); + View mResetAddButton = findViewById(R.id.reset); + + final int frameWidth = mCircleView.getMeasuredWidth(); + final int frameHeight = mCircleView.getMeasuredHeight(); + final int minBound = Math.min(frameWidth, frameHeight); + final int circleDiam = (int) (minBound - mDiamOffset); + + if (mResetAddButton != null) { + final MarginLayoutParams resetParams = (MarginLayoutParams) mResetAddButton + .getLayoutParams(); + resetParams.bottomMargin = circleDiam / 8; + if (minBound == frameWidth) { + resetParams.bottomMargin += (frameHeight - frameWidth) / 2; + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 000000000..210338e48 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/layout-land/timer_item.xml b/app/src/main/res/layout-land/timer_item.xml index 6020b7c65..3f52ee97f 100644 --- a/app/src/main/res/layout-land/timer_item.xml +++ b/app/src/main/res/layout-land/timer_item.xml @@ -5,14 +5,16 @@ modified --> + - - - - + app:layout_constraintEnd_toStartOf="@+id/timer_add_time_button" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent"> - + - - + + - - + + + app:layout_constraintBottom_toTopOf="@+id/play_pause" + tools:text="+ 1:00" /> diff --git a/app/src/main/res/layout-sw600dp-land/timer_item.xml b/app/src/main/res/layout-sw600dp-land/timer_item.xml new file mode 100644 index 000000000..639e91f10 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/timer_item.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/timer_item.xml b/app/src/main/res/layout/timer_item.xml index 96029d418..d747334ed 100644 --- a/app/src/main/res/layout/timer_item.xml +++ b/app/src/main/res/layout/timer_item.xml @@ -14,6 +14,7 @@ android:layout_height="wrap_content" android:layout_marginVertical="4dp" android:layout_marginHorizontal="10dp" + android:paddingBottom="5dp" tools:background="@drawable/card_background_for_preview"> - + app:layout_constraintEnd_toStartOf="@+id/timer_add_time_button" + app:layout_constraintTop_toBottomOf="@+id/timer_label" + app:layout_constraintBottom_toBottomOf="parent" + tools:ignore="InconsistentLayout"> + android:layout_height="match_parent" + tools:ignore="InconsistentLayout" /> - + - + + + + + + + + + + + tools:visibility="visible" /> - - + diff --git a/app/src/main/res/layout/timer_single_item.xml b/app/src/main/res/layout/timer_single_item.xml index a5db7e3eb..8328d978c 100644 --- a/app/src/main/res/layout/timer_single_item.xml +++ b/app/src/main/res/layout/timer_single_item.xml @@ -22,18 +22,11 @@ app:layout_constraintGuide_percent="0.70" /> - - + app:layout_constraintGuide_percent="0.5" /> - - + + + + + + - - + 125sp - 40sp 0.65 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 742f29292..cc177dc0f 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -10,7 +10,6 @@ 64sp 294dp - 32sp 0.2 0.55 diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/05.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/05.jpg index bfdadafe5..5a611df70 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/05.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/05.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/06.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/06.jpg index e85b60071..05204f6fd 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/06.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/06.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/07.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/07.jpg index bb5311174..5073fad5f 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/07.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/07.jpg differ