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

Commit 00a3fb8a authored by Kyle Horimoto's avatar Kyle Horimoto Committed by Android (Google) Code Review
Browse files

Merge "Integrate band selection into the files app."

parents 710885ff 62a7fd0e
Loading
Loading
Loading
Loading
+0 −335
Original line number Original line 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 static com.android.documentsui.Events.isMouseEvent;
import static com.android.internal.util.Preconditions.checkState;

import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.MotionEvent;
import android.view.View;

/**
 * Provides mouse driven band-select support when used in conjuction with {@link RecyclerView} and
 * {@link MultiSelectManager}. This class is responsible for rendering the band select overlay and
 * selecting overlaid items via MultiSelectManager.
 */
public class BandSelectManager extends RecyclerView.SimpleOnItemTouchListener {

    private static final int NOT_SELECTED = -1;
    private static final int NOT_SET = -1;

    // For debugging purposes.
    private static final String TAG = "BandSelectManager";
    private static final boolean DEBUG = false;

    private final RecyclerView mRecyclerView;
    private final MultiSelectManager mSelectManager;
    private final Drawable mRegionSelectorDrawable;
    private final SparseBooleanArray mSelectedByBand = new SparseBooleanArray();

    private boolean mIsBandSelectActive = false;
    private Point mOrigin;
    private Point mPointer;
    private Rect mBounds;

    // Maintain the last selection made by band, so if bounds shrink back, we can deselect
    // the respective items.
    private int mCursorDeltaY = 0;
    private int mFirstSelected = NOT_SELECTED;

    // The time at which the current band selection-induced scroll began. If no scroll is in
    // progress, the value is NOT_SET.
    private long mScrollStartTime = NOT_SET;
    private final Runnable mScrollRunnable = new Runnable() {
        /**
         * The number of milliseconds of scrolling at which scroll speed continues to increase. At
         * first, the scroll starts slowly; then, the rate of scrolling increases until it reaches
         * its maximum value at after this many milliseconds.
         */
        private static final long SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;

        @Override
        public void run() {
            // Compute the number of pixels the pointer's y-coordinate is past the view. Negative
            // values mean the pointer is at or before the top of the view, and positive values mean
            // that the pointer is at or after the bottom of the view. Note that one additional
            // pixel is added here so that the view still scrolls when the pointer is exactly at the
            // top or bottom.
            int pixelsPastView = 0;
            if (mPointer.y <= 0) {
                pixelsPastView = mPointer.y - 1;
            } else if (mPointer.y >= mRecyclerView.getHeight() - 1) {
                pixelsPastView = mPointer.y - mRecyclerView.getHeight() + 1;
            }

            if (!mIsBandSelectActive || pixelsPastView == 0) {
                // If band selection is inactive, or if it is active but not at the edge of the
                // view, no scrolling is necessary.
                mScrollStartTime = NOT_SET;
                return;
            }

            if (mScrollStartTime == NOT_SET) {
                // If the pointer was previously not at the edge of the view but now is, set the
                // start time for the scroll.
                mScrollStartTime = System.currentTimeMillis();
            }

            // Compute the number of pixels to scroll, and scroll that many pixels.
            final int numPixels = computeNumPixelsToScroll(
                    pixelsPastView, System.currentTimeMillis() - mScrollStartTime);
            mRecyclerView.scrollBy(0, numPixels);

            // Adjust the y-coordinate of the origin the opposite number of pixels so that the
            // origin remains in the same place relative to the view's items.
            mOrigin.y -= numPixels;
            resizeBandSelectRectangle();

            mRecyclerView.removeCallbacks(mScrollRunnable);
            mRecyclerView.postOnAnimation(this);
        }

        /**
         * Computes the number of pixels to scroll based on how far the pointer is past the end of
         * the view and how long it has been there. Roughly based on ItemTouchHelper's algorithm for
         * computing the number of pixels to scroll when an item is dragged to the end of a
         * {@link RecyclerView}.
         * @param pixelsPastView
         * @param scrollDuration
         * @return
         */
        private int computeNumPixelsToScroll(int pixelsPastView, long scrollDuration) {
            final int maxScrollStep = computeMaxScrollStep(mRecyclerView);
            final int direction = (int) Math.signum(pixelsPastView);
            final int absPastView = Math.abs(pixelsPastView);

            // Calculate the ratio of how far out of the view the pointer currently resides to the
            // entire height of the view.
            final float outOfBoundsRatio = Math.min(
                    1.0f, (float) absPastView / mRecyclerView.getHeight());
            // Interpolate this ratio and use it to compute the maximum scroll that should be
            // possible for this step.
            final float cappedScrollStep =
                    direction * maxScrollStep * smoothOutOfBoundsRatio(outOfBoundsRatio);

            // Likewise, calculate the ratio of the time spent in the scroll to the limit.
            final float timeRatio = Math.min(
                    1.0f, (float) scrollDuration / SCROLL_ACCELERATION_LIMIT_TIME_MS);
            // Interpolate this ratio and use it to compute the final number of pixels to scroll.
            final int numPixels = (int) (cappedScrollStep * smoothTimeRatio(timeRatio));

            // If the final number of pixels to scroll ends up being 0, the view should still scroll
            // at least one pixel.
            return numPixels != 0 ? numPixels : direction;
        }

        /**
         * Computes the maximum scroll allowed for a given animation frame. Currently, this
         * defaults to the height of the view, but this could be tweaked if this results in scrolls
         * that are too fast or too slow.
         * @param rv
         * @return
         */
        private int computeMaxScrollStep(RecyclerView rv) {
            return rv.getHeight();
        }

        /**
         * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends at
         * (1,1) and quickly approaches 1 near the start of that interval. This ensures that drags
         * that are at the edge or barely past the edge of the view still cause sufficient
         * scrolling. The equation y=(x-1)^5+1 is used, but this could also be tweaked if needed.
         * @param ratio A ratio which is in the range [0, 1].
         * @return A "smoothed" value, also in the range [0, 1].
         */
        private float smoothOutOfBoundsRatio(float ratio) {
            return (float) Math.pow(ratio - 1.0f, 5) + 1.0f;
        }

        /**
         * Interpolates the given time ratio on a curve which starts at (0,0) and ends at (1,1) and
         * stays close to 0 for most input values except those very close to 1. This ensures that
         * scrolls start out very slowly but speed up drastically after the scroll has been in
         * progress close to SCROLL_ACCELERATION_LIMIT_TIME_MS. The equation y=x^5 is used, but this
         * could also be tweaked if needed.
         * @param ratio A ratio which is in the range [0, 1].
         * @return A "smoothed" value, also in the range [0, 1].
         */
        private float smoothTimeRatio(float ratio) {
            return (float) Math.pow(ratio, 5);
        }
    };

    /**
     * @param recyclerView
     * @param multiSelectManager
     */
    public BandSelectManager(RecyclerView recyclerView, MultiSelectManager multiSelectManager) {
        mRecyclerView = recyclerView;
        mSelectManager = multiSelectManager;
        mRegionSelectorDrawable =
            mRecyclerView.getContext().getTheme().getDrawable(R.drawable.band_select_overlay);

        mRecyclerView.addOnItemTouchListener(this);
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        // Only intercept the event if it was triggered by a mouse. If band select is inactive,
        // do not intercept ACTION_UP events as they will not be processed.
        return isMouseEvent(e) &&
                (mIsBandSelectActive || e.getActionMasked() != MotionEvent.ACTION_UP);
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
        checkState(isMouseEvent(e));
        processMotionEvent(e);
    }

    /**
     * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
     * @param e
     */
    private void processMotionEvent(MotionEvent e) {
        if (mIsBandSelectActive && e.getActionMasked() == MotionEvent.ACTION_UP) {
            endBandSelect();
            return;
        }

        mPointer = new Point((int) e.getX(), (int) e.getY());
        if (!mIsBandSelectActive) {
            startBandSelect();
        }

        scrollViewIfNecessary();
        resizeBandSelectRectangle();
        selectChildrenCoveredBySelection();
    }

    /**
     * Starts band select by adding the drawable to the RecyclerView's overlay.
     */
    private void startBandSelect() {
        if (DEBUG) Log.d(TAG, "Starting band select from (" + mPointer.x + "," + mPointer.y + ").");
        mIsBandSelectActive = true;
        mOrigin = mPointer;
        mRecyclerView.getOverlay().add(mRegionSelectorDrawable);
    }

    /**
     * Scrolls the view if necessary.
     */
    private void scrollViewIfNecessary() {
        mRecyclerView.removeCallbacks(mScrollRunnable);
        mScrollRunnable.run();
        mRecyclerView.invalidate();
    }

    /**
     * Resizes the band select rectangle by using the origin and the current pointer positoin as
     * two opposite corners of the selection.
     */
    private void resizeBandSelectRectangle() {
        if (mBounds != null) {
            mCursorDeltaY = mPointer.y - mBounds.bottom;
        }

        mBounds = new Rect(Math.min(mOrigin.x, mPointer.x),
                Math.min(mOrigin.y, mPointer.y),
                Math.max(mOrigin.x, mPointer.x),
                Math.max(mOrigin.y, mPointer.y));

        mRegionSelectorDrawable.setBounds(mBounds);
    }

    /**
     * Selects the children covered by the band select overlay by delegating to MultiSelectManager.
     * TODO: Provide a finished implementation. This is down and dirty, proof of concept code.
     * Final optimized implementation, with support for managing offscreen selection to come.
     */
    private void selectChildrenCoveredBySelection() {

        // track top and bottom selections. Details on why this is useful below.
        int first = NOT_SELECTED;
        int last = NOT_SELECTED;

        for (int i = 0; i < mRecyclerView.getChildCount(); i++) {

            View child = mRecyclerView.getChildAt(i);
            ViewHolder holder = mRecyclerView.getChildViewHolder(child);
            Rect childRect = new Rect();
            child.getHitRect(childRect);

            boolean shouldSelect = Rect.intersects(childRect, mBounds);
            int position = holder.getAdapterPosition();

            // This also allows us to clear the selection of elements
            // that only temporarily entered the bounds of the band.
            if (mSelectedByBand.get(position) && !shouldSelect) {
                mSelectManager.setItemSelected(position, false);
                mSelectedByBand.delete(position);
            }

            // We need to keep track of the first and last items selected.
            // We'll use this information along with cursor direction
            // to determine the starting point of the selection.
            // We provide this information to selection manager
            // to enable more natural user interaction when working
            // with Shift+Click and multiple contiguous selection ranges.
            if (shouldSelect) {
                if (first == NOT_SELECTED) {
                    first = position;
                } else {
                    last = position;
                }
                mSelectManager.setItemSelected(position, true);
                mSelectedByBand.put(position, true);
            }
        }

        // Remember which is the last selected item, so we can
        // share that with selection manager when band select ends.
        // It'll use that as it's begin selection point when
        // user SHIFT+Clicks.
        if (mCursorDeltaY < 0 && last != NOT_SELECTED) {
            mFirstSelected = last;
        } else if (mCursorDeltaY > 0 && first != NOT_SELECTED) {
            mFirstSelected = first;
        }
    }

    /**
     * Ends band select by removing the overlay.
     */
    private void endBandSelect() {
        if (DEBUG) Log.d(TAG, "Ending band select.");
        mIsBandSelectActive = false;
        mSelectedByBand.clear();
        mRecyclerView.getOverlay().remove(mRegionSelectorDrawable);
        if (mFirstSelected != NOT_SELECTED) {
            mSelectManager.setSelectionFocusBegin(mFirstSelected);
        }
    }
}
+0 −645

File deleted.

Preview size limit exceeded, changes collapsed.

+1 −3
Original line number Original line Diff line number Diff line
@@ -1773,8 +1773,6 @@ public class DirectoryFragment extends Fragment {
        }
        }


        @Override
        @Override
        public void afterActivityCreated(DirectoryFragment fragment) {
        public void afterActivityCreated(DirectoryFragment fragment) {}
            new BandSelectManager(fragment.mRecView, fragment.mSelectionManager);
        }
    }
    }
}
}
+1184 −60

File changed.

Preview size limit exceeded, changes collapsed.

+51 −42
Original line number Original line Diff line number Diff line
@@ -18,7 +18,7 @@ package com.android.documentsui;


import static org.junit.Assert.*;
import static org.junit.Assert.*;


import com.android.documentsui.BandSelectMatrix;
import com.android.documentsui.MultiSelectManager.BandSelectModel;


import android.graphics.Point;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Rect;
@@ -28,13 +28,13 @@ import android.util.SparseBooleanArray;
import org.junit.After;
import org.junit.After;
import org.junit.Test;
import org.junit.Test;


public class BandSelectMatrixTest {
public class BandSelectModelTest {


    private static final int VIEW_PADDING_PX = 5;
    private static final int VIEW_PADDING_PX = 5;
    private static final int CHILD_VIEW_EDGE_PX = 100;
    private static final int CHILD_VIEW_EDGE_PX = 100;
    private static final int VIEWPORT_HEIGHT = 500;
    private static final int VIEWPORT_HEIGHT = 500;


    private static BandSelectMatrix matrix;
    private static BandSelectModel model;
    private static TestHelper helper;
    private static TestHelper helper;
    private static SparseBooleanArray lastSelection;
    private static SparseBooleanArray lastSelection;
    private static int viewWidth;
    private static int viewWidth;
@@ -42,8 +42,8 @@ public class BandSelectMatrixTest {
    private static void setUp(int numChildren, int numColumns) {
    private static void setUp(int numChildren, int numColumns) {
        helper = new TestHelper(numChildren, numColumns);
        helper = new TestHelper(numChildren, numColumns);
        viewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX);
        viewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX);
        matrix = new BandSelectMatrix(helper);
        model = new BandSelectModel(helper);
        matrix.addOnSelectionChangedListener(new BandSelectMatrix.OnSelectionChangedListener() {
        model.addOnSelectionChangedListener(new BandSelectModel.OnSelectionChangedListener() {


            @Override
            @Override
            public void onSelectionChanged(SparseBooleanArray updatedSelection) {
            public void onSelectionChanged(SparseBooleanArray updatedSelection) {
@@ -54,7 +54,7 @@ public class BandSelectMatrixTest {


    @After
    @After
    public void tearDown() {
    public void tearDown() {
        matrix = null;
        model = null;
        helper = null;
        helper = null;
        lastSelection = null;
        lastSelection = null;
    }
    }
@@ -62,111 +62,120 @@ public class BandSelectMatrixTest {
    @Test
    @Test
    public void testSelectionLeftOfItems() {
    public void testSelectionLeftOfItems() {
        setUp(20, 5);
        setUp(20, 5);
        matrix.startSelection(new Point(0, 10));
        model.startSelection(new Point(0, 10));
        matrix.resizeSelection(new Point(1, 11));
        model.resizeSelection(new Point(1, 11));
        assertSelected(new int[0]);
        assertSelected(new int[0]);
        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
    }
    }


    @Test
    @Test
    public void testSelectionRightOfItems() {
    public void testSelectionRightOfItems() {
        setUp(20, 4);
        setUp(20, 4);
        matrix.startSelection(new Point(viewWidth - 1, 10));
        model.startSelection(new Point(viewWidth - 1, 10));
        matrix.resizeSelection(new Point(viewWidth - 2, 11));
        model.resizeSelection(new Point(viewWidth - 2, 11));
        assertSelected(new int[0]);
        assertSelected(new int[0]);
        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
    }
    }


    @Test
    @Test
    public void testSelectionAboveItems() {
    public void testSelectionAboveItems() {
        setUp(20, 4);
        setUp(20, 4);
        matrix.startSelection(new Point(10, 0));
        model.startSelection(new Point(10, 0));
        matrix.resizeSelection(new Point(11, 1));
        model.resizeSelection(new Point(11, 1));
        assertSelected(new int[0]);
        assertSelected(new int[0]);
        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
    }
    }


    @Test
    @Test
    public void testSelectionBelowItems() {
    public void testSelectionBelowItems() {
        setUp(5, 4);
        setUp(5, 4);
        matrix.startSelection(new Point(10, VIEWPORT_HEIGHT - 1));
        model.startSelection(new Point(10, VIEWPORT_HEIGHT - 1));
        matrix.resizeSelection(new Point(11, VIEWPORT_HEIGHT - 2));
        model.resizeSelection(new Point(11, VIEWPORT_HEIGHT - 2));
        assertSelected(new int[0]);
        assertSelected(new int[0]);
        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
    }
    }


    @Test
    @Test
    public void testVerticalSelectionBetweenItems() {
    public void testVerticalSelectionBetweenItems() {
        setUp(20, 4);
        setUp(20, 4);
        matrix.startSelection(new Point(106, 0));
        model.startSelection(new Point(106, 0));
        matrix.resizeSelection(new Point(107, 200));
        model.resizeSelection(new Point(107, 200));
        assertSelected(new int[0]);
        assertSelected(new int[0]);
        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
    }
    }


    @Test
    @Test
    public void testHorizontalSelectionBetweenItems() {
    public void testHorizontalSelectionBetweenItems() {
        setUp(20, 4);
        setUp(20, 4);
        matrix.startSelection(new Point(0, 105));
        model.startSelection(new Point(0, 105));
        matrix.resizeSelection(new Point(200, 106));
        model.resizeSelection(new Point(200, 106));
        assertSelected(new int[0]);
        assertSelected(new int[0]);
        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
    }
    }


    @Test
    @Test
    public void testGrowingAndShrinkingSelection() {
    public void testGrowingAndShrinkingSelection() {
        setUp(20, 4);
        setUp(20, 4);
        matrix.startSelection(new Point(0, 0));
        model.startSelection(new Point(0, 0));
        matrix.resizeSelection(new Point(5, 5));
        model.resizeSelection(new Point(5, 5));
        assertSelected(new int[] {0});
        assertSelected(new int[] {0});
        matrix.resizeSelection(new Point(109, 109));
        model.resizeSelection(new Point(109, 109));
        assertSelected(new int[] {0});
        assertSelected(new int[] {0});
        matrix.resizeSelection(new Point(110, 109));
        model.resizeSelection(new Point(110, 109));
        assertSelected(new int[] {0, 1});
        assertSelected(new int[] {0, 1});
        matrix.resizeSelection(new Point(110, 110));
        model.resizeSelection(new Point(110, 110));
        assertSelected(new int[] {0, 1, 4, 5});
        assertSelected(new int[] {0, 1, 4, 5});
        matrix.resizeSelection(new Point(214, 214));
        model.resizeSelection(new Point(214, 214));
        assertSelected(new int[] {0, 1, 4, 5});
        assertSelected(new int[] {0, 1, 4, 5});
        matrix.resizeSelection(new Point(215, 214));
        model.resizeSelection(new Point(215, 214));
        assertSelected(new int[] {0, 1, 2, 4, 5, 6});
        assertSelected(new int[] {0, 1, 2, 4, 5, 6});
        matrix.resizeSelection(new Point(214, 214));
        model.resizeSelection(new Point(214, 214));
        assertSelected(new int[] {0, 1, 4, 5});
        assertSelected(new int[] {0, 1, 4, 5});
        matrix.resizeSelection(new Point(110, 110));
        model.resizeSelection(new Point(110, 110));
        assertSelected(new int[] {0, 1, 4, 5});
        assertSelected(new int[] {0, 1, 4, 5});
        matrix.resizeSelection(new Point(110, 109));
        model.resizeSelection(new Point(110, 109));
        assertSelected(new int[] {0, 1});
        assertSelected(new int[] {0, 1});
        matrix.resizeSelection(new Point(109, 109));
        model.resizeSelection(new Point(109, 109));
        assertSelected(new int[] {0});
        assertSelected(new int[] {0});
        matrix.resizeSelection(new Point(5, 5));
        model.resizeSelection(new Point(5, 5));
        assertSelected(new int[] {0});
        assertSelected(new int[] {0});
        matrix.resizeSelection(new Point(0, 0));
        model.resizeSelection(new Point(0, 0));
        assertSelected(new int[0]);
        assertSelected(new int[0]);
        assertEquals(BandSelectModel.NOT_SET, model.getPositionNearestOrigin());
    }
    }


    @Test
    @Test
    public void testSelectionMovingAroundOrigin() {
    public void testSelectionMovingAroundOrigin() {
        setUp(16, 4);
        setUp(16, 4);
        matrix.startSelection(new Point(210, 210));
        model.startSelection(new Point(210, 210));
        matrix.resizeSelection(new Point(viewWidth - 1, 0));
        model.resizeSelection(new Point(viewWidth - 1, 0));
        assertSelected(new int[] {2, 3, 6, 7});
        assertSelected(new int[] {2, 3, 6, 7});
        matrix.resizeSelection(new Point(0, 0));
        model.resizeSelection(new Point(0, 0));
        assertSelected(new int[] {0, 1, 4, 5});
        assertSelected(new int[] {0, 1, 4, 5});
        matrix.resizeSelection(new Point(0, 420));
        model.resizeSelection(new Point(0, 420));
        assertSelected(new int[] {8, 9, 12, 13});
        assertSelected(new int[] {8, 9, 12, 13});
        matrix.resizeSelection(new Point(viewWidth - 1, 420));
        model.resizeSelection(new Point(viewWidth - 1, 420));
        assertSelected(new int[] {10, 11, 14, 15});
        assertSelected(new int[] {10, 11, 14, 15});
        assertEquals(10, model.getPositionNearestOrigin());
    }
    }


    @Test
    @Test
    public void testScrollingBandSelect() {
    public void testScrollingBandSelect() {
        setUp(40, 4);
        setUp(40, 4);
        matrix.startSelection(new Point(0, 0));
        model.startSelection(new Point(0, 0));
        matrix.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
        model.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
        assertSelected(new int[] {0, 4, 8, 12, 16});
        assertSelected(new int[] {0, 4, 8, 12, 16});
        scroll(CHILD_VIEW_EDGE_PX);
        scroll(CHILD_VIEW_EDGE_PX);
        assertSelected(new int[] {0, 4, 8, 12, 16, 20});
        assertSelected(new int[] {0, 4, 8, 12, 16, 20});
        matrix.resizeSelection(new Point(200, VIEWPORT_HEIGHT - 1));
        model.resizeSelection(new Point(200, VIEWPORT_HEIGHT - 1));
        assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17, 20, 21});
        assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17, 20, 21});
        scroll(CHILD_VIEW_EDGE_PX);
        scroll(CHILD_VIEW_EDGE_PX);
        assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17, 20, 21, 24, 25});
        assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17, 20, 21, 24, 25});
        scroll(-2 * CHILD_VIEW_EDGE_PX);
        scroll(-2 * CHILD_VIEW_EDGE_PX);
        assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17});
        assertSelected(new int[] {0, 1, 4, 5, 8, 9, 12, 13, 16, 17});
        matrix.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
        model.resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
        assertSelected(new int[] {0, 4, 8, 12, 16});
        assertSelected(new int[] {0, 4, 8, 12, 16});
        assertEquals(0, model.getPositionNearestOrigin());
    }
    }


    private static void assertSelected(int[] selectedPositions) {
    private static void assertSelected(int[] selectedPositions) {
@@ -179,10 +188,10 @@ public class BandSelectMatrixTest {
    private static void scroll(int dy) {
    private static void scroll(int dy) {
        assertTrue(helper.verticalOffset + VIEWPORT_HEIGHT + dy <= helper.getTotalHeight());
        assertTrue(helper.verticalOffset + VIEWPORT_HEIGHT + dy <= helper.getTotalHeight());
        helper.verticalOffset += dy;
        helper.verticalOffset += dy;
        matrix.onScrolled(null, 0, dy);
        model.onScrolled(null, 0, dy);
    }
    }


    private static final class TestHelper implements BandSelectMatrix.RecyclerViewHelper {
    private static final class TestHelper implements MultiSelectManager.BandModelHelper {


        public int horizontalOffset = 0;
        public int horizontalOffset = 0;
        public int verticalOffset = 0;
        public int verticalOffset = 0;
Loading