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

Commit 68dfdb65 authored by Ben Kwa's avatar Ben Kwa Committed by Android (Google) Code Review
Browse files

Merge "Fix focus issues on animated items."

parents f695d3ae e48e4ca5
Loading
Loading
Loading
Loading
+44 −15
Original line number Diff line number Diff line
@@ -66,7 +66,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;
@@ -162,6 +161,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);
    }
@@ -254,8 +256,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) {
@@ -293,6 +294,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
@@ -897,24 +905,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
@@ -943,11 +953,11 @@ public class DirectoryFragment extends Fragment {
            checkState(mKeyListener == null);
            mKeyListener = listener;
        }
    }

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

    void showEmptyView() {
        mEmptyView.setVisibility(View.VISIBLE);
@@ -1005,6 +1015,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) {

@@ -1030,8 +1058,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);
@@ -1959,7 +1988,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);
    }

    /**