Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 896e1801 authored by Peter_Liang's avatar Peter_Liang
Browse files

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
parent 6a893a54
Loading
Loading
Loading
Loading
+5 −5
Original line number Diff line number Diff line
@@ -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:orientation="vertical"
        android:importantForAccessibility="noHideDescendants"/>

</FrameLayout>
+2 −2
Original line number Diff line number Diff line
@@ -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>
+121 −39
Original line number Diff line number Diff line
@@ -16,23 +16,58 @@

package com.android.settings.accessibility;

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.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ListView;
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;

import com.google.common.primitives.Floats;
import com.google.common.primitives.Ints;

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 class PaletteListPreference extends Preference {
public final class PaletteListPreference extends Preference {

    private ListView mListView;
    private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
    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 initPreDrawListener() {
        mPreDrawListener = new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                if (mListView == null) {
                    return false;
    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);
        }
    }

                final int listViewHeight = mListView.getMeasuredHeight();
                final int listViewWidth = mListView.getMeasuredWidth();
    private GradientDrawable createGradientDrawable(ViewGroup rootView, @ColorInt int color) {
        mGradientColors.set(Position.END, color);

                // Removes the callback after get result of measure view.
                final ViewTreeObserver viewTreeObserver = mListView.getViewTreeObserver();
                if (viewTreeObserver.isAlive()) {
                    viewTreeObserver.removeOnPreDrawListener(this);
        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));

        return gradientDrawable;
    }
                mPreDrawListener = null;

                // 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);
    }
}
+0 −301
Original line number Diff line number Diff line
/*
 * 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;
        }
    }
}
+64 −0
Original line number Diff line number Diff line
@@ -16,50 +16,49 @@

package com.android.settings.accessibility;

import static com.google.common.truth.Truth.assertThat;
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 PaletteListView} */
/** Tests for {@link PaletteListPreference}. */
@RunWith(RobolectricTestRunner.class)
public class PaletteListViewTest {
public final class PaletteListPreferenceTest {

    private PaletteListPreference mPaletteListPreference;
    private PreferenceViewHolder mPreferenceViewHolder;
    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"};
    public void initObjects() {
        mPaletteListPreference = new PaletteListPreference(mContext, null);

        assertThat(mPaletteListView.setPaletteListColors(colorName, colorCode)).isTrue();
        final LayoutInflater inflater = LayoutInflater.from(mContext);
        final View view =
                inflater.inflate(R.layout.daltonizer_preview, null);
        mPreferenceViewHolder = PreferenceViewHolder.createInstanceForTests(view);
    }

    @Test
    public void setColors_configureFailed() {
        final String[] colorName = null;
        final String[] colorCode = null;
    public void initPaletteView_success() {
        mPaletteListPreference.onBindViewHolder(mPreferenceViewHolder);

        assertThat(mPaletteListView.setPaletteListColors(colorName, colorCode)).isFalse();
        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());
    }
}
Loading