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

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

Move focus-related code out of DirectoryFragment.

BUG=25195767

Change-Id: Ibf2247a81e8903924037b5f01305593219a8c068
parent f517cbe9
Loading
Loading
Loading
Loading
+9 −154
Original line number Diff line number Diff line
@@ -151,6 +151,7 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
    private MultiSelectManager mSelectionManager;
    private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
    private ItemEventListener mItemEventListener = new ItemEventListener();
    private FocusManager mFocusManager;

    private IconHelper mIconHelper;

@@ -262,6 +263,8 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi

        mSelectionManager.addCallback(selectionListener);

        mFocusManager = new FocusManager(mRecView, mSelectionManager);

        mModel = new Model();
        mModel.addUpdateListener(mAdapter);
        mModel.addUpdateListener(mModelUpdateListener);
@@ -1249,165 +1252,17 @@ public class DirectoryFragment extends Fragment implements DocumentsAdapter.Envi
                return false;
            }

            boolean handled = false;
            if (Events.isNavigationKeyCode(keyCode)) {
                // Find the target item and focus it.
                int endPos = findTargetPosition(doc.itemView, keyCode);

                if (endPos != RecyclerView.NO_POSITION) {
                    focusItem(endPos);

                    // Handle any necessary adjustments to selection.
                    boolean extendSelection = event.isShiftPressed();
                    if (extendSelection) {
                        int startPos = doc.getAdapterPosition();
                        mSelectionManager.selectRange(startPos, endPos);
                    }
                    handled = true;
            if (mFocusManager.handleKey(doc, keyCode, event)) {
                return true;
            }
            } else {

            // Handle enter key events
            if (keyCode == KeyEvent.KEYCODE_ENTER) {
                    handled = onActivate(doc);
                }
            }

            return handled;
        }

        /**
         * Finds the destination position where the focus should land for a given navigation event.
         *
         * @param view The view that received the event.
         * @param keyCode The key code for the event.
         * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION.
         */
        private int findTargetPosition(View view, int keyCode) {
            switch (keyCode) {
                case KeyEvent.KEYCODE_MOVE_HOME:
                    return 0;
                case KeyEvent.KEYCODE_MOVE_END:
                    return mAdapter.getItemCount() - 1;
                case KeyEvent.KEYCODE_PAGE_UP:
                case KeyEvent.KEYCODE_PAGE_DOWN:
                    return findTargetPositionByPage(view, keyCode);
            }

            // Find a navigation target based on the arrow key that the user pressed.
            int searchDir = -1;
            switch (keyCode) {
                case KeyEvent.KEYCODE_DPAD_UP:
                    searchDir = View.FOCUS_UP;
                    break;
                case KeyEvent.KEYCODE_DPAD_DOWN:
                    searchDir = View.FOCUS_DOWN;
                    break;
                case KeyEvent.KEYCODE_DPAD_LEFT:
                    searchDir = View.FOCUS_LEFT;
                    break;
                case KeyEvent.KEYCODE_DPAD_RIGHT:
                    searchDir = View.FOCUS_RIGHT;
                    break;
            }

            if (searchDir != -1) {
                View targetView = view.focusSearch(searchDir);
                // TargetView can be null, for example, if the user pressed <down> at the bottom
                // of the list.
                if (targetView != null) {
                    // Ignore navigation targets that aren't items in the RecyclerView.
                    if (targetView.getParent() == mRecView) {
                        return mRecView.getChildAdapterPosition(targetView);
                    }
                }
            }

            return RecyclerView.NO_POSITION;
                return onActivate(doc);
            }

        /**
         * Given a PgUp/PgDn event and the current view, find the position of the target view.
         * This returns:
         * <li>The position of the topmost (or bottom-most) visible item, if the current item is not
         *     the top- or bottom-most visible item.
         * <li>The position of an item that is one page's worth of items up (or down) if the current
         *      item is the top- or bottom-most visible item.
         * <li>The first (or last) item, if paging up (or down) would go past those limits.
         * @param view The view that received the key event.
         * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN.
         * @return The adapter position of the target item.
         */
        private int findTargetPositionByPage(View view, int keyCode) {
            int first = mLayout.findFirstVisibleItemPosition();
            int last = mLayout.findLastVisibleItemPosition();
            int current = mRecView.getChildAdapterPosition(view);
            int pageSize = last - first + 1;

            if (keyCode == KeyEvent.KEYCODE_PAGE_UP) {
                if (current > first) {
                    // If the current item isn't the first item, target the first item.
                    return first;
                } else {
                    // If the current item is the first item, target the item one page up.
                    int target = current - pageSize;
                    return target < 0 ? 0 : target;
                }
            }

            if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) {
                if (current < last) {
                    // If the current item isn't the last item, target the last item.
                    return last;
                } else {
                    // If the current item is the last item, target the item one page down.
                    int target = current + pageSize;
                    int max = mAdapter.getItemCount() - 1;
                    return target < max ? target : max;
                }
            }

            throw new IllegalArgumentException("Unsupported keyCode: " + keyCode);
        }

        /**
         * Requests focus for the item in the given adapter position, scrolling the RecyclerView if
         * necessary.
         *
         * @param pos
         */
        public void focusItem(final int pos) {
            // If the item is already in view, focus it; otherwise, scroll to it and focus it.
            RecyclerView.ViewHolder vh = mRecView.findViewHolderForAdapterPosition(pos);
            if (vh != null) {
                vh.itemView.requestFocus();
            } else {
                mRecView.smoothScrollToPosition(pos);
                // Set a one-time listener to request focus when the scroll has completed.
                mRecView.addOnScrollListener(
                    new RecyclerView.OnScrollListener() {
                        @Override
                        public void onScrollStateChanged (RecyclerView view, int newState) {
                            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                                // When scrolling stops, find the item and focus it.
                                RecyclerView.ViewHolder vh =
                                        view.findViewHolderForAdapterPosition(pos);
                                if (vh != null) {
                                    vh.itemView.requestFocus();
                                } else {
                                    // This might happen in weird corner cases, e.g. if the user is
                                    // scrolling while a delete operation is in progress.  In that
                                    // case, just don't attempt to focus the missing item.
                                    Log.w(
                                        TAG, "Unable to focus position " + pos + " after a scroll");
                                }
                                view.removeOnScrollListener(this);
                            }
                        }
                    });
            }
            return false;
        }


    }

    private final class ModelUpdateListener implements Model.UpdateListener {
+207 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.dirlist;

import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;

import com.android.documentsui.Events;

/**
 * A class that handles navigation and focus within the DirectoryFragment.
 */
class FocusManager {
    private static final String TAG = "FocusManager";

    private RecyclerView mView;
    private RecyclerView.Adapter<?> mAdapter;
    private LinearLayoutManager mLayout;
    private MultiSelectManager mSelectionManager;

    public FocusManager(RecyclerView view, MultiSelectManager selectionManager) {
        mView = view;
        mAdapter = view.getAdapter();
        mLayout = (LinearLayoutManager) view.getLayoutManager();
        mSelectionManager = selectionManager;
    }

    /**
     * Handles navigation (setting focus, adjusting selection if needed) arising from incoming key
     * events.
     *
     * @param doc The DocumentHolder receiving the key event.
     * @param keyCode
     * @param event
     * @return Whether the event was handled.
     */
    public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
        boolean handled = false;
        if (Events.isNavigationKeyCode(keyCode)) {
            // Find the target item and focus it.
            int endPos = findTargetPosition(doc.itemView, keyCode, event);

            if (endPos != RecyclerView.NO_POSITION) {
                focusItem(endPos);

                // Handle any necessary adjustments to selection.
                boolean extendSelection = event.isShiftPressed();
                if (extendSelection) {
                    int startPos = doc.getAdapterPosition();
                    mSelectionManager.selectRange(startPos, endPos);
                }
                handled = true;
            }
        }
        return handled;
    }

    /**
     * Finds the destination position where the focus should land for a given navigation event.
     *
     * @param view The view that received the event.
     * @param keyCode The key code for the event.
     * @param event
     * @return The adapter position of the destination item. Could be RecyclerView.NO_POSITION.
     */
    private int findTargetPosition(View view, int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_MOVE_HOME:
                return 0;
            case KeyEvent.KEYCODE_MOVE_END:
                return mAdapter.getItemCount() - 1;
            case KeyEvent.KEYCODE_PAGE_UP:
            case KeyEvent.KEYCODE_PAGE_DOWN:
                return findPagedTargetPosition(view, keyCode, event);
        }

        // Find a navigation target based on the arrow key that the user pressed.
        int searchDir = -1;
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_UP:
                searchDir = View.FOCUS_UP;
                break;
            case KeyEvent.KEYCODE_DPAD_DOWN:
                searchDir = View.FOCUS_DOWN;
                break;
            case KeyEvent.KEYCODE_DPAD_LEFT:
                searchDir = View.FOCUS_LEFT;
                break;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                searchDir = View.FOCUS_RIGHT;
                break;
        }

        if (searchDir != -1) {
            View targetView = view.focusSearch(searchDir);
            // TargetView can be null, for example, if the user pressed <down> at the bottom
            // of the list.
            if (targetView != null) {
                // Ignore navigation targets that aren't items in the RecyclerView.
                if (targetView.getParent() == mView) {
                    return mView.getChildAdapterPosition(targetView);
                }
            }
        }

        return RecyclerView.NO_POSITION;
    }

    /**
     * Given a PgUp/PgDn event and the current view, find the position of the target view.
     * This returns:
     * <li>The position of the topmost (or bottom-most) visible item, if the current item is not
     *     the top- or bottom-most visible item.
     * <li>The position of an item that is one page's worth of items up (or down) if the current
     *      item is the top- or bottom-most visible item.
     * <li>The first (or last) item, if paging up (or down) would go past those limits.
     * @param view The view that received the key event.
     * @param keyCode Must be KEYCODE_PAGE_UP or KEYCODE_PAGE_DOWN.
     * @param event
     * @return The adapter position of the target item.
     */
    private int findPagedTargetPosition(View view, int keyCode, KeyEvent event) {
        int first = mLayout.findFirstVisibleItemPosition();
        int last = mLayout.findLastVisibleItemPosition();
        int current = mView.getChildAdapterPosition(view);
        int pageSize = last - first + 1;

        if (keyCode == KeyEvent.KEYCODE_PAGE_UP) {
            if (current > first) {
                // If the current item isn't the first item, target the first item.
                return first;
            } else {
                // If the current item is the first item, target the item one page up.
                int target = current - pageSize;
                return target < 0 ? 0 : target;
            }
        }

        if (keyCode == KeyEvent.KEYCODE_PAGE_DOWN) {
            if (current < last) {
                // If the current item isn't the last item, target the last item.
                return last;
            } else {
                // If the current item is the last item, target the item one page down.
                int target = current + pageSize;
                int max = mAdapter.getItemCount() - 1;
                return target < max ? target : max;
            }
        }

        throw new IllegalArgumentException("Unsupported keyCode: " + keyCode);
    }

    /**
     * Requests focus for the item in the given adapter position, scrolling the RecyclerView if
     * necessary.
     *
     * @param pos
     */
    private void focusItem(final int pos) {
        // If the item is already in view, focus it; otherwise, scroll to it and focus it.
        RecyclerView.ViewHolder vh = mView.findViewHolderForAdapterPosition(pos);
        if (vh != null) {
            vh.itemView.requestFocus();
        } else {
            mView.smoothScrollToPosition(pos);
            // Set a one-time listener to request focus when the scroll has completed.
            mView.addOnScrollListener(
                    new RecyclerView.OnScrollListener() {
                        @Override
                        public void onScrollStateChanged(RecyclerView view, int newState) {
                            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                                // When scrolling stops, find the item and focus it.
                                RecyclerView.ViewHolder vh =
                                        view.findViewHolderForAdapterPosition(pos);
                                if (vh != null) {
                                    vh.itemView.requestFocus();
                                } else {
                                    // This might happen in weird corner cases, e.g. if the user is
                                    // scrolling while a delete operation is in progress. In that
                                    // case, just don't attempt to focus the missing item.
                                    Log.w(TAG, "Unable to focus position " + pos + " after scroll");
                                }
                                view.removeOnScrollListener(this);
                            }
                        }
                    });
        }
    }
}