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

Commit 329131e7 authored by Ivan Chiang's avatar Ivan Chiang
Browse files

Fix A11y issue of chips and support reorder animation in RTL

- When we sort the chips, we removed all views of chipGroup
  before. It will lose the A11y focus. Keep the clicked chip.
- Support reorder animation in RTL
- Use LinearLayout to instead of ChipGroup.
  If we set paddingEnd in ChipGroup, the layout is wrong in RTL mode.

Test: manual
Change-Id: Ib7604275c809166e196818045c402e836a4298e2
Fix: 122492579
Fix: 122711575
parent 8ca87c66
Loading
Loading
Loading
Loading
+1 −0
Original line number Original line Diff line number Diff line
@@ -20,6 +20,7 @@
    android:checkable="true"
    android:checkable="true"
    android:layout_width="wrap_content"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="@dimen/search_chip_spacing"
    android:textColor="@color/search_chip_text_color"
    android:textColor="@color/search_chip_text_color"
    app:checkedIcon="@drawable/ic_check"
    app:checkedIcon="@drawable/ic_check"
    app:chipBackgroundColor="@color/search_chip_background_color"
    app:chipBackgroundColor="@color/search_chip_background_color"
+3 −7
Original line number Original line Diff line number Diff line
@@ -16,19 +16,15 @@


<HorizontalScrollView
<HorizontalScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_height="wrap_content"
    android:overScrollMode="never"
    android:scrollbars="none">
    android:scrollbars="none">
    <com.google.android.material.chip.ChipGroup
    <LinearLayout
        android:id="@+id/search_chip_group"
        android:id="@+id/search_chip_group"
        android:layout_width="match_parent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/search_chip_group_margin"
        android:layout_marginTop="@dimen/search_chip_group_margin"
        android:layout_marginBottom="@dimen/search_chip_group_margin"
        android:layout_marginBottom="@dimen/search_chip_group_margin"
        android:paddingStart="@dimen/search_chip_spacing"
        android:paddingEnd="@dimen/search_chip_spacing"/>
        android:paddingEnd="@dimen/search_chip_spacing"
        android:gravity="center"
        app:chipSpacing="@dimen/search_chip_spacing"
        app:singleLine="true"/>
</HorizontalScrollView>
</HorizontalScrollView>
 No newline at end of file
+2 −2
Original line number Original line Diff line number Diff line
@@ -34,6 +34,7 @@ import android.view.KeyEvent;
import android.view.Menu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem;
import android.view.View;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.TextView;


import androidx.annotation.CallSuper;
import androidx.annotation.CallSuper;
@@ -68,7 +69,6 @@ import com.android.documentsui.sorting.SortController;
import com.android.documentsui.sorting.SortModel;
import com.android.documentsui.sorting.SortModel;


import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.chip.ChipGroup;


import java.util.ArrayList;
import java.util.ArrayList;
import java.util.Date;
import java.util.Date;
@@ -191,7 +191,7 @@ public abstract class BaseActivity
                        mInjector.debugHelper::toggleDebugMode,
                        mInjector.debugHelper::toggleDebugMode,
                        cmdInterceptor);
                        cmdInterceptor);


        ChipGroup chipGroup = findViewById(R.id.search_chip_group);
        ViewGroup chipGroup = findViewById(R.id.search_chip_group);
        mSearchManager = new SearchViewManager(searchListener, queryInterceptor,
        mSearchManager = new SearchViewManager(searchListener, queryInterceptor,
                chipGroup, icicle);
                chipGroup, icicle);
        mSearchManager.updateChips(getCurrentRoot().derivedMimeTypes);
        mSearchManager.updateChips(getCurrentRoot().derivedMimeTypes);
+85 −28
Original line number Original line Diff line number Diff line
@@ -21,9 +21,11 @@ import android.content.Context;
import android.os.Bundle;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.HorizontalScrollView;


import androidx.annotation.NonNull;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.VisibleForTesting;


import com.android.documentsui.IconUtils;
import com.android.documentsui.IconUtils;
@@ -32,7 +34,6 @@ import com.android.documentsui.base.MimeTypes;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.Shared;


import com.google.android.material.chip.Chip;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Ints;


import java.util.ArrayList;
import java.util.ArrayList;
@@ -67,7 +68,7 @@ public class SearchChipViewManager {


    private static final Map<Integer, SearchChipData> sChipItems = new HashMap<>();
    private static final Map<Integer, SearchChipData> sChipItems = new HashMap<>();


    private final ChipGroup mChipGroup;
    private final ViewGroup mChipGroup;
    private SearchChipViewManagerListener mListener;
    private SearchChipViewManagerListener mListener;


    @VisibleForTesting
    @VisibleForTesting
@@ -85,9 +86,7 @@ public class SearchChipViewManager {
                new SearchChipData(TYPE_VIDEOS, R.string.chip_title_videos, VIDEOS_MIMETYPES));
                new SearchChipData(TYPE_VIDEOS, R.string.chip_title_videos, VIDEOS_MIMETYPES));
    }
    }



    public SearchChipViewManager(@NonNull ViewGroup chipGroup) {

    public SearchChipViewManager(@NonNull ChipGroup chipGroup) {
        mChipGroup = chipGroup;
        mChipGroup = chipGroup;
    }
    }


@@ -191,7 +190,7 @@ public class SearchChipViewManager {
                mChipGroup.addView(chip);
                mChipGroup.addView(chip);
            }
            }
        }
        }
        reorderCheckedChips(false /* hasAnim */);
        reorderCheckedChips(null /* clickedChip */, false /* hasAnim */);
    }
    }




@@ -206,7 +205,6 @@ public class SearchChipViewManager {


    private static void setChipChecked(Chip chip, boolean isChecked) {
    private static void setChipChecked(Chip chip, boolean isChecked) {
        chip.setChecked(isChecked);
        chip.setChecked(isChecked);
        chip.setCheckedIconVisible(isChecked);
        chip.setChipIconVisible(!isChecked);
        chip.setChipIconVisible(!isChecked);
    }
    }


@@ -224,6 +222,13 @@ public class SearchChipViewManager {


    private void onChipClick(View v) {
    private void onChipClick(View v) {
        final Chip chip = (Chip) v;
        final Chip chip = (Chip) v;

        // We need to show/hide the chip icon in our design.
        // When we show/hide the chip icon or do reorder animation,
        // the ripple effect will be interrupted. So, skip ripple
        // effect when the chip is clicked.
        chip.getBackground().setVisible(false /* visible */, false /* restart */);

        final SearchChipData item = (SearchChipData) chip.getTag();
        final SearchChipData item = (SearchChipData) chip.getTag();
        if (chip.isChecked()) {
        if (chip.isChecked()) {
            mCheckedChipItems.add(item);
            mCheckedChipItems.add(item);
@@ -232,7 +237,8 @@ public class SearchChipViewManager {
        }
        }


        setChipChecked(chip, chip.isChecked());
        setChipChecked(chip, chip.isChecked());
        reorderCheckedChips(true /* hasAnim */);
        reorderCheckedChips(chip, true /* hasAnim */);

        if (mListener != null) {
        if (mListener != null) {
            mListener.onChipCheckStateChanged();
            mListener.onChipCheckStateChanged();
        }
        }
@@ -254,45 +260,96 @@ public class SearchChipViewManager {
    /**
    /**
     * Reorder the chips in chip group. The checked chip has higher order.
     * Reorder the chips in chip group. The checked chip has higher order.
     *
     *
     * @param clickedChip the clicked chip, may be null.
     * @param hasAnim if true, play move animation. Otherwise, not.
     * @param hasAnim if true, play move animation. Otherwise, not.
     */
     */
    private void reorderCheckedChips(boolean hasAnim) {
    private void reorderCheckedChips(@Nullable Chip clickedChip, boolean hasAnim) {
        final ArrayList<Chip> chipList = new ArrayList<>();
        final ArrayList<Chip> chipList = new ArrayList<>();
        final int count = mChipGroup.getChildCount();
        final int count = mChipGroup.getChildCount();
        final boolean playAnimation = hasAnim && mChipGroup.isAttachedToWindow();

        final Map<String, Float> originalXList = new HashMap<>();
        // if the size of chips is less than 2, no need to reorder chips
        if (count < 2) {
            return;
        }

        Chip item;
        Chip item;
        // get the default order
        for (int i = 0; i < count; i++) {
        for (int i = 0; i < count; i++) {
            item = (Chip) mChipGroup.getChildAt(i);
            item = (Chip) mChipGroup.getChildAt(i);
            chipList.add(item);
            chipList.add(item);
            if (playAnimation) {
                originalXList.put(item.getText().toString(), item.getX());
            }
        }
        }


        final int chipSpacing = mChipGroup.getChipSpacingHorizontal();
        // sort chips
        float lastX = chipList.get(0).getX();
        Collections.sort(chipList, CHIP_COMPARATOR);
        Collections.sort(chipList, CHIP_COMPARATOR);


        mChipGroup.removeAllViews();
        if (isChipOrderMatched(mChipGroup, chipList)) {
            // the order of chips is not changed
            return;
        }

        final int chipSpacing = mChipGroup.getPaddingEnd();
        final boolean isRtl = mChipGroup.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
        float lastX = isRtl ? mChipGroup.getWidth() - chipSpacing : chipSpacing;

        // remove all chips except current clicked chip to avoid losing
        // accessibility focus.
        for (int i = count - 1; i >= 0; i--) {
            item = (Chip) mChipGroup.getChildAt(i);
            if (!item.equals(clickedChip)) {
                mChipGroup.removeView(item);
            }
        }

        // add sorted chips
        for (int i = 0; i < count; i++) {
            item = chipList.get(i);
            if (!item.equals(clickedChip)) {
                mChipGroup.addView(item, i);
            }
        }

        if (hasAnim && mChipGroup.isAttachedToWindow()) {
            // start animation
            for (Chip chip : chipList) {
            for (Chip chip : chipList) {
            mChipGroup.addView(chip);
                if (isRtl) {
            if (playAnimation) {
                    lastX -= chip.getMeasuredWidth();
                ObjectAnimator animator = ObjectAnimator.ofFloat(chip, "x",
                }
                        originalXList.get(chip.getText().toString()), lastX);

                ObjectAnimator animator = ObjectAnimator.ofFloat(chip, "x", chip.getX(), lastX);

                if (isRtl) {
                    lastX -= chipSpacing;
                } else {
                    lastX += chip.getMeasuredWidth() + chipSpacing;
                }
                animator.setDuration(CHIP_MOVE_ANIMATION_DURATION);
                animator.setDuration(CHIP_MOVE_ANIMATION_DURATION);
                animator.start();
                animator.start();
            }
            }
            lastX += chipSpacing + chip.getMeasuredWidth();
        }


        if (playAnimation) {
            // Let the first checked chip can be shown.
            // Let the first checked chip can be seen.
            View parent = (View) mChipGroup.getParent();
            View parent = (View) mChipGroup.getParent();
            if (parent != null && parent instanceof HorizontalScrollView) {
            if (parent instanceof HorizontalScrollView) {
                ((HorizontalScrollView) mChipGroup.getParent()).smoothScrollTo(0, 0);
                final int scrollToX = isRtl ? parent.getWidth() : 0;
                ((HorizontalScrollView) parent).smoothScrollTo(scrollToX, 0);
            }
        }
    }

    private static boolean isChipOrderMatched(ViewGroup chipGroup, ArrayList<Chip> chipList) {
        if (chipGroup == null || chipList == null) {
            return false;
        }

        final int chipCount = chipList.size();
        if (chipGroup.getChildCount() != chipCount) {
            return false;
        }
        for (int i = 0; i < chipCount; i++) {
            if (!chipList.get(i).equals(chipGroup.getChildAt(i))) {
                return false;
            }
            }
        }
        }
        return true;
    }
    }


    /**
    /**
+2 −3
Original line number Original line Diff line number Diff line
@@ -35,6 +35,7 @@ import android.view.MenuItem.OnActionExpandListener;
import android.view.View;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.View.OnFocusChangeListener;
import android.view.ViewGroup;


import androidx.annotation.GuardedBy;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.Nullable;
@@ -50,8 +51,6 @@ import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
import com.android.documentsui.base.State;


import com.google.android.material.chip.ChipGroup;

import java.util.Timer;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TimerTask;


@@ -91,7 +90,7 @@ public class SearchViewManager implements
    public SearchViewManager(
    public SearchViewManager(
            SearchManagerListener listener,
            SearchManagerListener listener,
            EventHandler<String> commandProcessor,
            EventHandler<String> commandProcessor,
            ChipGroup chipGroup,
            ViewGroup chipGroup,
            @Nullable Bundle savedState) {
            @Nullable Bundle savedState) {
        this(listener, commandProcessor, new SearchChipViewManager(chipGroup), savedState,
        this(listener, commandProcessor, new SearchChipViewManager(chipGroup), savedState,
                new Timer(), new Handler(Looper.getMainLooper()));
                new Timer(), new Handler(Looper.getMainLooper()));
Loading