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

Commit e48e4ca5 authored by Ben Kwa's avatar Ben Kwa
Browse files

Fix focus issues on animated items.

Implement a custom ItemAnimator to enable focus to persist across change
animations in the DirectoryFragment's RecyclerView.

Note that this is a temporary stop-gap - a better fix would be to have
the RecyclerView restore focus to the appropriate item after a change
animation.

BUG=24802917

Change-Id: I4e4e6f758a2ea87e6235533860863c822d71a27a
parent 67b78a3d
Loading
Loading
Loading
Loading
+44 −15
Original line number Diff line number Diff line
@@ -68,7 +68,6 @@ import android.support.v7.widget.RecyclerView.LayoutManager;
import android.support.v7.widget.RecyclerView.OnItemTouchListener;
import android.support.v7.widget.RecyclerView.RecyclerListener;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.SimpleItemAnimator;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.format.Formatter;
@@ -163,6 +162,9 @@ public class DirectoryFragment extends Fragment {
    private MessageBar mMessageBar;
    private View mProgressBar;

    private int mSelectedItemColor;
    private int mDefaultItemColor;

    public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
        show(fm, TYPE_NORMAL, root, doc, null, anim);
    }
@@ -255,8 +257,7 @@ public class DirectoryFragment extends Fragment {
                    }
                });

        // TODO: Restore transition animations.  See b/24802917.
        ((SimpleItemAnimator) mRecView.getItemAnimator()).setSupportsChangeAnimations(false);
        mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));

        // TODO: Add a divider between views (which might use RecyclerView.ItemDecoration).
        if (DEBUG_ENABLE_DND) {
@@ -294,6 +295,13 @@ public class DirectoryFragment extends Fragment {
        mAdapter = new DocumentsAdapter(context);
        mRecView.setAdapter(mAdapter);

        mDefaultItemColor = context.getResources().getColor(android.R.color.transparent);
        // Get the accent color.
        TypedValue selColor = new TypedValue();
        context.getTheme().resolveAttribute(android.R.attr.colorAccent, selColor, true);
        // Set the opacity to 10%.
        mSelectedItemColor = (selColor.data & 0x00ffffff) | 0x16000000;

        GestureDetector.SimpleOnGestureListener listener =
                new GestureDetector.SimpleOnGestureListener() {
                    @Override
@@ -899,24 +907,26 @@ public class DirectoryFragment extends Fragment {
    // Provide a reference to the views for each data item
    // Complex data items may need more than one view per item, and
    // you provide access to all the views for a data item in a view holder
    private static final class DocumentHolder
    private final class DocumentHolder
            extends RecyclerView.ViewHolder
            implements View.OnKeyListener
    {
        // each data item is just a string in this case
        public View view;
        public String docId;  // The stable document id.
        private ClickListener mClickListener;
        private View.OnKeyListener mKeyListener;

        public DocumentHolder(View view) {
            super(view);
            this.view = view;
            // Setting this using android:focusable in the item layouts doesn't work for list items.
            // So we set it here.  Note that touch mode focus is a separate issue - see
            // View.setFocusableInTouchMode and View.isInTouchMode for more info.
            this.view.setFocusable(true);
            this.view.setOnKeyListener(this);
            view.setFocusable(true);
            view.setOnKeyListener(this);
        }

        public void setSelected(boolean selected) {
            itemView.setActivated(selected);
            itemView.setBackgroundColor(selected ? mSelectedItemColor : mDefaultItemColor);
        }

        @Override
@@ -945,11 +955,11 @@ public class DirectoryFragment extends Fragment {
            checkState(mKeyListener == null);
            mKeyListener = listener;
        }
    }

    interface ClickListener {
        public void onClick(DocumentHolder doc);
    }
    }

    void showEmptyView() {
        mEmptyView.setVisibility(View.VISIBLE);
@@ -1007,6 +1017,24 @@ public class DirectoryFragment extends Fragment {
            return holder;
        }

        /**
         * Deal with selection changed events by using a custom ItemAnimator that just changes the
         * background color.  This works around focus issues (otherwise items lose focus when their
         * selection state changes) but also optimizes change animations for selection.
         */
        @Override
        public void onBindViewHolder(DocumentHolder holder, int position, List<Object> payload) {
            final View itemView = holder.itemView;

            if (payload.contains(MultiSelectManager.SELECTION_CHANGED_MARKER)) {
                final boolean selected = isSelected(position);
                itemView.setActivated(selected);
                return;
            } else {
                onBindViewHolder(holder, position);
            }
        }

        @Override
        public void onBindViewHolder(DocumentHolder holder, int position) {

@@ -1032,8 +1060,9 @@ public class DirectoryFragment extends Fragment {
            final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);

            holder.docId = docId;
            final View itemView = holder.view;
            itemView.setActivated(isSelected(position));
            final View itemView = holder.itemView;

            holder.setSelected(isSelected(position));

            final View line1 = itemView.findViewById(R.id.line1);
            final View line2 = itemView.findViewById(R.id.line2);
@@ -2035,7 +2064,7 @@ public class DirectoryFragment extends Fragment {
        }
    }

    private class ItemClickListener implements DocumentHolder.ClickListener {
    private class ItemClickListener implements ClickListener {
        @Override
        public void onClick(DocumentHolder doc) {
            final int position = doc.getAdapterPosition();
+195 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 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.documentsui;

import android.animation.Animator;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.support.v4.util.ArrayMap;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Performs change animations on Items in DirectoryFragment's RecyclerView.  This class overrides
 * the way selection animations are normally performed - instead of cross fading the old Item with a
 * new Item, this class manually animates a background color change.  This enables selected Items to
 * correctly maintain focus.
 */
class DirectoryItemAnimator extends DefaultItemAnimator {
    private final List<ColorAnimation> mPendingAnimations = new ArrayList<>();
    private final Map<RecyclerView.ViewHolder, ColorAnimation> mRunningAnimations =
            new ArrayMap<>();
    private final Integer mDefaultColor;
    private final Integer mSelectedColor;

    public DirectoryItemAnimator(Context context) {
        mDefaultColor = context.getResources().getColor(android.R.color.transparent);
        // Get the accent color.
        TypedValue selColor = new TypedValue();
        context.getTheme().resolveAttribute(android.R.attr.colorAccent, selColor, true);
        // Set the opacity to 10%.
        mSelectedColor = (selColor.data & 0x00ffffff) | 0x16000000;
    }

    @Override
    public void runPendingAnimations() {
        super.runPendingAnimations();
        for (ColorAnimation anim: mPendingAnimations) {
            anim.start();
            mRunningAnimations.put(anim.viewHolder, anim);
        }
        mPendingAnimations.clear();
    }

    @Override
    public void endAnimation(RecyclerView.ViewHolder vh) {
        super.endAnimation(vh);

        for (int i = mPendingAnimations.size() - 1; i >= 0; --i) {
            ColorAnimation anim = mPendingAnimations.get(i);
            if (anim.viewHolder == vh) {
                mPendingAnimations.remove(i);
                anim.end();
            }
        }

        ColorAnimation anim = mRunningAnimations.get(vh);
        if (anim != null) {
            anim.cancel();
        }
    }

    @Override
    public ItemHolderInfo recordPreLayoutInformation(
        RecyclerView.State state,
        RecyclerView.ViewHolder viewHolder,
        @AdapterChanges int changeFlags,
        List<Object> payloads) {
        ItemInfo info = (ItemInfo) super.recordPreLayoutInformation(state,
                viewHolder, changeFlags, payloads);
        info.isActivated = viewHolder.itemView.isActivated();
        return info;
    }


    @Override
    public ItemHolderInfo recordPostLayoutInformation(
        RecyclerView.State state, RecyclerView.ViewHolder viewHolder) {
        ItemInfo info = (ItemInfo) super.recordPostLayoutInformation(state,
                viewHolder);
        info.isActivated = viewHolder.itemView.isActivated();
        return info;
    }

    @Override
    public boolean animateChange(final RecyclerView.ViewHolder oldHolder,
            RecyclerView.ViewHolder newHolder, ItemHolderInfo preInfo,
            ItemHolderInfo postInfo) {
        if (oldHolder != newHolder) {
            return super.animateChange(oldHolder, newHolder, preInfo, postInfo);
        }

        ItemInfo pre = (ItemInfo)preInfo;
        ItemInfo post = (ItemInfo)postInfo;

        if (pre.isActivated == post.isActivated) {
            dispatchAnimationFinished(oldHolder);
            return false;
        } else {
            Integer startColor = pre.isActivated ? mSelectedColor : mDefaultColor;
            Integer endColor = post.isActivated ? mSelectedColor : mDefaultColor;
            oldHolder.itemView.setBackgroundColor(startColor);
            mPendingAnimations.add(new ColorAnimation(oldHolder, startColor, endColor));
        }
        return true;
    }

    @Override
    public ItemHolderInfo obtainHolderInfo() {
        return new ItemInfo();
    }

    @Override
    public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder vh) {
        return true;
    }

    class ItemInfo extends DefaultItemAnimator.ItemHolderInfo {
        boolean isActivated;
    };

    /**
     * Animates changes in background color.
     */
    class ColorAnimation
            implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener {
        ValueAnimator mValueAnimator;
        final RecyclerView.ViewHolder viewHolder;
        int mEndColor;

        public ColorAnimation(RecyclerView.ViewHolder vh, int startColor, int endColor)
        {
            viewHolder = vh;
            mValueAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), startColor, endColor);
            mValueAnimator.addUpdateListener(this);
            mValueAnimator.addListener(this);

            mEndColor = endColor;
        }

        public void start() {
            mValueAnimator.start();
        }

        public void cancel() {
            mValueAnimator.cancel();
        }

        public void end() {
            mValueAnimator.end();
        }

        @Override
        public void onAnimationUpdate(ValueAnimator animator) {
            viewHolder.itemView.setBackgroundColor((Integer)animator.getAnimatedValue());
        }

        @Override
        public void onAnimationEnd(Animator animator) {
            viewHolder.itemView.setBackgroundColor(mEndColor);
            mRunningAnimations.remove(viewHolder);
            dispatchAnimationFinished(viewHolder);
        }

        @Override
        public void onAnimationStart(Animator animation) {
            dispatchAnimationStarted(viewHolder);
        }

        @Override
        public void onAnimationCancel(Animator animation) {}

        @Override
        public void onAnimationRepeat(Animator animation) {}
    };
};
+4 −1
Original line number Diff line number Diff line
@@ -76,6 +76,9 @@ public final class MultiSelectManager implements View.OnKeyListener {
    private Adapter<?> mAdapter;
    private boolean mSingleSelect;

    // Payloads for notifyItemChange to distinguish between selection and other events.
    public static final String SELECTION_CHANGED_MARKER = "Selection-Changed";

    @Nullable private BandController mBandManager;

    /**
@@ -460,7 +463,7 @@ public final class MultiSelectManager implements View.OnKeyListener {
        for (int i = lastListener; i > -1; i--) {
            mCallbacks.get(i).onItemStateChanged(position, selected);
        }
        mAdapter.notifyItemChanged(position);
        mAdapter.notifyItemChanged(position, SELECTION_CHANGED_MARKER);
    }

    /**