From 7476d942895c72bc1a4509387af43304fbf1d406 Mon Sep 17 00:00:00 2001 From: Candice Lo Date: Sun, 5 Feb 2023 08:19:07 +0000 Subject: [PATCH] Create SeekBar with icon buttons for common usage Create a custom view includes a seekbar and two icon buttons on both ends. We could control the progress of the seekbar through the icon buttons. Bug: 242326166 Test: atest SystemUITests:com.android.systemui.common.ui.view.SeekBarWithIconButtonsViewTest Change-Id: I9dc39008fdc7b6964884649e0374861e70ccd951 Merged-In: If64490a6bdc599c66ae55b8f75e438b11d7c1b54 --- .../res/layout/seekbar_with_icon_buttons.xml | 73 +++++++ packages/SystemUI/res/values/attrs.xml | 7 + packages/SystemUI/res/values/dimens.xml | 3 + .../ui/view/SeekBarWithIconButtonsView.java | 195 ++++++++++++++++++ .../view/SeekBarWithIconButtonsViewTest.java | 97 +++++++++ 5 files changed, 375 insertions(+) create mode 100644 packages/SystemUI/res/layout/seekbar_with_icon_buttons.xml create mode 100644 packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java diff --git a/packages/SystemUI/res/layout/seekbar_with_icon_buttons.xml b/packages/SystemUI/res/layout/seekbar_with_icon_buttons.xml new file mode 100644 index 0000000000000..52d1d4fa9bf90 --- /dev/null +++ b/packages/SystemUI/res/layout/seekbar_with_icon_buttons.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index f46266b339d1c..e346fe4ab3ef6 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -214,5 +214,12 @@ + + + + + + + diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 939171479d32d..d48ea214e8d97 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1409,6 +1409,9 @@ 4dp 8dp + + 24dp + 3dp 1dp diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java b/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java new file mode 100644 index 0000000000000..e8288a0d2a875 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.common.ui.view; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.SeekBar; + +import com.android.systemui.R; + +/** + * The layout contains a seekbar whose progress could be modified + * through the icons on two ends of the seekbar. + */ +public class SeekBarWithIconButtonsView extends LinearLayout { + + private static final int DEFAULT_SEEKBAR_MAX = 6; + private static final int DEFAULT_SEEKBAR_PROGRESS = 0; + + private ViewGroup mIconStartFrame; + private ViewGroup mIconEndFrame; + private ImageView mIconStart; + private ImageView mIconEnd; + private SeekBar mSeekbar; + + private SeekBarChangeListener mSeekBarListener = new SeekBarChangeListener(); + + public SeekBarWithIconButtonsView(Context context) { + this(context, null); + } + + public SeekBarWithIconButtonsView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SeekBarWithIconButtonsView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SeekBarWithIconButtonsView(Context context, + AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + LayoutInflater.from(context).inflate( + R.layout.seekbar_with_icon_buttons, this, /* attachToRoot= */ true); + + mIconStartFrame = findViewById(R.id.icon_start_frame); + mIconEndFrame = findViewById(R.id.icon_end_frame); + mIconStart = findViewById(R.id.icon_start); + mIconEnd = findViewById(R.id.icon_end); + mSeekbar = findViewById(R.id.seekbar); + + if (attrs != null) { + TypedArray typedArray = context.obtainStyledAttributes( + attrs, + R.styleable.SeekBarWithIconButtonsView_Layout, + defStyleAttr, defStyleRes + ); + int max = typedArray.getInt( + R.styleable.SeekBarWithIconButtonsView_Layout_max, DEFAULT_SEEKBAR_MAX); + int progress = typedArray.getInt( + R.styleable.SeekBarWithIconButtonsView_Layout_progress, + DEFAULT_SEEKBAR_PROGRESS); + mSeekbar.setMax(max); + setProgress(progress); + + int iconStartFrameContentDescriptionId = typedArray.getResourceId( + R.styleable.SeekBarWithIconButtonsView_Layout_iconStartContentDescription, + /* defValue= */ 0); + int iconEndFrameContentDescriptionId = typedArray.getResourceId( + R.styleable.SeekBarWithIconButtonsView_Layout_iconEndContentDescription, + /* defValue= */ 0); + if (iconStartFrameContentDescriptionId != 0) { + final String contentDescription = + context.getString(iconStartFrameContentDescriptionId); + mIconStartFrame.setContentDescription(contentDescription); + } + if (iconEndFrameContentDescriptionId != 0) { + final String contentDescription = + context.getString(iconEndFrameContentDescriptionId); + mIconEndFrame.setContentDescription(contentDescription); + } + + typedArray.recycle(); + } else { + mSeekbar.setMax(DEFAULT_SEEKBAR_MAX); + setProgress(DEFAULT_SEEKBAR_PROGRESS); + } + + mSeekbar.setOnSeekBarChangeListener(mSeekBarListener); + + mIconStart.setOnClickListener((view) -> { + final int progress = mSeekbar.getProgress(); + if (progress > 0) { + mSeekbar.setProgress(progress - 1); + setIconViewAndFrameEnabled(mIconStart, mSeekbar.getProgress() > 0); + } + }); + + mIconEnd.setOnClickListener((view) -> { + final int progress = mSeekbar.getProgress(); + if (progress < mSeekbar.getMax()) { + mSeekbar.setProgress(progress + 1); + setIconViewAndFrameEnabled(mIconEnd, mSeekbar.getProgress() < mSeekbar.getMax()); + } + }); + } + + private static void setIconViewAndFrameEnabled(View iconView, boolean enabled) { + iconView.setEnabled(enabled); + final ViewGroup iconFrame = (ViewGroup) iconView.getParent(); + iconFrame.setEnabled(enabled); + } + + /** + * Sets a onSeekbarChangeListener to the seekbar in the layout. + * We update the Start Icon and End Icon if needed when the seekbar progress is changed. + */ + public void setOnSeekBarChangeListener( + @Nullable SeekBar.OnSeekBarChangeListener onSeekBarChangeListener) { + mSeekBarListener.setOnSeekBarChangeListener(onSeekBarChangeListener); + } + + /** + * Start and End icons might need to be updated when there is a change in seekbar progress. + * Icon Start will need to be enabled when the seekbar progress is larger than 0. + * Icon End will need to be enabled when the seekbar progress is less than Max. + */ + private void updateIconViewIfNeeded(int progress) { + setIconViewAndFrameEnabled(mIconStart, progress > 0); + setIconViewAndFrameEnabled(mIconEnd, progress < mSeekbar.getMax()); + } + + /** + * Sets progress to the seekbar in the layout. + * If the progress is smaller than or equals to 0, the IconStart will be disabled. If the + * progress is larger than or equals to Max, the IconEnd will be disabled. The seekbar progress + * will be constrained in {@link SeekBar}. + */ + public void setProgress(int progress) { + mSeekbar.setProgress(progress); + updateIconViewIfNeeded(progress); + } + + private class SeekBarChangeListener implements SeekBar.OnSeekBarChangeListener { + private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener = null; + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser); + } + updateIconViewIfNeeded(progress); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onStartTrackingTouch(seekBar); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onStopTrackingTouch(seekBar); + } + } + + void setOnSeekBarChangeListener(SeekBar.OnSeekBarChangeListener listener) { + mOnSeekBarChangeListener = listener; + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java new file mode 100644 index 0000000000000..2ed03465c6f0c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.common.ui.view; + +import static com.google.common.truth.Truth.assertThat; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.widget.ImageView; +import android.widget.SeekBar; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link SeekBarWithIconButtonsView} + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class SeekBarWithIconButtonsViewTest extends SysuiTestCase { + + private ImageView mIconStart; + private ImageView mIconEnd; + private SeekBar mSeekbar; + private SeekBarWithIconButtonsView mIconDiscreteSliderLinearLayout; + + @Before + public void setUp() { + mIconDiscreteSliderLinearLayout = new SeekBarWithIconButtonsView(mContext); + mIconStart = mIconDiscreteSliderLinearLayout.findViewById(R.id.icon_start); + mIconEnd = mIconDiscreteSliderLinearLayout.findViewById(R.id.icon_end); + mSeekbar = mIconDiscreteSliderLinearLayout.findViewById(R.id.seekbar); + } + + @Test + public void setSeekBarProgressZero_startIconAndFrameDisabled() { + mIconDiscreteSliderLinearLayout.setProgress(0); + + assertThat(mIconStart.isEnabled()).isFalse(); + assertThat(mIconEnd.isEnabled()).isTrue(); + } + + @Test + public void setSeekBarProgressMax_endIconAndFrameDisabled() { + mIconDiscreteSliderLinearLayout.setProgress(mSeekbar.getMax()); + + assertThat(mIconEnd.isEnabled()).isFalse(); + assertThat(mIconStart.isEnabled()).isTrue(); + } + + @Test + public void setSeekBarProgressMax_allIconsAndFramesEnabled() { + // We are using the default value for the max of seekbar. + // Therefore, the max value will be DEFAULT_SEEKBAR_MAX = 6. + mIconDiscreteSliderLinearLayout.setProgress(1); + + assertThat(mIconStart.isEnabled()).isTrue(); + assertThat(mIconEnd.isEnabled()).isTrue(); + } + + @Test + public void clickIconEnd_currentProgressIsOneToMax_reachesMax() { + mIconDiscreteSliderLinearLayout.setProgress(mSeekbar.getMax() - 1); + mIconEnd.performClick(); + + assertThat(mSeekbar.getProgress()).isEqualTo(mSeekbar.getMax()); + } + + @Test + public void clickIconStart_currentProgressIsOne_reachesZero() { + mIconDiscreteSliderLinearLayout.setProgress(1); + mIconStart.performClick(); + + assertThat(mSeekbar.getProgress()).isEqualTo(0); + } +}