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..294b9794c0c 100644 --- a/res/layout/preference_labeled_slider.xml +++ b/res/layout/preference_labeled_slider.xml @@ -46,21 +46,19 @@ android:textAlignment="viewStart" android:textColor="?android:attr/textColorSecondary" /> - - - + 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..3e095239d18 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) { @@ -49,13 +74,30 @@ public class LabeledSeekBarPreference extends SeekBarPreference { final TypedArray styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.LabeledSeekBarPreference); mTextStartId = styledAttrs.getResourceId( - R.styleable.LabeledSeekBarPreference_textStart, - R.string.summary_placeholder); + R.styleable.LabeledSeekBarPreference_textStart, /* defValue= */ 0); mTextEndId = styledAttrs.getResourceId( - R.styleable.LabeledSeekBarPreference_textEnd, - R.string.summary_placeholder); + R.styleable.LabeledSeekBarPreference_textEnd, /* defValue= */ 0); 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(); } @@ -71,14 +113,22 @@ public class LabeledSeekBarPreference extends SeekBarPreference { super.onBindViewHolder(holder); final TextView startText = (TextView) holder.findViewById(android.R.id.text1); - final TextView endText = (TextView) holder.findViewById(android.R.id.text2); - startText.setText(mTextStartId); - endText.setText(mTextEndId); + if (mTextStartId > 0) { + startText.setText(mTextStartId); + } + final TextView endText = (TextView) holder.findViewById(android.R.id.text2); + 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) { final Drawable tickMark = getContext().getDrawable(mTickMarkId); - final SeekBar seekBar = (SeekBar) holder.findViewById( - com.android.internal.R.id.seekbar); seekBar.setTickMark(tickMark); } @@ -90,19 +140,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 +206,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..d00e905e816 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,61 @@ 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 View mLabelFrame; 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); + mLabelFrame = mViewHolder.findViewById(R.id.label_frame); } @Test @@ -97,4 +121,122 @@ public class LabeledSeekBarPreferenceTest { assertThat(mSummary.getText()).isEqualTo(""); 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() + .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(); + } }