Fix the screen will move by itself when enabled Color correction.

Root Cause:
Using the listener to update the UI dynamically might have some time delay.

Solution:
Refactor to another implementation and avoid using the listener to update it.

Additional condition:
Add height restriction in preference to avoid the palette view to cover whole screen.

Bug: 148785841
Test: make RunSettingsRoboTests ROBOTEST_FILTER=PaletteListPreferenceTest
Change-Id: I6a854e16321b3426e2f8ff65c6404036d55caed4
This commit is contained in:
Peter_Liang
2020-08-14 16:27:31 +08:00
parent 6a893a54b6
commit 896e18012d
6 changed files with 192 additions and 412 deletions

View File

@@ -17,13 +17,13 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/preview_viewport"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.android.settings.accessibility.PaletteListView
android:id="@+id/palette_listView"
<LinearLayout
android:id="@+id/palette_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAccessibility="noHideDescendants" />
android:orientation="vertical"
android:importantForAccessibility="noHideDescendants"/>
</FrameLayout>

View File

@@ -1468,7 +1468,7 @@
</string-array>
<!-- Array of titles palette list for accessibility. -->
<string-array name="setting_palette_colors" translatable="false" >
<string-array name="setting_palette_data" translatable="false" >
<item>@string/color_red</item>
<item>@string/color_orange</item>
<item>@string/color_yellow</item>
@@ -1479,7 +1479,7 @@
</string-array>
<!-- Values for palette list view preference. -->
<array name="setting_palette_data" translatable="false" >
<array name="setting_palette_colors" translatable="false" >
<item>@color/palette_list_color_red</item>
<item>@color/palette_list_color_orange</item>
<item>@color/palette_list_color_yellow</item>

View File

@@ -16,23 +16,58 @@
package com.android.settings.accessibility;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ListView;
import static android.graphics.drawable.GradientDrawable.Orientation;
import static com.android.settings.accessibility.AccessibilityUtil.getScreenHeightPixels;
import static com.android.settings.accessibility.AccessibilityUtil.getScreenWidthPixels;
import static com.google.common.primitives.Ints.max;
import android.content.Context;
import android.graphics.Paint.FontMetrics;
import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
/** Preference that easier preview by matching name to color. */
public class PaletteListPreference extends Preference {
import com.google.common.primitives.Floats;
import com.google.common.primitives.Ints;
private ListView mListView;
private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/** Preference that easier preview by matching name to color. */
public final class PaletteListPreference extends Preference {
private final List<Integer> mGradientColors = new ArrayList<>();
private final List<Float> mGradientOffsets = new ArrayList<>();
@IntDef({
Position.START,
Position.CENTER,
Position.END,
})
@Retention(RetentionPolicy.SOURCE)
@interface Position {
int START = 0;
int CENTER = 1;
int END = 2;
}
/**
* Constructs a new PaletteListPreference with the given context's theme and the supplied
@@ -61,47 +96,94 @@ public class PaletteListPreference extends Preference {
public PaletteListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setLayoutResource(R.layout.daltonizer_preview);
initPreDrawListener();
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final View rootView = holder.itemView;
mListView = rootView.findViewById(R.id.palette_listView);
if (mPreDrawListener != null) {
mListView.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
final ViewGroup paletteView = holder.itemView.findViewById(R.id.palette_view);
initPaletteAttributes(getContext());
initPaletteView(getContext(), paletteView);
}
private void initPaletteAttributes(Context context) {
final int defaultColor = context.getColor(R.color.palette_list_gradient_background);
mGradientColors.add(Position.START, defaultColor);
mGradientColors.add(Position.CENTER, defaultColor);
mGradientColors.add(Position.END, defaultColor);
mGradientOffsets.add(Position.START, /* element= */ 0.0f);
mGradientOffsets.add(Position.CENTER, /* element= */ 0.5f);
mGradientOffsets.add(Position.END, /* element= */ 1.0f);
}
private void initPaletteView(Context context, ViewGroup rootView) {
if (rootView.getChildCount() > 0) {
rootView.removeAllViews();
}
final List<Integer> paletteColors = getPaletteColors(context);
final List<String> paletteData = getPaletteData(context);
final float textPadding =
context.getResources().getDimension(R.dimen.accessibility_layout_margin_start_end);
final String maxLengthData =
Collections.max(paletteData, Comparator.comparing(String::length));
final int textWidth = getTextWidth(context, maxLengthData);
final float textBound = (textWidth + textPadding) / getScreenWidthPixels(context);
mGradientOffsets.set(Position.CENTER, textBound);
final int screenHalfHeight = getScreenHeightPixels(context) / 2;
final int paletteItemHeight =
max(screenHalfHeight / paletteData.size(), getTextLineHeight(context));
for (int i = 0; i < paletteData.size(); ++i) {
final TextView textView = new TextView(context);
textView.setText(paletteData.get(i));
textView.setHeight(paletteItemHeight);
textView.setPaddingRelative(Math.round(textPadding), 0, 0, 0);
textView.setGravity(Gravity.CENTER_VERTICAL);
textView.setBackground(createGradientDrawable(rootView, paletteColors.get(i)));
rootView.addView(textView);
}
}
private void initPreDrawListener() {
mPreDrawListener = new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (mListView == null) {
return false;
}
private GradientDrawable createGradientDrawable(ViewGroup rootView, @ColorInt int color) {
mGradientColors.set(Position.END, color);
final int listViewHeight = mListView.getMeasuredHeight();
final int listViewWidth = mListView.getMeasuredWidth();
final GradientDrawable gradientDrawable = new GradientDrawable();
final Orientation orientation =
rootView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
? Orientation.RIGHT_LEFT
: Orientation.LEFT_RIGHT;
gradientDrawable.setOrientation(orientation);
gradientDrawable.setColors(Ints.toArray(mGradientColors), Floats.toArray(mGradientOffsets));
// Removes the callback after get result of measure view.
final ViewTreeObserver viewTreeObserver = mListView.getViewTreeObserver();
if (viewTreeObserver.isAlive()) {
viewTreeObserver.removeOnPreDrawListener(this);
}
mPreDrawListener = null;
return gradientDrawable;
}
// Resets layout parameters to display whole items from listView.
final FrameLayout.LayoutParams layoutParams =
(FrameLayout.LayoutParams) mListView.getLayoutParams();
layoutParams.height = listViewHeight * mListView.getAdapter().getCount();
layoutParams.width = listViewWidth;
mListView.setLayoutParams(layoutParams);
private List<Integer> getPaletteColors(Context context) {
final int[] paletteResources =
context.getResources().getIntArray(R.array.setting_palette_colors);
return Arrays.stream(paletteResources).boxed().collect(Collectors.toList());
}
return true;
}
};
private List<String> getPaletteData(Context context) {
final String[] paletteResources =
context.getResources().getStringArray(R.array.setting_palette_data);
return Arrays.asList(paletteResources);
}
private int getTextWidth(Context context, String text) {
final TextView tempView = new TextView(context);
return Math.round(tempView.getPaint().measureText(text));
}
private int getTextLineHeight(Context context) {
final TextView tempView = new TextView(context);
final FontMetrics fontMetrics = tempView.getPaint().getFontMetrics();
return Math.round(fontMetrics.bottom - fontMetrics.top);
}
}

View File

@@ -1,301 +0,0 @@
/*
* Copyright (C) 2020 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.settings.accessibility;
import android.annotation.NonNull;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.GradientDrawable.Orientation;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Custom ListView {@link ListView} which displays palette to deploy the color code preview.
*
* <p>The preview shows gradient from color white to specific color code on each list view item, in
* addition, text view adjusts the attribute of width for adapting the text length.
*
* <p>The text cannot fills the whole view for ensuring the gradient color preview can purely
* display also the view background shows the color beside the text variable end point.
*/
public class PaletteListView extends ListView {
private final Context mContext;
private final DisplayAdapter mDisplayAdapter;
private final LayoutInflater mLayoutInflater;
private final String mDefaultGradientColorCodeString;
private final int mDefaultGradientColor;
private float mTextBound;
private static final float LANDSCAPE_MAX_WIDTH_PERCENTAGE = 100f;
public PaletteListView(Context context) {
this(context, null);
}
public PaletteListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PaletteListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
mDisplayAdapter = new DisplayAdapter();
mLayoutInflater = LayoutInflater.from(context);
mDefaultGradientColorCodeString =
getResources().getString(R.color.palette_list_gradient_background);
mDefaultGradientColor =
getResources().getColor(R.color.palette_list_gradient_background, null);
mTextBound = 0.0f;
init();
}
private static int getScreenWidth(WindowManager windowManager) {
final Display display = windowManager.getDefaultDisplay();
final DisplayMetrics displayMetrics = new DisplayMetrics();
display.getMetrics(displayMetrics);
return displayMetrics.widthPixels;
}
private void init() {
final TypedArray colorNameArray = getResources().obtainTypedArray(
R.array.setting_palette_colors);
final TypedArray colorCodeArray = getResources().obtainTypedArray(
R.array.setting_palette_data);
final int colorNameArrayLength = colorNameArray.length();
final List<ColorAttributes> colorList = new ArrayList<>();
computeTextWidthBounds(colorNameArray);
for (int index = 0; index < colorNameArrayLength; index++) {
colorList.add(
new ColorAttributes(
/* colorName= */ colorNameArray.getString(index),
/* colorCode= */ colorCodeArray.getColor(index, mDefaultGradientColor),
/* textBound= */ mTextBound,
/* gradientDrawable= */
new GradientDrawable(Orientation.LEFT_RIGHT, null)));
}
mDisplayAdapter.setColorList(colorList);
setAdapter(mDisplayAdapter);
setDividerHeight(/* height= */ 0);
}
/**
* Sets string array that required the color name and color code for deploy the new color
* preview.
*
* <p>The parameters not allow null define but two array length inconsistent are acceptable, in
* addition, to prevent IndexOutOfBoundsException the algorithm will check array data, and base
* on the array size to display data, or fills color code array if length less than other.
*
* @param colorNames a string array of color name
* @param colorCodes a string array of color code
* @return true if new array data apply successful
*/
@VisibleForTesting
boolean setPaletteListColors(@NonNull String[] colorNames, @NonNull String[] colorCodes) {
if (colorNames == null || colorCodes == null) {
return false;
}
final int colorNameArrayLength = colorNames.length;
final int colorCodeArrayLength = colorCodes.length;
final List<ColorAttributes> colorList = new ArrayList<>();
final String[] colorCodeArray = fillColorCodeArray(colorCodes, colorNameArrayLength,
colorCodeArrayLength);
computeTextWidthBounds(colorNames);
for (int index = 0; index < colorNameArrayLength; index++) {
colorList.add(
new ColorAttributes(
/* colorName= */ colorNames[index],
/* colorCode= */ Color.parseColor(colorCodeArray[index]),
/* textBound= */ mTextBound,
/* gradientDrawable= */
new GradientDrawable(Orientation.LEFT_RIGHT, null)));
}
mDisplayAdapter.setColorList(colorList);
mDisplayAdapter.notifyDataSetChanged();
return true;
}
private String[] fillColorCodeArray(String[] colorCodes, int colorNameArrayLength,
int colorCodeArrayLength) {
if (colorNameArrayLength == colorCodeArrayLength
|| colorNameArrayLength < colorCodeArrayLength) {
return colorCodes;
}
final String[] colorCodeArray = new String[colorNameArrayLength];
for (int index = 0; index < colorNameArrayLength; index++) {
if (index < colorCodeArrayLength) {
colorCodeArray[index] = colorCodes[index];
} else {
colorCodeArray[index] = mDefaultGradientColorCodeString;
}
}
return colorCodeArray;
}
private void computeTextWidthBounds(TypedArray colorNameTypedArray) {
final int colorNameArrayLength = colorNameTypedArray.length();
final String[] colorNames = new String[colorNameArrayLength];
for (int index = 0; index < colorNameArrayLength; index++) {
colorNames[index] = colorNameTypedArray.getString(index);
}
measureBound(colorNames);
}
private void computeTextWidthBounds(String[] colorNameArray) {
final int colorNameArrayLength = colorNameArray.length;
final String[] colorNames = new String[colorNameArrayLength];
for (int index = 0; index < colorNameArrayLength; index++) {
colorNames[index] = colorNameArray[index];
}
measureBound(colorNames);
}
private void measureBound(String[] dataArray) {
final WindowManager windowManager = (WindowManager) mContext.getSystemService(
Context.WINDOW_SERVICE);
final View view = mLayoutInflater.inflate(R.layout.palette_listview_item, null);
final TextView textView = view.findViewById(R.id.item_textview);
final List<String> colorNameList = new ArrayList<>(Arrays.asList(dataArray));
Collections.sort(colorNameList, Comparator.comparing(String::length));
// Gets the last index of list which sort by text length.
textView.setText(Iterables.getLast(colorNameList));
final float textWidth = textView.getPaint().measureText(textView.getText().toString());
// Computes rate of text width compare to screen width, and measures the round the double
// to two decimal places manually.
final float textBound = Math.round(
textWidth / getScreenWidth(windowManager) * LANDSCAPE_MAX_WIDTH_PERCENTAGE)
/ LANDSCAPE_MAX_WIDTH_PERCENTAGE;
// Left padding and right padding with color preview.
final float paddingPixel = getResources().getDimension(
R.dimen.accessibility_layout_margin_start_end);
final float paddingWidth =
Math.round(paddingPixel / getScreenWidth(windowManager)
* LANDSCAPE_MAX_WIDTH_PERCENTAGE) / LANDSCAPE_MAX_WIDTH_PERCENTAGE;
mTextBound = textBound + paddingWidth + paddingWidth;
}
private static class ViewHolder {
public TextView textView;
}
/** An adapter that converts color text title and color code to text views. */
private final class DisplayAdapter extends BaseAdapter {
private List<ColorAttributes> mColorList;
@Override
public int getCount() {
return mColorList.size();
}
@Override
public Object getItem(int position) {
return mColorList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder viewHolder;
final ColorAttributes paletteAttribute = mColorList.get(position);
final String colorName = paletteAttribute.getColorName();
final GradientDrawable gradientDrawable = paletteAttribute.getGradientDrawable();
if (convertView == null) {
convertView = mLayoutInflater.inflate(R.layout.palette_listview_item, null);
viewHolder = new ViewHolder();
viewHolder.textView = convertView.findViewById(R.id.item_textview);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.textView.setText(colorName);
viewHolder.textView.setBackground(gradientDrawable);
return convertView;
}
protected void setColorList(List<ColorAttributes> colorList) {
mColorList = colorList;
}
}
private final class ColorAttributes {
private final int mColorIndex = 2; // index for inject color.
private final int mColorOffsetIndex = 1; // index for offset effect.
private final String mColorName;
private final GradientDrawable mGradientDrawable;
private final int[] mGradientColors =
{/* startColor=*/ mDefaultGradientColor, /* centerColor=*/ mDefaultGradientColor,
/* endCode= */ 0};
private final float[] mGradientOffsets =
{/* starOffset= */ 0.0f, /* centerOffset= */ 0.5f, /* endOffset= */ 1.0f};
ColorAttributes(
String colorName, int colorCode, float textBound,
GradientDrawable gradientDrawable) {
mGradientColors[mColorIndex] = colorCode;
mGradientOffsets[mColorOffsetIndex] = textBound;
gradientDrawable.setColors(mGradientColors, mGradientOffsets);
mColorName = colorName;
mGradientDrawable = gradientDrawable;
}
public String getColorName() {
return mColorName;
}
public GradientDrawable getGradientDrawable() {
return mGradientDrawable;
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2020 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.settings.accessibility;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.preference.PreferenceViewHolder;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link PaletteListPreference}. */
@RunWith(RobolectricTestRunner.class)
public final class PaletteListPreferenceTest {
private PaletteListPreference mPaletteListPreference;
private PreferenceViewHolder mPreferenceViewHolder;
private final Context mContext = ApplicationProvider.getApplicationContext();
@Before
public void initObjects() {
mPaletteListPreference = new PaletteListPreference(mContext, null);
final LayoutInflater inflater = LayoutInflater.from(mContext);
final View view =
inflater.inflate(R.layout.daltonizer_preview, null);
mPreferenceViewHolder = PreferenceViewHolder.createInstanceForTests(view);
}
@Test
public void initPaletteView_success() {
mPaletteListPreference.onBindViewHolder(mPreferenceViewHolder);
final ViewGroup viewGroup =
mPreferenceViewHolder.itemView.findViewById(R.id.palette_view);
final int expectedCount =
mContext.getResources().getStringArray(R.array.setting_palette_data).length;
assertEquals(expectedCount, viewGroup.getChildCount());
}
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright (C) 2020 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.settings.accessibility;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link PaletteListView} */
@RunWith(RobolectricTestRunner.class)
public class PaletteListViewTest {
private final Context mContext = ApplicationProvider.getApplicationContext();
private PaletteListView mPaletteListView;
@Before
public void setUp() {
mPaletteListView = new PaletteListView(mContext);
}
@Test
public void setColors_applySameLengthArray_configureSuccessful() {
final String[] colorName = {"White", "Black", "Yellow"};
final String[] colorCode = {"#ffffff", "#000000", "#f9ab00"};
assertThat(mPaletteListView.setPaletteListColors(colorName, colorCode)).isTrue();
}
@Test
public void setColors_applyDifferentLengthArray_configureSuccessful() {
final String[] colorName = {"White", "Black", "Yellow", "Orange", "Red"};
final String[] colorCode = {"#ffffff", "#000000", "#f9ab00"};
assertThat(mPaletteListView.setPaletteListColors(colorName, colorCode)).isTrue();
}
@Test
public void setColors_configureFailed() {
final String[] colorName = null;
final String[] colorCode = null;
assertThat(mPaletteListView.setPaletteListColors(colorName, colorCode)).isFalse();
}
}