New feature “Text and reading options” for SetupWizard, Wallpaper, and Settings (8/n).

- 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
This commit is contained in:
Peter_Liang
2022-01-19 21:41:17 +08:00
parent 53efa5be66
commit cc5808cbd7
5 changed files with 341 additions and 16 deletions

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2022 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/seekbar_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:gravity="center_vertical">
<FrameLayout
android:id="@+id/icon_start_frame"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:focusable="true"
android:visibility="gone">
<ImageView
android:id="@+id/icon_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:adjustViewBounds="true"
android:focusable="false"
android:tint="?android:attr/textColorPrimary"
android:tintMode="src_in" />
</FrameLayout>
<SeekBar
android:id="@*android:id/seekbar"
style="@android:style/Widget.Material.SeekBar.Discrete"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:paddingEnd="12dp"
android:paddingStart="0dp" />
<FrameLayout
android:id="@+id/icon_end_frame"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:focusable="true"
android:visibility="gone">
<ImageView
android:id="@+id/icon_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:adjustViewBounds="true"
android:focusable="false"
android:tint="?android:attr/textColorPrimary"
android:tintMode="src_in" />
</FrameLayout>
</LinearLayout>

View File

@@ -46,20 +46,16 @@
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorSecondary" />
<SeekBar
android:id="@*android:id/seekbar"
android:layout_below="@android:id/summary"
android:layout_gravity="center_vertical"
<include
layout="@layout/icon_discrete_slider"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingStart="0dp"
android:paddingEnd="12dp"
style="@android:style/Widget.Material.SeekBar.Discrete" />
android:layout_height="wrap_content"
android:layout_below="@android:id/summary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@*android:id/seekbar"
android:layout_below="@id/seekbar_frame"
android:orientation="horizontal">
<TextView

View File

@@ -169,6 +169,10 @@
<attr name="textStart" format="reference" />
<attr name="textEnd" format="reference" />
<attr name="tickMark" format="reference" />
<attr name="iconStart" format="reference" />
<attr name="iconEnd" format="reference" />
<attr name="iconStartContentDescription" format="reference" />
<attr name="iconEndContentDescription" format="reference" />
</declare-styleable>
<declare-styleable name="TintDrawable">

View File

@@ -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.
*
* <p>
* The component provides the attribute usage below.
* <attr name="textStart" format="reference" />
* <attr name="textEnd" format="reference" />
* <attr name="tickMark" format="reference" />
* <attr name="iconStart" format="reference" />
* <attr name="iconEnd" format="reference" />
* <attr name="iconStartContentDescription" format="reference" />
* <attr name="iconEndContentDescription" format="reference" />
* </p>
*
* <p> 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}.</p>
*/
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);
}
}

View File

@@ -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();
}
}