From cc5808cbd748b154a5143638ad580a15d9671eef Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Wed, 19 Jan 2022 21:41:17 +0800 Subject: [PATCH 1/2] =?UTF-8?q?New=20feature=20=E2=80=9CText=20and=20readi?= =?UTF-8?q?ng=20options=E2=80=9D=20for=20SetupWizard,=20Wallpaper,=20and?= =?UTF-8?q?=20Settings=20(8/n).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extending the LabeledSeekBarPreference 1) Add new attributes, "iconStart", "iconEnd", "iconStartContentDescription", "iconEndContentDescription" 2) Add new interface setOnSeekBarChangeListener - It will be integrated with display/font size items in next patches. Bug: 211503117 Test: make -j64 RunSettingsRoboTests ROBOTEST_FILTER=LabeledSeekBarPreferenceTest Change-Id: Id8fe4fb68062c0e92ca4c291d2f7c47303e8691e --- res/layout/icon_discrete_slider.xml | 72 +++++++++ res/layout/preference_labeled_slider.xml | 14 +- res/values/attrs.xml | 4 + .../widget/LabeledSeekBarPreference.java | 145 +++++++++++++++++- .../LabeledSeekBarPreferenceTest.java | 122 ++++++++++++++- 5 files changed, 341 insertions(+), 16 deletions(-) create mode 100644 res/layout/icon_discrete_slider.xml diff --git a/res/layout/icon_discrete_slider.xml b/res/layout/icon_discrete_slider.xml new file mode 100644 index 00000000000..b1d960f8dec --- /dev/null +++ b/res/layout/icon_discrete_slider.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + diff --git a/res/layout/preference_labeled_slider.xml b/res/layout/preference_labeled_slider.xml index f120a44adeb..ebe3e4ec3b3 100644 --- a/res/layout/preference_labeled_slider.xml +++ b/res/layout/preference_labeled_slider.xml @@ -46,20 +46,16 @@ android:textAlignment="viewStart" android:textColor="?android:attr/textColorSecondary" /> - + android:layout_height="wrap_content" + android:layout_below="@android:id/summary" /> + + + + diff --git a/src/com/android/settings/widget/LabeledSeekBarPreference.java b/src/com/android/settings/widget/LabeledSeekBarPreference.java index b34ea1946f9..2fce9b5456b 100644 --- a/src/com/android/settings/widget/LabeledSeekBarPreference.java +++ b/src/com/android/settings/widget/LabeledSeekBarPreference.java @@ -21,6 +21,8 @@ import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; @@ -28,17 +30,40 @@ import androidx.annotation.Nullable; import androidx.core.content.res.TypedArrayUtils; import androidx.preference.PreferenceViewHolder; +import com.android.internal.util.Preconditions; import com.android.settings.R; -/** A slider preference with left and right labels **/ +/** + * A labeled {@link SeekBarPreference} with left and right text label, icon label, or both. + * + *

+ * The component provides the attribute usage below. + * + * + * + * + * + * + * + *

+ * + *

If you set the attribute values {@code iconStartContentDescription} or {@code + * iconEndContentDescription} from XML, you must also set the corresponding attributes {@code + * iconStart} or {@code iconEnd}, otherwise throws an {@link IllegalArgumentException}.

+ */ public class LabeledSeekBarPreference extends SeekBarPreference { private final int mTextStartId; private final int mTextEndId; private final int mTickMarkId; + private final int mIconStartId; + private final int mIconEndId; + private final int mIconStartContentDescriptionId; + private final int mIconEndContentDescriptionId; private OnPreferenceChangeListener mStopListener; @Nullable private CharSequence mSummary; + private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener; public LabeledSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { @@ -56,6 +81,25 @@ public class LabeledSeekBarPreference extends SeekBarPreference { R.string.summary_placeholder); mTickMarkId = styledAttrs.getResourceId( R.styleable.LabeledSeekBarPreference_tickMark, /* defValue= */ 0); + mIconStartId = styledAttrs.getResourceId( + R.styleable.LabeledSeekBarPreference_iconStart, /* defValue= */ 0); + mIconEndId = styledAttrs.getResourceId( + R.styleable.LabeledSeekBarPreference_iconEnd, /* defValue= */ 0); + + mIconStartContentDescriptionId = styledAttrs.getResourceId( + R.styleable.LabeledSeekBarPreference_iconStartContentDescription, + /* defValue= */ 0); + Preconditions.checkArgument(!(mIconStartContentDescriptionId != 0 && mIconStartId == 0), + "The resource of the iconStart attribute may be invalid or not set, " + + "you should set the iconStart attribute and have the valid resource."); + + mIconEndContentDescriptionId = styledAttrs.getResourceId( + R.styleable.LabeledSeekBarPreference_iconEndContentDescription, + /* defValue= */ 0); + Preconditions.checkArgument(!(mIconEndContentDescriptionId != 0 && mIconEndId == 0), + "The resource of the iconEnd attribute may be invalid or not set, " + + "you should set the iconEnd attribute and have the valid resource."); + mSummary = styledAttrs.getText(R.styleable.Preference_android_summary); styledAttrs.recycle(); } @@ -75,10 +119,9 @@ public class LabeledSeekBarPreference extends SeekBarPreference { startText.setText(mTextStartId); endText.setText(mTextEndId); + final SeekBar seekBar = (SeekBar) holder.findViewById(com.android.internal.R.id.seekbar); if (mTickMarkId != 0) { final Drawable tickMark = getContext().getDrawable(mTickMarkId); - final SeekBar seekBar = (SeekBar) holder.findViewById( - com.android.internal.R.id.seekbar); seekBar.setTickMark(tickMark); } @@ -90,19 +133,52 @@ public class LabeledSeekBarPreference extends SeekBarPreference { summary.setText(null); summary.setVisibility(View.GONE); } + + final ViewGroup iconStartFrame = (ViewGroup) holder.findViewById(R.id.icon_start_frame); + final ImageView iconStartView = (ImageView) holder.findViewById(R.id.icon_start); + updateIconStartIfNeeded(iconStartFrame, iconStartView, seekBar); + + final ViewGroup iconEndFrame = (ViewGroup) holder.findViewById(R.id.icon_end_frame); + final ImageView iconEndView = (ImageView) holder.findViewById(R.id.icon_end); + updateIconEndIfNeeded(iconEndFrame, iconEndView, seekBar); } public void setOnPreferenceChangeStopListener(OnPreferenceChangeListener listener) { mStopListener = listener; } + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + super.onStartTrackingTouch(seekBar); + + if (mSeekBarChangeListener != null) { + mSeekBarChangeListener.onStartTrackingTouch(seekBar); + } + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + super.onProgressChanged(seekBar, progress, fromUser); + + if (mSeekBarChangeListener != null) { + mSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser); + } + } + @Override public void onStopTrackingTouch(SeekBar seekBar) { super.onStopTrackingTouch(seekBar); + if (mSeekBarChangeListener != null) { + mSeekBarChangeListener.onStopTrackingTouch(seekBar); + } + if (mStopListener != null) { mStopListener.onPreferenceChange(this, seekBar.getProgress()); } + + // Need to update the icon enabled status + notifyChanged(); } @Override @@ -123,5 +199,68 @@ public class LabeledSeekBarPreference extends SeekBarPreference { public CharSequence getSummary() { return mSummary; } + + public void setOnSeekBarChangeListener(SeekBar.OnSeekBarChangeListener seekBarChangeListener) { + mSeekBarChangeListener = seekBarChangeListener; + } + + private void updateIconStartIfNeeded(ViewGroup iconFrame, ImageView iconStart, + SeekBar seekBar) { + if (mIconStartId == 0) { + return; + } + + if (iconStart.getDrawable() == null) { + iconStart.setImageResource(mIconStartId); + } + + if (mIconStartContentDescriptionId != 0) { + final String contentDescription = + iconFrame.getContext().getString(mIconStartContentDescriptionId); + iconFrame.setContentDescription(contentDescription); + } + + iconFrame.setOnClickListener((view) -> { + final int progress = getProgress(); + if (progress > 0) { + setProgress(progress - 1); + } + }); + + iconFrame.setVisibility(View.VISIBLE); + setIconViewAndFrameEnabled(iconStart, seekBar.getProgress() > 0); + } + + private void updateIconEndIfNeeded(ViewGroup iconFrame, ImageView iconEnd, SeekBar seekBar) { + if (mIconEndId == 0) { + return; + } + + if (iconEnd.getDrawable() == null) { + iconEnd.setImageResource(mIconEndId); + } + + if (mIconEndContentDescriptionId != 0) { + final String contentDescription = + iconFrame.getContext().getString(mIconEndContentDescriptionId); + iconFrame.setContentDescription(contentDescription); + } + + iconFrame.setOnClickListener((view) -> { + final int progress = getProgress(); + if (progress < getMax()) { + setProgress(progress + 1); + } + }); + + iconFrame.setVisibility(View.VISIBLE); + setIconViewAndFrameEnabled(iconEnd, seekBar.getProgress() < seekBar.getMax()); + } + + private static void setIconViewAndFrameEnabled(View iconView, boolean enabled) { + iconView.setEnabled(enabled); + final ViewGroup iconFrame = (ViewGroup) iconView.getParent(); + iconFrame.setEnabled(enabled); + } } diff --git a/tests/robotests/src/com/android/settings/gestures/LabeledSeekBarPreferenceTest.java b/tests/robotests/src/com/android/settings/gestures/LabeledSeekBarPreferenceTest.java index 3d83d58141b..058e16b6c36 100644 --- a/tests/robotests/src/com/android/settings/gestures/LabeledSeekBarPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/gestures/LabeledSeekBarPreferenceTest.java @@ -18,12 +18,17 @@ package com.android.settings.gestures; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; @@ -31,42 +36,59 @@ import android.widget.TextView; import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; -import com.android.internal.R; +import com.android.settings.R; +import com.android.settings.testutils.shadow.ShadowUserManager; import com.android.settings.widget.LabeledSeekBarPreference; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +/** + * Tests for {@link LabeledSeekBarPreference}. + */ @RunWith(RobolectricTestRunner.class) +@Config(shadows = { + ShadowUserManager.class +}) public class LabeledSeekBarPreferenceTest { private Context mContext; private PreferenceViewHolder mViewHolder; private SeekBar mSeekBar; private TextView mSummary; + private ViewGroup mIconStartFrame; + private ViewGroup mIconEndFrame; private LabeledSeekBarPreference mSeekBarPreference; @Mock private Preference.OnPreferenceChangeListener mListener; + @Mock + private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener; + @Before public void setUp() { MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; + mContext = Mockito.spy(RuntimeEnvironment.application); mSeekBarPreference = new LabeledSeekBarPreference(mContext, null); LayoutInflater inflater = LayoutInflater.from(mContext); final View view = inflater.inflate(mSeekBarPreference.getLayoutResource(), new LinearLayout(mContext), false); mViewHolder = PreferenceViewHolder.createInstanceForTests(view); - mSeekBar = (SeekBar) mViewHolder.findViewById(R.id.seekbar); - mSummary = (TextView) mViewHolder.findViewById(R.id.summary); + mSeekBar = (SeekBar) mViewHolder.findViewById(com.android.internal.R.id.seekbar); + mSummary = (TextView) mViewHolder.findViewById(android.R.id.summary); + mIconStartFrame = (ViewGroup) mViewHolder.findViewById(R.id.icon_start_frame); + mIconEndFrame = (ViewGroup) mViewHolder.findViewById(R.id.icon_end_frame); } @Test @@ -97,4 +119,96 @@ public class LabeledSeekBarPreferenceTest { assertThat(mSummary.getText()).isEqualTo(""); assertThat(mSummary.getVisibility()).isEqualTo(View.GONE); } + + @Test + public void setIconAttributes_iconVisible() { + final AttributeSet attributeSet = Robolectric.buildAttributeSet() + .addAttribute(R.attr.iconStart, "@drawable/ic_remove_24dp") + .addAttribute(R.attr.iconEnd, "@drawable/ic_add_24dp") + .build(); + final LabeledSeekBarPreference seekBarPreference = + new LabeledSeekBarPreference(mContext, attributeSet); + + seekBarPreference.onBindViewHolder(mViewHolder); + + assertThat(mIconStartFrame.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mIconEndFrame.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void notSetIconAttributes_iconGone() { + final AttributeSet attributeSet = Robolectric.buildAttributeSet() + .build(); + final LabeledSeekBarPreference seekBarPreference = + new LabeledSeekBarPreference(mContext, attributeSet); + + seekBarPreference.onBindViewHolder(mViewHolder); + + assertThat(mIconStartFrame.getVisibility()).isEqualTo(View.GONE); + assertThat(mIconEndFrame.getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void setSeekBarListener_success() { + mSeekBarPreference.setOnSeekBarChangeListener(mSeekBarChangeListener); + mSeekBarPreference.onStartTrackingTouch(mSeekBar); + mSeekBarPreference.onProgressChanged(mSeekBar, /* progress= */ 0, + /* fromUser= */ false); + mSeekBarPreference.onStopTrackingTouch(mSeekBar); + + verify(mSeekBarChangeListener).onStartTrackingTouch(any(SeekBar.class)); + verify(mSeekBarChangeListener).onProgressChanged(any(SeekBar.class), anyInt(), + anyBoolean()); + verify(mSeekBarChangeListener).onStopTrackingTouch(any(SeekBar.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void setContentDescriptionWithoutIcon_throwException() { + final AttributeSet attributeSet = Robolectric.buildAttributeSet() + .addAttribute(R.attr.iconStartContentDescription, + "@string/screen_zoom_make_smaller_desc") + .addAttribute(R.attr.iconEndContentDescription, + "@string/screen_zoom_make_larger_desc") + .build(); + + new LabeledSeekBarPreference(mContext, attributeSet); + } + + @Test + public void setContentDescriptionWithIcon_success() { + final String startDescription = + mContext.getResources().getString(R.string.screen_zoom_make_smaller_desc); + final String endDescription = + mContext.getResources().getString(R.string.screen_zoom_make_larger_desc); + final AttributeSet attributeSet = Robolectric.buildAttributeSet() + .addAttribute(R.attr.iconStart, "@drawable/ic_remove_24dp") + .addAttribute(R.attr.iconEnd, "@drawable/ic_add_24dp") + .addAttribute(R.attr.iconStartContentDescription, + "@string/screen_zoom_make_smaller_desc") + .addAttribute(R.attr.iconEndContentDescription, + "@string/screen_zoom_make_larger_desc") + .build(); + final LabeledSeekBarPreference seekBarPreference = + new LabeledSeekBarPreference(mContext, attributeSet); + + seekBarPreference.onBindViewHolder(mViewHolder); + + assertThat(mIconStartFrame.getContentDescription().toString().contentEquals( + startDescription)).isTrue(); + assertThat(mIconEndFrame.getContentDescription().toString().contentEquals( + endDescription)).isTrue(); + } + + @Test + public void notSetContentDescriptionAttributes_noDescription() { + final AttributeSet attributeSet = Robolectric.buildAttributeSet() + .build(); + final LabeledSeekBarPreference seekBarPreference = + new LabeledSeekBarPreference(mContext, attributeSet); + + seekBarPreference.onBindViewHolder(mViewHolder); + + assertThat(mIconStartFrame.getContentDescription()).isNull(); + assertThat(mIconEndFrame.getContentDescription()).isNull(); + } } From 88d63ea3aff05cfc4798b35b926e4bd10fa04069 Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Thu, 20 Jan 2022 01:32:58 +0800 Subject: [PATCH 2/2] =?UTF-8?q?New=20feature=20=E2=80=9CText=20and=20readi?= =?UTF-8?q?ng=20options=E2=80=9D=20for=20SetupWizard,=20Wallpaper,=20and?= =?UTF-8?q?=20Settings=20(9/n).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fine-tune the legacy interface of the LabeledSeekBarPreference 1) If not set any label, the layout should be gone instead of visible. If not gone, the preference will have redundant space. Bug: 211503117 Test: make -j64 RunSettingsRoboTests ROBOTEST_FILTER=LabeledSeekBarPreferenceTest Change-Id: Ideea8589e35083eb89495c0eceba38c62b4807a7 --- res/layout/preference_labeled_slider.xml | 4 ++- .../widget/LabeledSeekBarPreference.java | 19 +++++++++---- .../LabeledSeekBarPreferenceTest.java | 28 +++++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/res/layout/preference_labeled_slider.xml b/res/layout/preference_labeled_slider.xml index ebe3e4ec3b3..294b9794c0c 100644 --- a/res/layout/preference_labeled_slider.xml +++ b/res/layout/preference_labeled_slider.xml @@ -53,10 +53,12 @@ android:layout_below="@android:id/summary" /> + android:orientation="horizontal" + android:visibility="gone"> 0) { + startText.setText(mTextStartId); + } + final TextView endText = (TextView) holder.findViewById(android.R.id.text2); - startText.setText(mTextStartId); - endText.setText(mTextEndId); + if (mTextEndId > 0) { + endText.setText(mTextEndId); + } + + final View labelFrame = holder.findViewById(R.id.label_frame); + final boolean isValidTextResIdExist = mTextStartId > 0 || mTextEndId > 0; + labelFrame.setVisibility(isValidTextResIdExist ? View.VISIBLE : View.GONE); final SeekBar seekBar = (SeekBar) holder.findViewById(com.android.internal.R.id.seekbar); if (mTickMarkId != 0) { diff --git a/tests/robotests/src/com/android/settings/gestures/LabeledSeekBarPreferenceTest.java b/tests/robotests/src/com/android/settings/gestures/LabeledSeekBarPreferenceTest.java index 058e16b6c36..d00e905e816 100644 --- a/tests/robotests/src/com/android/settings/gestures/LabeledSeekBarPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/gestures/LabeledSeekBarPreferenceTest.java @@ -66,6 +66,7 @@ public class LabeledSeekBarPreferenceTest { private TextView mSummary; private ViewGroup mIconStartFrame; private ViewGroup mIconEndFrame; + private View mLabelFrame; private LabeledSeekBarPreference mSeekBarPreference; @Mock @@ -89,6 +90,7 @@ public class LabeledSeekBarPreferenceTest { mSummary = (TextView) mViewHolder.findViewById(android.R.id.summary); mIconStartFrame = (ViewGroup) mViewHolder.findViewById(R.id.icon_start_frame); mIconEndFrame = (ViewGroup) mViewHolder.findViewById(R.id.icon_end_frame); + mLabelFrame = mViewHolder.findViewById(R.id.label_frame); } @Test @@ -120,6 +122,32 @@ public class LabeledSeekBarPreferenceTest { assertThat(mSummary.getVisibility()).isEqualTo(View.GONE); } + @Test + public void setTextAttributes_textStart_textEnd_labelFrameVisible() { + final AttributeSet attributeSet = Robolectric.buildAttributeSet() + .addAttribute(R.attr.textStart, "@string/screen_zoom_make_smaller_desc") + .addAttribute(R.attr.textEnd, "@string/screen_zoom_make_larger_desc") + .build(); + final LabeledSeekBarPreference seekBarPreference = + new LabeledSeekBarPreference(mContext, attributeSet); + + seekBarPreference.onBindViewHolder(mViewHolder); + + assertThat(mLabelFrame.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void notSetTextAttributes_labelFrameGone() { + final AttributeSet attributeSet = Robolectric.buildAttributeSet() + .build(); + final LabeledSeekBarPreference seekBarPreference = + new LabeledSeekBarPreference(mContext, attributeSet); + + seekBarPreference.onBindViewHolder(mViewHolder); + + assertThat(mLabelFrame.getVisibility()).isEqualTo(View.GONE); + } + @Test public void setIconAttributes_iconVisible() { final AttributeSet attributeSet = Robolectric.buildAttributeSet()