mirror of https://github.com/M66B/FairEmail.git
				
				
				
			
							parent
							
								
									e6c4e05e9d
								
							
						
					
					
						commit
						c761642ef2
					
				@ -0,0 +1,45 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 | 
				
			||||
 | 
				
			||||
import android.graphics.Point;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.RestrictTo;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Provides support for auto-scrolling a view.
 | 
				
			||||
 *
 | 
				
			||||
 * @hide
 | 
				
			||||
 */
 | 
				
			||||
@RestrictTo(LIBRARY_GROUP)
 | 
				
			||||
public abstract class AutoScroller {
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Resets state of the scroller. Call this when the user activity that is driving
 | 
				
			||||
     * auto-scrolling is done.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract void reset();
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Processes a new input location.
 | 
				
			||||
     * @param location
 | 
				
			||||
     */
 | 
				
			||||
    public abstract void scroll(@NonNull Point location);
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,139 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
import android.view.View;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.recyclerview.widget.GridLayoutManager;
 | 
				
			||||
import androidx.recyclerview.widget.LinearLayoutManager;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Provides a means of controlling when and where band selection can be initiated.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Two default implementations are provided: {@link EmptyArea}, and {@link NonDraggableArea}.
 | 
				
			||||
 *
 | 
				
			||||
 * @see SelectionTracker.Builder#withBandPredicate(BandPredicate)
 | 
				
			||||
 */
 | 
				
			||||
public abstract class BandPredicate {
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return true if band selection can be initiated in response to the {@link MotionEvent}.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract boolean canInitiate(MotionEvent e);
 | 
				
			||||
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    static boolean hasSupportedLayoutManager(@NonNull RecyclerView recyclerView) {
 | 
				
			||||
        RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
 | 
				
			||||
        return lm instanceof GridLayoutManager
 | 
				
			||||
                || lm instanceof LinearLayoutManager;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * A BandPredicate that allows initiation of band selection only in areas of RecyclerView
 | 
				
			||||
     * that map to {@link RecyclerView#NO_POSITION}. In most cases, this will be the empty areas
 | 
				
			||||
     * between views.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * Use this implementation to permit band selection only in empty areas
 | 
				
			||||
     * surrounding view items. But be advised that if there is no empy area around
 | 
				
			||||
     * view items, band selection cannot be initiated.
 | 
				
			||||
     */
 | 
				
			||||
    public static final class EmptyArea extends BandPredicate {
 | 
				
			||||
 | 
				
			||||
        private final RecyclerView mRecyclerView;
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * @param recyclerView the owner RecyclerView
 | 
				
			||||
         */
 | 
				
			||||
        public EmptyArea(@NonNull RecyclerView recyclerView) {
 | 
				
			||||
            checkArgument(recyclerView != null);
 | 
				
			||||
 | 
				
			||||
            mRecyclerView = recyclerView;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public boolean canInitiate(@NonNull MotionEvent e) {
 | 
				
			||||
            if (!hasSupportedLayoutManager(mRecyclerView)
 | 
				
			||||
                    || mRecyclerView.hasPendingAdapterUpdates()) {
 | 
				
			||||
                return false;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            View itemView = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
 | 
				
			||||
            int position = itemView != null
 | 
				
			||||
                    ? mRecyclerView.getChildAdapterPosition(itemView)
 | 
				
			||||
                    : RecyclerView.NO_POSITION;
 | 
				
			||||
 | 
				
			||||
            return position == RecyclerView.NO_POSITION;
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * A BandPredicate that allows initiation of band selection in any area that is not
 | 
				
			||||
     * draggable as determined by consulting
 | 
				
			||||
     * {@link ItemDetailsLookup.ItemDetails#inDragRegion(MotionEvent)}. By default empty
 | 
				
			||||
     * areas (those with a position that maps to {@link RecyclerView#NO_POSITION}
 | 
				
			||||
     * are considered non-draggable.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * Use this implementation in order to permit band selection in
 | 
				
			||||
     * otherwise empty areas of a View. This is useful especially in
 | 
				
			||||
     * list layouts where there is no empty space surrounding the list items,
 | 
				
			||||
     * and individual list items may contain extra white space (like
 | 
				
			||||
     * in a list of varying length words).
 | 
				
			||||
     *
 | 
				
			||||
     * @see ItemDetailsLookup.ItemDetails#inDragRegion(MotionEvent)
 | 
				
			||||
     */
 | 
				
			||||
    public static final class NonDraggableArea extends BandPredicate {
 | 
				
			||||
 | 
				
			||||
        private final RecyclerView mRecyclerView;
 | 
				
			||||
        private final ItemDetailsLookup mDetailsLookup;
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Creates a new instance.
 | 
				
			||||
         *
 | 
				
			||||
         * @param recyclerView the owner RecyclerView
 | 
				
			||||
         * @param detailsLookup provides access to item details.
 | 
				
			||||
         */
 | 
				
			||||
        public NonDraggableArea(
 | 
				
			||||
                @NonNull RecyclerView recyclerView, @NonNull ItemDetailsLookup detailsLookup) {
 | 
				
			||||
 | 
				
			||||
            checkArgument(recyclerView != null);
 | 
				
			||||
            checkArgument(detailsLookup != null);
 | 
				
			||||
 | 
				
			||||
            mRecyclerView = recyclerView;
 | 
				
			||||
            mDetailsLookup = detailsLookup;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public boolean canInitiate(@NonNull MotionEvent e) {
 | 
				
			||||
            if (!hasSupportedLayoutManager(mRecyclerView)
 | 
				
			||||
                    || mRecyclerView.hasPendingAdapterUpdates()) {
 | 
				
			||||
                return false;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Nullable ItemDetailsLookup.ItemDetails details = mDetailsLookup.getItemDetails(e);
 | 
				
			||||
            return (details == null) || !details.inDragRegion(e);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,356 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
import static androidx.core.util.Preconditions.checkState;
 | 
				
			||||
import static androidx.recyclerview.selection.Shared.VERBOSE;
 | 
				
			||||
 | 
				
			||||
import android.graphics.Point;
 | 
				
			||||
import android.graphics.Rect;
 | 
				
			||||
import android.util.Log;
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.DrawableRes;
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.annotation.VisibleForTesting;
 | 
				
			||||
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
 | 
				
			||||
 | 
				
			||||
import java.util.Set;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Provides mouse driven band-selection support when used in conjunction with a {@link RecyclerView}
 | 
				
			||||
 * instance. This class is responsible for rendering a band overlay and manipulating selection
 | 
				
			||||
 * status of the items it intersects with.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Given the recycling nature of RecyclerView items that have scrolled off-screen would not
 | 
				
			||||
 * be selectable with a band that itself was partially rendered off-screen. To address this,
 | 
				
			||||
 * BandSelectionController builds a model of the list/grid information presented by RecyclerView as
 | 
				
			||||
 * the user interacts with items using their pointer (and the band). Selectable items that intersect
 | 
				
			||||
 * with the band, both on and off screen, are selected on pointer up.
 | 
				
			||||
 *
 | 
				
			||||
 * @see SelectionTracker.Builder#withPointerTooltypes(int...) for details on the specific
 | 
				
			||||
 *     tooltypes routed to this helper.
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
 */
 | 
				
			||||
class BandSelectionHelper<K> implements OnItemTouchListener {
 | 
				
			||||
 | 
				
			||||
    static final String TAG = "BandSelectionHelper";
 | 
				
			||||
    static final boolean DEBUG = false;
 | 
				
			||||
 | 
				
			||||
    private final BandHost mHost;
 | 
				
			||||
    private final ItemKeyProvider<K> mKeyProvider;
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    final SelectionTracker<K> mSelectionTracker;
 | 
				
			||||
    private final BandPredicate mBandPredicate;
 | 
				
			||||
    private final FocusDelegate<K> mFocusDelegate;
 | 
				
			||||
    private final OperationMonitor mLock;
 | 
				
			||||
    private final AutoScroller mScroller;
 | 
				
			||||
    private final GridModel.SelectionObserver mGridObserver;
 | 
				
			||||
 | 
				
			||||
    private @Nullable Point mCurrentPosition;
 | 
				
			||||
    private @Nullable Point mOrigin;
 | 
				
			||||
    private @Nullable GridModel mModel;
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * See {@link BandSelectionHelper#create}.
 | 
				
			||||
     */
 | 
				
			||||
    BandSelectionHelper(
 | 
				
			||||
            @NonNull BandHost host,
 | 
				
			||||
            @NonNull AutoScroller scroller,
 | 
				
			||||
            @NonNull ItemKeyProvider<K> keyProvider,
 | 
				
			||||
            @NonNull SelectionTracker<K> selectionTracker,
 | 
				
			||||
            @NonNull BandPredicate bandPredicate,
 | 
				
			||||
            @NonNull FocusDelegate<K> focusDelegate,
 | 
				
			||||
            @NonNull OperationMonitor lock) {
 | 
				
			||||
 | 
				
			||||
        checkArgument(host != null);
 | 
				
			||||
        checkArgument(scroller != null);
 | 
				
			||||
        checkArgument(keyProvider != null);
 | 
				
			||||
        checkArgument(selectionTracker != null);
 | 
				
			||||
        checkArgument(bandPredicate != null);
 | 
				
			||||
        checkArgument(focusDelegate != null);
 | 
				
			||||
        checkArgument(lock != null);
 | 
				
			||||
 | 
				
			||||
        mHost = host;
 | 
				
			||||
        mKeyProvider = keyProvider;
 | 
				
			||||
        mSelectionTracker = selectionTracker;
 | 
				
			||||
        mBandPredicate = bandPredicate;
 | 
				
			||||
        mFocusDelegate = focusDelegate;
 | 
				
			||||
        mLock = lock;
 | 
				
			||||
 | 
				
			||||
        mHost.addOnScrollListener(
 | 
				
			||||
                new OnScrollListener() {
 | 
				
			||||
                    @Override
 | 
				
			||||
                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
 | 
				
			||||
                        BandSelectionHelper.this.onScrolled(recyclerView, dx, dy);
 | 
				
			||||
                    }
 | 
				
			||||
                });
 | 
				
			||||
 | 
				
			||||
        mScroller = scroller;
 | 
				
			||||
 | 
				
			||||
        mGridObserver = new GridModel.SelectionObserver<K>() {
 | 
				
			||||
            @Override
 | 
				
			||||
            public void onSelectionChanged(Set<K> updatedSelection) {
 | 
				
			||||
                mSelectionTracker.setProvisionalSelection(updatedSelection);
 | 
				
			||||
            }
 | 
				
			||||
        };
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Creates a new instance.
 | 
				
			||||
     *
 | 
				
			||||
     * @return new BandSelectionHelper instance.
 | 
				
			||||
     */
 | 
				
			||||
    static <K> BandSelectionHelper create(
 | 
				
			||||
            @NonNull RecyclerView recyclerView,
 | 
				
			||||
            @NonNull AutoScroller scroller,
 | 
				
			||||
            @DrawableRes int bandOverlayId,
 | 
				
			||||
            @NonNull ItemKeyProvider<K> keyProvider,
 | 
				
			||||
            @NonNull SelectionTracker<K> selectionTracker,
 | 
				
			||||
            @NonNull SelectionPredicate<K> selectionPredicate,
 | 
				
			||||
            @NonNull BandPredicate bandPredicate,
 | 
				
			||||
            @NonNull FocusDelegate<K> focusDelegate,
 | 
				
			||||
            @NonNull OperationMonitor lock) {
 | 
				
			||||
 | 
				
			||||
        return new BandSelectionHelper<>(
 | 
				
			||||
                new DefaultBandHost<>(recyclerView, bandOverlayId, keyProvider, selectionPredicate),
 | 
				
			||||
                scroller,
 | 
				
			||||
                keyProvider,
 | 
				
			||||
                selectionTracker,
 | 
				
			||||
                bandPredicate,
 | 
				
			||||
                focusDelegate,
 | 
				
			||||
                lock);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @VisibleForTesting
 | 
				
			||||
    boolean isActive() {
 | 
				
			||||
        boolean active = mModel != null;
 | 
				
			||||
        if (DEBUG && active) {
 | 
				
			||||
            mLock.checkStarted();
 | 
				
			||||
        }
 | 
				
			||||
        return active;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Clients must call reset when there are any material changes to the layout of items
 | 
				
			||||
     * in RecyclerView.
 | 
				
			||||
     */
 | 
				
			||||
    void reset() {
 | 
				
			||||
        if (!isActive()) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        mHost.hideBand();
 | 
				
			||||
        if (mModel != null) {
 | 
				
			||||
            mModel.stopCapturing();
 | 
				
			||||
            mModel.onDestroy();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        mModel = null;
 | 
				
			||||
        mOrigin = null;
 | 
				
			||||
 | 
				
			||||
        mScroller.reset();
 | 
				
			||||
        mLock.stop();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @VisibleForTesting
 | 
				
			||||
    boolean shouldStart(@NonNull MotionEvent e) {
 | 
				
			||||
        // b/30146357 && b/23793622. onInterceptTouchEvent does not dispatch events to onTouchEvent
 | 
				
			||||
        // unless the event is != ACTION_DOWN. Thus, we need to actually start band selection when
 | 
				
			||||
        // mouse moves.
 | 
				
			||||
        return MotionEvents.isPrimaryMouseButtonPressed(e)
 | 
				
			||||
                && MotionEvents.isActionMove(e)
 | 
				
			||||
                && mBandPredicate.canInitiate(e)
 | 
				
			||||
                && !isActive();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @VisibleForTesting
 | 
				
			||||
    boolean shouldStop(@NonNull MotionEvent e) {
 | 
				
			||||
        return isActive()
 | 
				
			||||
                && (MotionEvents.isActionUp(e)
 | 
				
			||||
                || MotionEvents.isActionPointerUp(e)
 | 
				
			||||
                || MotionEvents.isActionCancel(e));
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
 | 
				
			||||
        if (shouldStart(e)) {
 | 
				
			||||
            startBandSelect(e);
 | 
				
			||||
        } else if (shouldStop(e)) {
 | 
				
			||||
            endBandSelect();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        return isActive();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
 | 
				
			||||
     */
 | 
				
			||||
    @Override
 | 
				
			||||
    public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
 | 
				
			||||
        if (shouldStop(e)) {
 | 
				
			||||
            endBandSelect();
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // We shouldn't get any events in this method when band select is not active,
 | 
				
			||||
        // but it turns some guests show up late to the party.
 | 
				
			||||
        // Probably happening when a re-layout is happening to the ReyclerView (ie. Pull-To-Refresh)
 | 
				
			||||
        if (!isActive()) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (DEBUG) {
 | 
				
			||||
            checkArgument(MotionEvents.isActionMove(e));
 | 
				
			||||
            checkState(mModel != null);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        mCurrentPosition = MotionEvents.getOrigin(e);
 | 
				
			||||
 | 
				
			||||
        mModel.resizeSelection(mCurrentPosition);
 | 
				
			||||
 | 
				
			||||
        resizeBand();
 | 
				
			||||
        mScroller.scroll(mCurrentPosition);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Starts band select by adding the drawable to the RecyclerView's overlay.
 | 
				
			||||
     */
 | 
				
			||||
    private void startBandSelect(@NonNull MotionEvent e) {
 | 
				
			||||
        checkState(!isActive());
 | 
				
			||||
 | 
				
			||||
        if (!MotionEvents.isCtrlKeyPressed(e)) {
 | 
				
			||||
            mSelectionTracker.clearSelection();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        Point origin = MotionEvents.getOrigin(e);
 | 
				
			||||
        if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
 | 
				
			||||
 | 
				
			||||
        mModel = mHost.createGridModel();
 | 
				
			||||
        mModel.addOnSelectionChangedListener(mGridObserver);
 | 
				
			||||
 | 
				
			||||
        mLock.start();
 | 
				
			||||
        mFocusDelegate.clearFocus();
 | 
				
			||||
        mOrigin = origin;
 | 
				
			||||
        // NOTE: Pay heed that resizeBand modifies the y coordinates
 | 
				
			||||
        // in onScrolled. Not sure if model expects this. If not
 | 
				
			||||
        // it should be defending against this.
 | 
				
			||||
        mModel.startCapturing(mOrigin);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Resizes the band select rectangle by using the origin and the current pointer position as
 | 
				
			||||
     * two opposite corners of the selection.
 | 
				
			||||
     */
 | 
				
			||||
    private void resizeBand() {
 | 
				
			||||
        Rect bounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
 | 
				
			||||
                Math.min(mOrigin.y, mCurrentPosition.y),
 | 
				
			||||
                Math.max(mOrigin.x, mCurrentPosition.x),
 | 
				
			||||
                Math.max(mOrigin.y, mCurrentPosition.y));
 | 
				
			||||
 | 
				
			||||
        if (VERBOSE) Log.v(TAG, "Resizing band! " + bounds);
 | 
				
			||||
        mHost.showBand(bounds);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Ends band select by removing the overlay.
 | 
				
			||||
     */
 | 
				
			||||
    private void endBandSelect() {
 | 
				
			||||
        if (DEBUG) {
 | 
				
			||||
            Log.d(TAG, "Ending band select.");
 | 
				
			||||
            checkState(mModel != null);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // TODO: Currently when a band select operation ends outside
 | 
				
			||||
        // of an item (e.g. in the empty area between items),
 | 
				
			||||
        // getPositionNearestOrigin may return an unselected item.
 | 
				
			||||
        // Since the point of this code is to establish the
 | 
				
			||||
        // anchor point for subsequent range operations (SHIFT+CLICK)
 | 
				
			||||
        // we really want to do a better job figuring out the last
 | 
				
			||||
        // item selected (and nearest to the cursor).
 | 
				
			||||
        int firstSelected = mModel.getPositionNearestOrigin();
 | 
				
			||||
        if (firstSelected != GridModel.NOT_SET
 | 
				
			||||
                && mSelectionTracker.isSelected(mKeyProvider.getKey(firstSelected))) {
 | 
				
			||||
            // Establish the band selection point as range anchor. This
 | 
				
			||||
            // allows touch and keyboard based selection activities
 | 
				
			||||
            // to be based on the band selection anchor point.
 | 
				
			||||
            mSelectionTracker.anchorRange(firstSelected);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        mSelectionTracker.mergeProvisionalSelection();
 | 
				
			||||
        reset();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @see OnScrollListener
 | 
				
			||||
     */
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
 | 
				
			||||
        if (!isActive()) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // 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 -= dy;
 | 
				
			||||
        resizeBand();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Provides functionality for BandController. Exists primarily to tests that are
 | 
				
			||||
     * fully isolated from RecyclerView.
 | 
				
			||||
     *
 | 
				
			||||
     * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
     */
 | 
				
			||||
    abstract static class BandHost<K> {
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Returns a new GridModel instance.
 | 
				
			||||
         */
 | 
				
			||||
        abstract GridModel<K> createGridModel();
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Show the band covering the bounds.
 | 
				
			||||
         *
 | 
				
			||||
         * @param bounds The boundaries of the band to show.
 | 
				
			||||
         */
 | 
				
			||||
        abstract void showBand(@NonNull Rect bounds);
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Hide the band.
 | 
				
			||||
         */
 | 
				
			||||
        abstract void hideBand();
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Add a listener to be notified on scroll events.
 | 
				
			||||
         *
 | 
				
			||||
         * @param listener
 | 
				
			||||
         */
 | 
				
			||||
        abstract void addOnScrollListener(@NonNull OnScrollListener listener);
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,156 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
 | 
				
			||||
import android.graphics.Canvas;
 | 
				
			||||
import android.graphics.Point;
 | 
				
			||||
import android.graphics.Rect;
 | 
				
			||||
import android.graphics.drawable.Drawable;
 | 
				
			||||
import android.view.View;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.DrawableRes;
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
 | 
				
			||||
import androidx.recyclerview.widget.GridLayoutManager;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * RecyclerView backed {@link BandSelectionHelper.BandHost}.
 | 
				
			||||
 */
 | 
				
			||||
final class DefaultBandHost<K> extends GridModel.GridHost<K> {
 | 
				
			||||
 | 
				
			||||
    private static final Rect NILL_RECT = new Rect(0, 0, 0, 0);
 | 
				
			||||
 | 
				
			||||
    private final RecyclerView mRecyclerView;
 | 
				
			||||
    private final Drawable mBand;
 | 
				
			||||
    private final ItemKeyProvider<K> mKeyProvider;
 | 
				
			||||
    private final SelectionPredicate<K> mSelectionPredicate;
 | 
				
			||||
 | 
				
			||||
    DefaultBandHost(
 | 
				
			||||
            @NonNull RecyclerView recyclerView,
 | 
				
			||||
            @DrawableRes int bandOverlayId,
 | 
				
			||||
            @NonNull ItemKeyProvider<K> keyProvider,
 | 
				
			||||
            @NonNull SelectionPredicate<K> selectionPredicate) {
 | 
				
			||||
 | 
				
			||||
        checkArgument(recyclerView != null);
 | 
				
			||||
 | 
				
			||||
        mRecyclerView = recyclerView;
 | 
				
			||||
        mBand = mRecyclerView.getContext().getResources().getDrawable(bandOverlayId);
 | 
				
			||||
 | 
				
			||||
        checkArgument(mBand != null);
 | 
				
			||||
        checkArgument(keyProvider != null);
 | 
				
			||||
        checkArgument(selectionPredicate != null);
 | 
				
			||||
 | 
				
			||||
        mKeyProvider = keyProvider;
 | 
				
			||||
        mSelectionPredicate = selectionPredicate;
 | 
				
			||||
 | 
				
			||||
        mRecyclerView.addItemDecoration(
 | 
				
			||||
                new ItemDecoration() {
 | 
				
			||||
                    @Override
 | 
				
			||||
                    public void onDrawOver(
 | 
				
			||||
                            Canvas canvas,
 | 
				
			||||
                            RecyclerView unusedParent,
 | 
				
			||||
                            RecyclerView.State unusedState) {
 | 
				
			||||
                        DefaultBandHost.this.onDrawBand(canvas);
 | 
				
			||||
                    }
 | 
				
			||||
                });
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    GridModel<K> createGridModel() {
 | 
				
			||||
        return new GridModel<>(this, mKeyProvider, mSelectionPredicate);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    int getAdapterPositionAt(int index) {
 | 
				
			||||
        return mRecyclerView.getChildAdapterPosition(mRecyclerView.getChildAt(index));
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    void addOnScrollListener(@NonNull OnScrollListener listener) {
 | 
				
			||||
        mRecyclerView.addOnScrollListener(listener);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    void removeOnScrollListener(@NonNull OnScrollListener listener) {
 | 
				
			||||
        mRecyclerView.removeOnScrollListener(listener);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    Point createAbsolutePoint(@NonNull Point relativePoint) {
 | 
				
			||||
        return new Point(relativePoint.x + mRecyclerView.computeHorizontalScrollOffset(),
 | 
				
			||||
                relativePoint.y + mRecyclerView.computeVerticalScrollOffset());
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    Rect getAbsoluteRectForChildViewAt(int index) {
 | 
				
			||||
        final View child = mRecyclerView.getChildAt(index);
 | 
				
			||||
        final Rect childRect = new Rect();
 | 
				
			||||
        child.getHitRect(childRect);
 | 
				
			||||
        childRect.left += mRecyclerView.computeHorizontalScrollOffset();
 | 
				
			||||
        childRect.right += mRecyclerView.computeHorizontalScrollOffset();
 | 
				
			||||
        childRect.top += mRecyclerView.computeVerticalScrollOffset();
 | 
				
			||||
        childRect.bottom += mRecyclerView.computeVerticalScrollOffset();
 | 
				
			||||
        return childRect;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    int getVisibleChildCount() {
 | 
				
			||||
        return mRecyclerView.getChildCount();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    int getColumnCount() {
 | 
				
			||||
        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
 | 
				
			||||
        if (layoutManager instanceof GridLayoutManager) {
 | 
				
			||||
            return ((GridLayoutManager) layoutManager).getSpanCount();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // Otherwise, it is a list with 1 column.
 | 
				
			||||
        return 1;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    void showBand(@NonNull Rect rect) {
 | 
				
			||||
        mBand.setBounds(rect);
 | 
				
			||||
        // TODO: mRecyclerView.invalidateItemDecorations() should work, but it isn't currently.
 | 
				
			||||
        // NOTE: That without invalidating rv, the band only gets updated
 | 
				
			||||
        // when the pointer moves off a the item view into "NO_POSITION" territory.
 | 
				
			||||
        mRecyclerView.invalidate();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    void hideBand() {
 | 
				
			||||
        mBand.setBounds(NILL_RECT);
 | 
				
			||||
        // TODO: mRecyclerView.invalidateItemDecorations() should work, but it isn't currently.
 | 
				
			||||
        mRecyclerView.invalidate();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    void onDrawBand(@NonNull Canvas c) {
 | 
				
			||||
        mBand.draw(c);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    boolean hasView(int pos) {
 | 
				
			||||
        return mRecyclerView.findViewHolderForAdapterPosition(pos) != null;
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,589 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
import static androidx.core.util.Preconditions.checkState;
 | 
				
			||||
import static androidx.recyclerview.selection.Shared.DEBUG;
 | 
				
			||||
 | 
				
			||||
import android.os.Bundle;
 | 
				
			||||
import android.util.Log;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.annotation.RestrictTo;
 | 
				
			||||
import androidx.annotation.VisibleForTesting;
 | 
				
			||||
import androidx.recyclerview.selection.Range.RangeType;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
 | 
				
			||||
 | 
				
			||||
import java.util.ArrayList;
 | 
				
			||||
import java.util.List;
 | 
				
			||||
import java.util.Map;
 | 
				
			||||
import java.util.Set;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * {@link SelectionTracker} providing support for traditional multi-item selection on top
 | 
				
			||||
 * of {@link RecyclerView}.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * The class supports running in a single-select mode, which can be enabled using
 | 
				
			||||
 * {@link SelectionPredicate#canSelectMultiple()}.
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
 *
 | 
				
			||||
 * @hide
 | 
				
			||||
 */
 | 
				
			||||
@RestrictTo(LIBRARY_GROUP)
 | 
				
			||||
public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
 | 
				
			||||
 | 
				
			||||
    private static final String TAG = "DefaultSelectionTracker";
 | 
				
			||||
    private static final String EXTRA_SELECTION_PREFIX = "androidx.recyclerview.selection";
 | 
				
			||||
 | 
				
			||||
    private final Selection<K> mSelection = new Selection<>();
 | 
				
			||||
    private final List<SelectionObserver> mObservers = new ArrayList<>(1);
 | 
				
			||||
    private final ItemKeyProvider<K> mKeyProvider;
 | 
				
			||||
    private final SelectionPredicate<K> mSelectionPredicate;
 | 
				
			||||
    private final StorageStrategy<K> mStorage;
 | 
				
			||||
    private final RangeCallbacks mRangeCallbacks;
 | 
				
			||||
    private final AdapterObserver mAdapterObserver;
 | 
				
			||||
    private final boolean mSingleSelect;
 | 
				
			||||
    private final String mSelectionId;
 | 
				
			||||
 | 
				
			||||
    private @Nullable Range mRange;
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Creates a new instance.
 | 
				
			||||
     *
 | 
				
			||||
     * @param selectionId A unique string identifying this selection in the context
 | 
				
			||||
     *        of the activity or fragment.
 | 
				
			||||
     * @param keyProvider client supplied class providing access to stable ids.
 | 
				
			||||
     * @param selectionPredicate A predicate allowing the client to disallow selection
 | 
				
			||||
     * @param storage Strategy for storing typed selection in bundle.
 | 
				
			||||
     */
 | 
				
			||||
    public DefaultSelectionTracker(
 | 
				
			||||
            @NonNull String selectionId,
 | 
				
			||||
            @NonNull ItemKeyProvider keyProvider,
 | 
				
			||||
            @NonNull SelectionPredicate selectionPredicate,
 | 
				
			||||
            @NonNull StorageStrategy<K> storage) {
 | 
				
			||||
 | 
				
			||||
        checkArgument(selectionId != null);
 | 
				
			||||
        checkArgument(!selectionId.trim().isEmpty());
 | 
				
			||||
        checkArgument(keyProvider != null);
 | 
				
			||||
        checkArgument(selectionPredicate != null);
 | 
				
			||||
        checkArgument(storage != null);
 | 
				
			||||
 | 
				
			||||
        mSelectionId = selectionId;
 | 
				
			||||
        mKeyProvider = keyProvider;
 | 
				
			||||
        mSelectionPredicate = selectionPredicate;
 | 
				
			||||
        mStorage = storage;
 | 
				
			||||
 | 
				
			||||
        mRangeCallbacks = new RangeCallbacks();
 | 
				
			||||
 | 
				
			||||
        mSingleSelect = !selectionPredicate.canSelectMultiple();
 | 
				
			||||
 | 
				
			||||
        mAdapterObserver = new AdapterObserver(this);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void addObserver(@NonNull SelectionObserver callback) {
 | 
				
			||||
        checkArgument(callback != null);
 | 
				
			||||
        mObservers.add(callback);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean hasSelection() {
 | 
				
			||||
        return !mSelection.isEmpty();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public Selection getSelection() {
 | 
				
			||||
        return mSelection;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void copySelection(@NonNull MutableSelection dest) {
 | 
				
			||||
        dest.copyFrom(mSelection);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean isSelected(@Nullable K key) {
 | 
				
			||||
        return mSelection.contains(key);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    protected void restoreSelection(@NonNull Selection other) {
 | 
				
			||||
        checkArgument(other != null);
 | 
				
			||||
        setItemsSelectedQuietly(other.mSelection, true);
 | 
				
			||||
        // NOTE: We intentionally don't restore provisional selection. It's provisional.
 | 
				
			||||
        notifySelectionRestored();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean setItemsSelected(@NonNull Iterable<K> keys, boolean selected) {
 | 
				
			||||
        boolean changed = setItemsSelectedQuietly(keys, selected);
 | 
				
			||||
        notifySelectionChanged();
 | 
				
			||||
        return changed;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private boolean setItemsSelectedQuietly(@NonNull Iterable<K> keys, boolean selected) {
 | 
				
			||||
        boolean changed = false;
 | 
				
			||||
        for (K key: keys) {
 | 
				
			||||
            boolean itemChanged = selected
 | 
				
			||||
                    ? canSetState(key, true) && mSelection.add(key)
 | 
				
			||||
                    : canSetState(key, false) && mSelection.remove(key);
 | 
				
			||||
            if (itemChanged) {
 | 
				
			||||
                notifyItemStateChanged(key, selected);
 | 
				
			||||
            }
 | 
				
			||||
            changed |= itemChanged;
 | 
				
			||||
        }
 | 
				
			||||
        return changed;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean clearSelection() {
 | 
				
			||||
        if (!hasSelection()) {
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        clearProvisionalSelection();
 | 
				
			||||
        clearPrimarySelection();
 | 
				
			||||
        return true;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private void clearPrimarySelection() {
 | 
				
			||||
        if (!hasSelection()) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        Selection prev = clearSelectionQuietly();
 | 
				
			||||
        notifySelectionCleared(prev);
 | 
				
			||||
        notifySelectionChanged();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Clears the selection, without notifying selection listeners.
 | 
				
			||||
     * Returns items in previous selection. Callers are responsible for notifying
 | 
				
			||||
     * listeners about changes.
 | 
				
			||||
     */
 | 
				
			||||
    private Selection clearSelectionQuietly() {
 | 
				
			||||
        mRange = null;
 | 
				
			||||
 | 
				
			||||
        MutableSelection prevSelection = new MutableSelection();
 | 
				
			||||
        if (hasSelection()) {
 | 
				
			||||
            copySelection(prevSelection);
 | 
				
			||||
            mSelection.clear();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        return prevSelection;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean select(@NonNull K key) {
 | 
				
			||||
        checkArgument(key != null);
 | 
				
			||||
 | 
				
			||||
        if (mSelection.contains(key)) {
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (!canSetState(key, true)) {
 | 
				
			||||
            if (DEBUG) Log.d(TAG, "Select cancelled by selection predicate test.");
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // Enforce single selection policy.
 | 
				
			||||
        if (mSingleSelect && hasSelection()) {
 | 
				
			||||
            Selection prev = clearSelectionQuietly();
 | 
				
			||||
            notifySelectionCleared(prev);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        mSelection.add(key);
 | 
				
			||||
        notifyItemStateChanged(key, true);
 | 
				
			||||
        notifySelectionChanged();
 | 
				
			||||
 | 
				
			||||
        return true;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean deselect(@NonNull K key) {
 | 
				
			||||
        checkArgument(key != null);
 | 
				
			||||
 | 
				
			||||
        if (mSelection.contains(key)) {
 | 
				
			||||
            if (!canSetState(key, false)) {
 | 
				
			||||
                if (DEBUG) Log.d(TAG, "Deselect cancelled by selection predicate test.");
 | 
				
			||||
                return false;
 | 
				
			||||
            }
 | 
				
			||||
            mSelection.remove(key);
 | 
				
			||||
            notifyItemStateChanged(key, false);
 | 
				
			||||
            notifySelectionChanged();
 | 
				
			||||
            if (mSelection.isEmpty() && isRangeActive()) {
 | 
				
			||||
                // if there's nothing in the selection and there is an active ranger it results
 | 
				
			||||
                // in unexpected behavior when the user tries to start range selection: the item
 | 
				
			||||
                // which the ranger 'thinks' is the already selected anchor becomes unselectable
 | 
				
			||||
                endRange();
 | 
				
			||||
            }
 | 
				
			||||
            return true;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        return false;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void startRange(int position) {
 | 
				
			||||
        if (mSelection.contains(mKeyProvider.getKey(position))
 | 
				
			||||
                || select(mKeyProvider.getKey(position))) {
 | 
				
			||||
            anchorRange(position);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void extendRange(int position) {
 | 
				
			||||
        extendRange(position, Range.TYPE_PRIMARY);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void endRange() {
 | 
				
			||||
        mRange = null;
 | 
				
			||||
        // Clean up in case there was any leftover provisional selection
 | 
				
			||||
        clearProvisionalSelection();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void anchorRange(int position) {
 | 
				
			||||
        checkArgument(position != RecyclerView.NO_POSITION);
 | 
				
			||||
        checkArgument(mSelection.contains(mKeyProvider.getKey(position)));
 | 
				
			||||
 | 
				
			||||
        mRange = new Range(position, mRangeCallbacks);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void extendProvisionalRange(int position) {
 | 
				
			||||
        if (mSingleSelect) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (DEBUG) Log.i(TAG, "Extending provision range to position: " + position);
 | 
				
			||||
        checkState(isRangeActive(), "Range start point not set.");
 | 
				
			||||
        extendRange(position, Range.TYPE_PROVISIONAL);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Sets the end point for the current range selection, started by a call to
 | 
				
			||||
     * {@link #startRange(int)}. This function should only be called when a range selection
 | 
				
			||||
     * is active (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
 | 
				
			||||
     * selected or in provisional select, depending on the type supplied. Note that if the type is
 | 
				
			||||
     * provisional selection, one should do {@link #mergeProvisionalSelection()} at some
 | 
				
			||||
     * point before calling on {@link #endRange()}.
 | 
				
			||||
     *
 | 
				
			||||
     * @param position The new end position for the selection range.
 | 
				
			||||
     * @param type The type of selection the range should utilize.
 | 
				
			||||
     */
 | 
				
			||||
    private void extendRange(int position, @RangeType int type) {
 | 
				
			||||
        checkState(isRangeActive(), "Range start point not set.");
 | 
				
			||||
 | 
				
			||||
        mRange.extendRange(position, type);
 | 
				
			||||
 | 
				
			||||
        // We're being lazy here notifying even when something might not have changed.
 | 
				
			||||
        // To make this more correct, we'd need to update the Ranger class to return
 | 
				
			||||
        // information about what has changed.
 | 
				
			||||
        notifySelectionChanged();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void setProvisionalSelection(@NonNull Set<K> newSelection) {
 | 
				
			||||
        if (mSingleSelect) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        Map<K, Boolean> delta = mSelection.setProvisionalSelection(newSelection);
 | 
				
			||||
        for (Map.Entry<K, Boolean> entry: delta.entrySet()) {
 | 
				
			||||
            notifyItemStateChanged(entry.getKey(), entry.getValue());
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        notifySelectionChanged();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void mergeProvisionalSelection() {
 | 
				
			||||
        mSelection.mergeProvisionalSelection();
 | 
				
			||||
 | 
				
			||||
        // Note, that for almost all functional purposes, merging a provisional selection
 | 
				
			||||
        // into a the primary selection doesn't change the selection, just an internal
 | 
				
			||||
        // representation of it. But there are some nuanced areas cases where
 | 
				
			||||
        // that isn't true. equality for 1. So, we notify regardless.
 | 
				
			||||
 | 
				
			||||
        notifySelectionChanged();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void clearProvisionalSelection() {
 | 
				
			||||
        for (K key : mSelection.mProvisionalSelection) {
 | 
				
			||||
            notifyItemStateChanged(key, false);
 | 
				
			||||
        }
 | 
				
			||||
        mSelection.clearProvisionalSelection();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean isRangeActive() {
 | 
				
			||||
        return mRange != null;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private boolean canSetState(@NonNull K key, boolean nextState) {
 | 
				
			||||
        return mSelectionPredicate.canSetStateForKey(key, nextState);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    AdapterDataObserver getAdapterDataObserver() {
 | 
				
			||||
        return mAdapterObserver;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    void onDataSetChanged() {
 | 
				
			||||
        mSelection.clearProvisionalSelection();
 | 
				
			||||
 | 
				
			||||
        notifySelectionRefresh();
 | 
				
			||||
 | 
				
			||||
        List<K> toRemove = null;
 | 
				
			||||
        for (K key : mSelection) {
 | 
				
			||||
            // If the underlying data set has changed, before restoring
 | 
				
			||||
            // selection we must re-verify that it can be selected.
 | 
				
			||||
            // Why? Because if the dataset has changed, then maybe the
 | 
				
			||||
            // selectability of an item has changed.
 | 
				
			||||
            if (!canSetState(key, true)) {
 | 
				
			||||
                if (toRemove == null) {
 | 
				
			||||
                    toRemove = new ArrayList<>();
 | 
				
			||||
                }
 | 
				
			||||
                toRemove.add(key);
 | 
				
			||||
            } else {
 | 
				
			||||
                int lastListener = mObservers.size() - 1;
 | 
				
			||||
                for (int i = lastListener; i >= 0; i--) {
 | 
				
			||||
                    mObservers.get(i).onItemStateChanged(key, true);
 | 
				
			||||
                }
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (toRemove != null) {
 | 
				
			||||
            for (K key : toRemove) {
 | 
				
			||||
                deselect(key);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        notifySelectionChanged();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Notifies registered listeners when the selection status of a single item
 | 
				
			||||
     * (identified by {@code position}) changes.
 | 
				
			||||
     */
 | 
				
			||||
    private void notifyItemStateChanged(@NonNull K key, boolean selected) {
 | 
				
			||||
        checkArgument(key != null);
 | 
				
			||||
 | 
				
			||||
        int lastListenerIndex = mObservers.size() - 1;
 | 
				
			||||
        for (int i = lastListenerIndex; i >= 0; i--) {
 | 
				
			||||
            mObservers.get(i).onItemStateChanged(key, selected);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private void notifySelectionCleared(@NonNull Selection<K> selection) {
 | 
				
			||||
        for (K key: selection.mSelection) {
 | 
				
			||||
            notifyItemStateChanged(key, false);
 | 
				
			||||
        }
 | 
				
			||||
        for (K key: selection.mProvisionalSelection) {
 | 
				
			||||
            notifyItemStateChanged(key, false);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Notifies registered listeners when the selection has changed. This
 | 
				
			||||
     * notification should be sent only once a full series of changes
 | 
				
			||||
     * is complete, e.g. clearingSelection, or updating the single
 | 
				
			||||
     * selection from one item to another.
 | 
				
			||||
     */
 | 
				
			||||
    private void notifySelectionChanged() {
 | 
				
			||||
        int lastListenerIndex = mObservers.size() - 1;
 | 
				
			||||
        for (int i = lastListenerIndex; i >= 0; i--) {
 | 
				
			||||
            mObservers.get(i).onSelectionChanged();
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private void notifySelectionRestored() {
 | 
				
			||||
        int lastListenerIndex = mObservers.size() - 1;
 | 
				
			||||
        for (int i = lastListenerIndex; i >= 0; i--) {
 | 
				
			||||
            mObservers.get(i).onSelectionRestored();
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private void notifySelectionRefresh() {
 | 
				
			||||
        int lastListenerIndex = mObservers.size() - 1;
 | 
				
			||||
        for (int i = lastListenerIndex; i >= 0; i--) {
 | 
				
			||||
            mObservers.get(i).onSelectionRefresh();
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private void updateForRange(int begin, int end, boolean selected, @RangeType int type) {
 | 
				
			||||
        switch (type) {
 | 
				
			||||
            case Range.TYPE_PRIMARY:
 | 
				
			||||
                updateForRegularRange(begin, end, selected);
 | 
				
			||||
                break;
 | 
				
			||||
            case Range.TYPE_PROVISIONAL:
 | 
				
			||||
                updateForProvisionalRange(begin, end, selected);
 | 
				
			||||
                break;
 | 
				
			||||
            default:
 | 
				
			||||
                throw new IllegalArgumentException("Invalid range type: " + type);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    void updateForRegularRange(int begin, int end, boolean selected) {
 | 
				
			||||
        checkArgument(end >= begin);
 | 
				
			||||
 | 
				
			||||
        for (int i = begin; i <= end; i++) {
 | 
				
			||||
            K key = mKeyProvider.getKey(i);
 | 
				
			||||
            if (key == null) {
 | 
				
			||||
                continue;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            if (selected) {
 | 
				
			||||
                select(key);
 | 
				
			||||
            } else {
 | 
				
			||||
                deselect(key);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    void updateForProvisionalRange(int begin, int end, boolean selected) {
 | 
				
			||||
        checkArgument(end >= begin);
 | 
				
			||||
 | 
				
			||||
        for (int i = begin; i <= end; i++) {
 | 
				
			||||
            K key = mKeyProvider.getKey(i);
 | 
				
			||||
            if (key == null) {
 | 
				
			||||
                continue;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            boolean changedState = false;
 | 
				
			||||
            if (selected) {
 | 
				
			||||
                boolean canSelect = canSetState(key, true);
 | 
				
			||||
                if (canSelect && !mSelection.mSelection.contains(key)) {
 | 
				
			||||
                    mSelection.mProvisionalSelection.add(key);
 | 
				
			||||
                    changedState = true;
 | 
				
			||||
                }
 | 
				
			||||
            } else {
 | 
				
			||||
                mSelection.mProvisionalSelection.remove(key);
 | 
				
			||||
                changedState = true;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            // Only notify item callbacks when something's state is actually changed in provisional
 | 
				
			||||
            // selection.
 | 
				
			||||
            if (changedState) {
 | 
				
			||||
                notifyItemStateChanged(key, selected);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        notifySelectionChanged();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @VisibleForTesting
 | 
				
			||||
    String getInstanceStateKey() {
 | 
				
			||||
        return EXTRA_SELECTION_PREFIX + ":" + mSelectionId;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    @SuppressWarnings("unchecked")
 | 
				
			||||
    public final void onSaveInstanceState(@NonNull Bundle state) {
 | 
				
			||||
        if (mSelection.isEmpty()) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        state.putBundle(getInstanceStateKey(), mStorage.asBundle(mSelection));
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public final void onRestoreInstanceState(@Nullable Bundle state) {
 | 
				
			||||
        if (state == null) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Nullable Bundle selectionState = state.getBundle(getInstanceStateKey());
 | 
				
			||||
        if (selectionState == null) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        Selection<K> selection = mStorage.asSelection(selectionState);
 | 
				
			||||
        if (selection != null && !selection.isEmpty()) {
 | 
				
			||||
            restoreSelection(selection);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private final class RangeCallbacks extends Range.Callbacks {
 | 
				
			||||
        RangeCallbacks() {
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        void updateForRange(int begin, int end, boolean selected, int type) {
 | 
				
			||||
            switch (type) {
 | 
				
			||||
                case Range.TYPE_PRIMARY:
 | 
				
			||||
                    updateForRegularRange(begin, end, selected);
 | 
				
			||||
                    break;
 | 
				
			||||
                case Range.TYPE_PROVISIONAL:
 | 
				
			||||
                    updateForProvisionalRange(begin, end, selected);
 | 
				
			||||
                    break;
 | 
				
			||||
                default:
 | 
				
			||||
                    throw new IllegalArgumentException("Invalid range type: " + type);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private static final class AdapterObserver extends AdapterDataObserver {
 | 
				
			||||
 | 
				
			||||
        private final DefaultSelectionTracker<?> mSelectionTracker;
 | 
				
			||||
 | 
				
			||||
        AdapterObserver(@NonNull DefaultSelectionTracker<?> selectionTracker) {
 | 
				
			||||
            checkArgument(selectionTracker != null);
 | 
				
			||||
            mSelectionTracker = selectionTracker;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public void onChanged() {
 | 
				
			||||
            mSelectionTracker.onDataSetChanged();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public void onItemRangeChanged(int startPosition, int itemCount, @Nullable Object payload) {
 | 
				
			||||
            if (!SelectionTracker.SELECTION_CHANGED_MARKER.equals(payload)) {
 | 
				
			||||
                mSelectionTracker.onDataSetChanged();
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public void onItemRangeInserted(int startPosition, int itemCount) {
 | 
				
			||||
            mSelectionTracker.endRange();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public void onItemRangeRemoved(int startPosition, int itemCount) {
 | 
				
			||||
            mSelectionTracker.endRange();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
 | 
				
			||||
            mSelectionTracker.endRange();
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,105 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 | 
				
			||||
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
import static androidx.recyclerview.selection.Shared.VERBOSE;
 | 
				
			||||
 | 
				
			||||
import android.util.Log;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.RestrictTo;
 | 
				
			||||
import androidx.annotation.VisibleForTesting;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Provides the necessary glue to notify RecyclerView when selection data changes,
 | 
				
			||||
 * and to notify SelectionTracker when the underlying RecyclerView.Adapter data changes.
 | 
				
			||||
 *
 | 
				
			||||
 * This strict decoupling is necessary to permit a single SelectionTracker to work
 | 
				
			||||
 * with multiple RecyclerView instances. This may be necessary when multiple
 | 
				
			||||
 * different views of data are presented to the user.
 | 
				
			||||
 *
 | 
				
			||||
 * @hide
 | 
				
			||||
 */
 | 
				
			||||
@RestrictTo(LIBRARY_GROUP)
 | 
				
			||||
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
 | 
				
			||||
public class EventBridge {
 | 
				
			||||
 | 
				
			||||
    private static final String TAG = "EventsRelays";
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Installs the event bridge for on the supplied adapter/helper.
 | 
				
			||||
     *
 | 
				
			||||
     * @param adapter
 | 
				
			||||
     * @param selectionTracker
 | 
				
			||||
     * @param keyProvider
 | 
				
			||||
     *
 | 
				
			||||
     * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
     */
 | 
				
			||||
    public static <K> void install(
 | 
				
			||||
            @NonNull RecyclerView.Adapter<?> adapter,
 | 
				
			||||
            @NonNull SelectionTracker<K> selectionTracker,
 | 
				
			||||
            @NonNull ItemKeyProvider<K> keyProvider) {
 | 
				
			||||
 | 
				
			||||
        // setup bridges to relay selection and adapter events
 | 
				
			||||
        new TrackerToAdapterBridge<>(selectionTracker, keyProvider, adapter);
 | 
				
			||||
        adapter.registerAdapterDataObserver(selectionTracker.getAdapterDataObserver());
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private static final class TrackerToAdapterBridge<K>
 | 
				
			||||
            extends SelectionTracker.SelectionObserver<K> {
 | 
				
			||||
 | 
				
			||||
        private final ItemKeyProvider<K> mKeyProvider;
 | 
				
			||||
        private final RecyclerView.Adapter<?> mAdapter;
 | 
				
			||||
 | 
				
			||||
        TrackerToAdapterBridge(
 | 
				
			||||
                @NonNull SelectionTracker<K> selectionTracker,
 | 
				
			||||
                @NonNull ItemKeyProvider<K> keyProvider,
 | 
				
			||||
                @NonNull RecyclerView.Adapter<?> adapter) {
 | 
				
			||||
 | 
				
			||||
            selectionTracker.addObserver(this);
 | 
				
			||||
 | 
				
			||||
            checkArgument(keyProvider != null);
 | 
				
			||||
            checkArgument(adapter != null);
 | 
				
			||||
 | 
				
			||||
            mKeyProvider = keyProvider;
 | 
				
			||||
            mAdapter = adapter;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Called when state of an item has been changed.
 | 
				
			||||
         */
 | 
				
			||||
        @Override
 | 
				
			||||
        public void onItemStateChanged(@NonNull K key, boolean selected) {
 | 
				
			||||
            int position = mKeyProvider.getPosition(key);
 | 
				
			||||
            if (VERBOSE) Log.v(TAG, "ITEM " + key + " CHANGED at pos: " + position);
 | 
				
			||||
 | 
				
			||||
            if (position < 0) {
 | 
				
			||||
                Log.w(TAG, "Item change notification received for unknown item: " + key);
 | 
				
			||||
                return;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            mAdapter.notifyItemChanged(position, SelectionTracker.SELECTION_CHANGED_MARKER);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private EventBridge() {
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,72 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Override methods in this class to provide application specific behaviors
 | 
				
			||||
 * related to focusing item.
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
 */
 | 
				
			||||
public abstract class FocusDelegate<K> {
 | 
				
			||||
 | 
				
			||||
    static <K> FocusDelegate<K> dummy() {
 | 
				
			||||
        return new FocusDelegate<K>() {
 | 
				
			||||
            @Override
 | 
				
			||||
            public void focusItem(@NonNull ItemDetails<K> item) {
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Override
 | 
				
			||||
            public boolean hasFocusedItem() {
 | 
				
			||||
                return false;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Override
 | 
				
			||||
            public int getFocusedPosition() {
 | 
				
			||||
                return RecyclerView.NO_POSITION;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Override
 | 
				
			||||
            public void clearFocus() {
 | 
				
			||||
            }
 | 
				
			||||
        };
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * If environment supports focus, focus {@code item}.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract void focusItem(@NonNull ItemDetails<K> item);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return true if there is a focused item.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract boolean hasFocusedItem();
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return the position of the currently focused item, if any.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract int getFocusedPosition();
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * If the environment supports focus and something is focused, unfocus it.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract void clearFocus();
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,104 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
 | 
				
			||||
import android.view.GestureDetector.OnDoubleTapListener;
 | 
				
			||||
import android.view.GestureDetector.OnGestureListener;
 | 
				
			||||
import android.view.GestureDetector.SimpleOnGestureListener;
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * GestureRouter is responsible for routing gestures detected by a GestureDetector
 | 
				
			||||
 * to registered handlers. The primary function is to divide events by tool-type
 | 
				
			||||
 * allowing handlers to cleanly implement tool-type specific policies.
 | 
				
			||||
 *
 | 
				
			||||
 * @param <T> listener type. Must extend OnGestureListener & OnDoubleTapListener.
 | 
				
			||||
 */
 | 
				
			||||
final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
 | 
				
			||||
        implements OnGestureListener, OnDoubleTapListener {
 | 
				
			||||
 | 
				
			||||
    private final ToolHandlerRegistry<T> mDelegates;
 | 
				
			||||
 | 
				
			||||
    GestureRouter(@NonNull T defaultDelegate) {
 | 
				
			||||
        checkArgument(defaultDelegate != null);
 | 
				
			||||
        mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    GestureRouter() {
 | 
				
			||||
        this((T) new SimpleOnGestureListener());
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @param toolType
 | 
				
			||||
     * @param delegate the delegate, or null to unregister.
 | 
				
			||||
     */
 | 
				
			||||
    public void register(int toolType, @Nullable T delegate) {
 | 
				
			||||
        mDelegates.set(toolType, delegate);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
 | 
				
			||||
        return mDelegates.get(e).onSingleTapConfirmed(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onDoubleTap(@NonNull MotionEvent e) {
 | 
				
			||||
        return mDelegates.get(e).onDoubleTap(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onDoubleTapEvent(@NonNull MotionEvent e) {
 | 
				
			||||
        return mDelegates.get(e).onDoubleTapEvent(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onDown(@NonNull MotionEvent e) {
 | 
				
			||||
        return mDelegates.get(e).onDown(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void onShowPress(@NonNull MotionEvent e) {
 | 
				
			||||
        mDelegates.get(e).onShowPress(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onSingleTapUp(@NonNull MotionEvent e) {
 | 
				
			||||
        return mDelegates.get(e).onSingleTapUp(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
 | 
				
			||||
            float distanceX, float distanceY) {
 | 
				
			||||
        return mDelegates.get(e2).onScroll(e1, e2, distanceX, distanceY);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void onLongPress(@NonNull MotionEvent e) {
 | 
				
			||||
        mDelegates.get(e).onLongPress(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
 | 
				
			||||
            float velocityX, float velocityY) {
 | 
				
			||||
        return mDelegates.get(e2).onFling(e1, e2, velocityX, velocityY);
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,319 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
import static androidx.core.util.Preconditions.checkState;
 | 
				
			||||
 | 
				
			||||
import android.graphics.Point;
 | 
				
			||||
import android.util.Log;
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
import android.view.View;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.VisibleForTesting;
 | 
				
			||||
import androidx.core.view.ViewCompat;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * GestureSelectionHelper provides logic that interprets a combination
 | 
				
			||||
 * of motions and gestures in order to provide gesture driven selection support
 | 
				
			||||
 * when used in conjunction with RecyclerView and other classes in the ReyclerView
 | 
				
			||||
 * selection support package.
 | 
				
			||||
 */
 | 
				
			||||
final class GestureSelectionHelper implements OnItemTouchListener {
 | 
				
			||||
 | 
				
			||||
    private static final String TAG = "GestureSelectionHelper";
 | 
				
			||||
 | 
				
			||||
    private final SelectionTracker<?> mSelectionMgr;
 | 
				
			||||
    private final ItemDetailsLookup<?> mDetailsLookup;
 | 
				
			||||
    private final AutoScroller mScroller;
 | 
				
			||||
    private final ViewDelegate mView;
 | 
				
			||||
    private final OperationMonitor mLock;
 | 
				
			||||
 | 
				
			||||
    private int mLastStartedItemPos = RecyclerView.NO_POSITION;
 | 
				
			||||
    private boolean mStarted = false;
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * See {@link GestureSelectionHelper#create} for convenience
 | 
				
			||||
     * method.
 | 
				
			||||
     */
 | 
				
			||||
    GestureSelectionHelper(
 | 
				
			||||
            @NonNull SelectionTracker<?> selectionTracker,
 | 
				
			||||
            @NonNull ItemDetailsLookup<?> detailsLookup,
 | 
				
			||||
            @NonNull ViewDelegate view,
 | 
				
			||||
            @NonNull AutoScroller scroller,
 | 
				
			||||
            @NonNull OperationMonitor lock) {
 | 
				
			||||
 | 
				
			||||
        checkArgument(selectionTracker != null);
 | 
				
			||||
        checkArgument(detailsLookup != null);
 | 
				
			||||
        checkArgument(view != null);
 | 
				
			||||
        checkArgument(scroller != null);
 | 
				
			||||
        checkArgument(lock != null);
 | 
				
			||||
 | 
				
			||||
        mSelectionMgr = selectionTracker;
 | 
				
			||||
        mDetailsLookup = detailsLookup;
 | 
				
			||||
        mView = view;
 | 
				
			||||
        mScroller = scroller;
 | 
				
			||||
        mLock = lock;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Explicitly kicks off a gesture multi-select.
 | 
				
			||||
     */
 | 
				
			||||
    void start() {
 | 
				
			||||
        checkState(!mStarted);
 | 
				
			||||
        // See: b/70518185. It appears start() is being called via onLongPress
 | 
				
			||||
        // even though we never received an intial handleInterceptedDownEvent
 | 
				
			||||
        // where we would usually initialize mLastStartedItemPos.
 | 
				
			||||
        if (mLastStartedItemPos == RecyclerView.NO_POSITION) {
 | 
				
			||||
            Log.w(TAG, "Illegal state. Can't start without valid mLastStartedItemPos.");
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // Partner code in MotionInputHandler ensures items
 | 
				
			||||
        // are selected and range established prior to
 | 
				
			||||
        // start being called.
 | 
				
			||||
        // Verify the truth of that statement here
 | 
				
			||||
        // to make the implicit coupling less of a time bomb.
 | 
				
			||||
        checkState(mSelectionMgr.isRangeActive());
 | 
				
			||||
 | 
				
			||||
        mLock.checkStopped();
 | 
				
			||||
 | 
				
			||||
        mStarted = true;
 | 
				
			||||
        mLock.start();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    /** @hide */
 | 
				
			||||
    public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
 | 
				
			||||
        if (MotionEvents.isMouseEvent(e)) {
 | 
				
			||||
            if (Shared.DEBUG) Log.w(TAG, "Unexpected Mouse event. Check configuration.");
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // TODO(b/109808552): It seems that mLastStartedItemPos should likely be set as a method
 | 
				
			||||
        // parameter in start().
 | 
				
			||||
        if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
 | 
				
			||||
            if (mDetailsLookup.getItemDetails(e) != null) {
 | 
				
			||||
                mLastStartedItemPos = mView.getItemUnder(e);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // See handleTouch(MotionEvent) javadoc for explanation as to why this is correct.
 | 
				
			||||
        return handleTouch(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    /** @hide */
 | 
				
			||||
    public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
 | 
				
			||||
        // See handleTouch(MotionEvent) javadoc for explanation as to why this is correct.
 | 
				
			||||
        handleTouch(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * If selection has started, will handle all appropriate types of MotionEvents and will return
 | 
				
			||||
     * true if this OnItemTouchListener should start intercepting the rest of the MotionEvents.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>This code, and the fact that this method is used by both OnInterceptTouchEvent and
 | 
				
			||||
     * OnTouchEvent, is correct and valid because:
 | 
				
			||||
     * <ol>
 | 
				
			||||
     * <li>MotionEvents that aren't ACTION_DOWN are only ever passed to either onInterceptTouchEvent
 | 
				
			||||
     * or onTouchEvent; never to both.  The MotionEvents we are handling in this method are not
 | 
				
			||||
     * ACTION_DOWN, and therefore, its appropriate that both the onInterceptTouchEvent and
 | 
				
			||||
     * onTouchEvent code paths cross this method.
 | 
				
			||||
     * <li>This method returns true when we want to intercept MotionEvents.  OnInterceptTouchEvent
 | 
				
			||||
     * uses that information to determine its own return, and OnMotionEvent doesn't have a return
 | 
				
			||||
     * so this methods return value is irrelevant to it.
 | 
				
			||||
     * </ol>
 | 
				
			||||
     */
 | 
				
			||||
    private boolean handleTouch(MotionEvent e) {
 | 
				
			||||
        if (!mStarted) {
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        switch (e.getActionMasked()) {
 | 
				
			||||
            case MotionEvent.ACTION_MOVE:
 | 
				
			||||
                handleMoveEvent(e);
 | 
				
			||||
                return true;
 | 
				
			||||
            case MotionEvent.ACTION_UP:
 | 
				
			||||
                handleUpEvent();
 | 
				
			||||
                return true;
 | 
				
			||||
            case MotionEvent.ACTION_CANCEL:
 | 
				
			||||
                handleCancelEvent();
 | 
				
			||||
                return true;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        return false;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    /** @hide */
 | 
				
			||||
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    // Called when ACTION_UP event is to be handled.
 | 
				
			||||
    // Essentially, since this means all gesture movement is over, reset everything and apply
 | 
				
			||||
    // provisional selection.
 | 
				
			||||
    private void handleUpEvent() {
 | 
				
			||||
        mSelectionMgr.mergeProvisionalSelection();
 | 
				
			||||
        endSelection();
 | 
				
			||||
        if (mLastStartedItemPos != RecyclerView.NO_POSITION) {
 | 
				
			||||
            mSelectionMgr.startRange(mLastStartedItemPos);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    // Called when ACTION_CANCEL event is to be handled.
 | 
				
			||||
    // This means this gesture selection is aborted, so reset everything and abandon provisional
 | 
				
			||||
    // selection.
 | 
				
			||||
    private void handleCancelEvent() {
 | 
				
			||||
        mSelectionMgr.clearProvisionalSelection();
 | 
				
			||||
        endSelection();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private void endSelection() {
 | 
				
			||||
        checkState(mStarted);
 | 
				
			||||
 | 
				
			||||
        mLastStartedItemPos = RecyclerView.NO_POSITION;
 | 
				
			||||
        mStarted = false;
 | 
				
			||||
        mScroller.reset();
 | 
				
			||||
        mLock.stop();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    // Call when an intercepted ACTION_MOVE event is passed down.
 | 
				
			||||
    // At this point, we are sure user wants to gesture multi-select.
 | 
				
			||||
    private void handleMoveEvent(@NonNull MotionEvent e) {
 | 
				
			||||
        Point lastInterceptedPoint = MotionEvents.getOrigin(e);
 | 
				
			||||
 | 
				
			||||
        int lastGlidedItemPos = mView.getLastGlidedItemPosition(e);
 | 
				
			||||
        if (lastGlidedItemPos != RecyclerView.NO_POSITION) {
 | 
				
			||||
            extendSelection(lastGlidedItemPos);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        mScroller.scroll(lastInterceptedPoint);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    // It's possible for events to go over the top/bottom of the RecyclerView.
 | 
				
			||||
    // We want to get a Y-coordinate within the RecyclerView so we can find the childView underneath
 | 
				
			||||
    // correctly.
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    static float getInboundY(float max, float y) {
 | 
				
			||||
        if (y < 0f) {
 | 
				
			||||
            return 0f;
 | 
				
			||||
        } else if (y > max) {
 | 
				
			||||
            return max;
 | 
				
			||||
        }
 | 
				
			||||
        return y;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /* Given the end position, select everything in-between.
 | 
				
			||||
     * @param endPos  The adapter position of the end item.
 | 
				
			||||
     */
 | 
				
			||||
    private void extendSelection(int endPos) {
 | 
				
			||||
        mSelectionMgr.extendProvisionalRange(endPos);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Returns a new instance of GestureSelectionHelper.
 | 
				
			||||
     */
 | 
				
			||||
    static GestureSelectionHelper create(
 | 
				
			||||
            @NonNull SelectionTracker<?> selectionMgr,
 | 
				
			||||
            @NonNull ItemDetailsLookup<?> detailsLookup,
 | 
				
			||||
            @NonNull RecyclerView recyclerView,
 | 
				
			||||
            @NonNull AutoScroller scroller,
 | 
				
			||||
            @NonNull OperationMonitor lock) {
 | 
				
			||||
 | 
				
			||||
        return new GestureSelectionHelper(
 | 
				
			||||
                selectionMgr,
 | 
				
			||||
                detailsLookup,
 | 
				
			||||
                new RecyclerViewDelegate(recyclerView),
 | 
				
			||||
                scroller,
 | 
				
			||||
                lock);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @VisibleForTesting
 | 
				
			||||
    abstract static class ViewDelegate {
 | 
				
			||||
        abstract int getHeight();
 | 
				
			||||
 | 
				
			||||
        abstract int getItemUnder(@NonNull MotionEvent e);
 | 
				
			||||
 | 
				
			||||
        abstract int getLastGlidedItemPosition(@NonNull MotionEvent e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @VisibleForTesting
 | 
				
			||||
    static final class RecyclerViewDelegate extends ViewDelegate {
 | 
				
			||||
 | 
				
			||||
        private final RecyclerView mRecyclerView;
 | 
				
			||||
 | 
				
			||||
        RecyclerViewDelegate(@NonNull RecyclerView recyclerView) {
 | 
				
			||||
            checkArgument(recyclerView != null);
 | 
				
			||||
            mRecyclerView = recyclerView;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        int getHeight() {
 | 
				
			||||
            return mRecyclerView.getHeight();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        int getItemUnder(@NonNull MotionEvent e) {
 | 
				
			||||
            View child = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
 | 
				
			||||
            return child != null
 | 
				
			||||
                    ? mRecyclerView.getChildAdapterPosition(child)
 | 
				
			||||
                    : RecyclerView.NO_POSITION;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        int getLastGlidedItemPosition(@NonNull MotionEvent e) {
 | 
				
			||||
            // If user has moved his pointer to the bottom-right empty pane (ie. to the right of the
 | 
				
			||||
            // last item of the recycler view), we would want to set that as the currentItemPos
 | 
				
			||||
            View lastItem = mRecyclerView.getLayoutManager()
 | 
				
			||||
                    .getChildAt(mRecyclerView.getLayoutManager().getChildCount() - 1);
 | 
				
			||||
            int direction = ViewCompat.getLayoutDirection(mRecyclerView);
 | 
				
			||||
            final boolean pastLastItem = isPastLastItem(lastItem.getTop(),
 | 
				
			||||
                    lastItem.getLeft(),
 | 
				
			||||
                    lastItem.getRight(),
 | 
				
			||||
                    e,
 | 
				
			||||
                    direction);
 | 
				
			||||
 | 
				
			||||
            // Since views get attached & detached from RecyclerView,
 | 
				
			||||
            // {@link LayoutManager#getChildCount} can return a different number from the actual
 | 
				
			||||
            // number
 | 
				
			||||
            // of items in the adapter. Using the adapter is the for sure way to get the actual last
 | 
				
			||||
            // item position.
 | 
				
			||||
            final float inboundY = getInboundY(mRecyclerView.getHeight(), e.getY());
 | 
				
			||||
            return (pastLastItem) ? mRecyclerView.getAdapter().getItemCount() - 1
 | 
				
			||||
                    : mRecyclerView.getChildAdapterPosition(
 | 
				
			||||
                            mRecyclerView.findChildViewUnder(e.getX(), inboundY));
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /*
 | 
				
			||||
         * Check to see if MotionEvent if past a particular item, i.e. to the right or to the bottom
 | 
				
			||||
         * of the item.
 | 
				
			||||
         * For RTL, it would to be to the left or to the bottom of the item.
 | 
				
			||||
         */
 | 
				
			||||
        @VisibleForTesting
 | 
				
			||||
        static boolean isPastLastItem(
 | 
				
			||||
                int top, int left, int right, @NonNull MotionEvent e, int direction) {
 | 
				
			||||
            if (direction == View.LAYOUT_DIRECTION_LTR) {
 | 
				
			||||
                return e.getX() > right && e.getY() > top;
 | 
				
			||||
            } else {
 | 
				
			||||
                return e.getX() < left && e.getY() > top;
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,795 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
 | 
				
			||||
import android.graphics.Point;
 | 
				
			||||
import android.graphics.Rect;
 | 
				
			||||
import android.util.Log;
 | 
				
			||||
import android.util.SparseArray;
 | 
				
			||||
import android.util.SparseBooleanArray;
 | 
				
			||||
import android.util.SparseIntArray;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
 | 
				
			||||
 | 
				
			||||
import java.util.ArrayList;
 | 
				
			||||
import java.util.Collections;
 | 
				
			||||
import java.util.HashSet;
 | 
				
			||||
import java.util.List;
 | 
				
			||||
import java.util.Set;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Provides a band selection item model for views within a RecyclerView. This class queries the
 | 
				
			||||
 * RecyclerView to determine where its items are placed; then, once band selection is underway,
 | 
				
			||||
 * it alerts listeners of which items are covered by the selections.
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
 */
 | 
				
			||||
final class GridModel<K> {
 | 
				
			||||
 | 
				
			||||
    // Magical value indicating that a value has not been previously set. primitive null :)
 | 
				
			||||
    static final int NOT_SET = -1;
 | 
				
			||||
 | 
				
			||||
    // Enum values used to determine the corner at which the origin is located within the
 | 
				
			||||
    private static final int UPPER = 0x00;
 | 
				
			||||
    private static final int LOWER = 0x01;
 | 
				
			||||
    private static final int LEFT = 0x00;
 | 
				
			||||
    private static final int RIGHT = 0x02;
 | 
				
			||||
    private static final int UPPER_LEFT = UPPER | LEFT;
 | 
				
			||||
    private static final int UPPER_RIGHT = UPPER | RIGHT;
 | 
				
			||||
    private static final int LOWER_LEFT = LOWER | LEFT;
 | 
				
			||||
    private static final int LOWER_RIGHT = LOWER | RIGHT;
 | 
				
			||||
 | 
				
			||||
    private final GridHost<K> mHost;
 | 
				
			||||
    private final ItemKeyProvider<K> mKeyProvider;
 | 
				
			||||
    private final SelectionPredicate<K> mSelectionPredicate;
 | 
				
			||||
 | 
				
			||||
    private final List<SelectionObserver> mOnSelectionChangedListeners = new ArrayList<>();
 | 
				
			||||
 | 
				
			||||
    // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed
 | 
				
			||||
    // by their y-offset. For example, if the first column of the view starts at an x-value of 5,
 | 
				
			||||
    // mColumns.get(5) would return an array of positions in that column. Within that array, the
 | 
				
			||||
    // value for key y is the adapter position for the item whose y-offset is y.
 | 
				
			||||
    private final SparseArray<SparseIntArray> mColumns = new SparseArray<>();
 | 
				
			||||
 | 
				
			||||
    // List of limits along the x-axis (columns).
 | 
				
			||||
    // This list is sorted from furthest left to furthest right.
 | 
				
			||||
    private final List<Limits> mColumnBounds = new ArrayList<>();
 | 
				
			||||
 | 
				
			||||
    // List of limits along the y-axis (rows). Note that this list only contains items which
 | 
				
			||||
    // have been in the viewport.
 | 
				
			||||
    private final List<Limits> mRowBounds = new ArrayList<>();
 | 
				
			||||
 | 
				
			||||
    // The adapter positions which have been recorded so far.
 | 
				
			||||
    private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
 | 
				
			||||
 | 
				
			||||
    // Array passed to registered OnSelectionChangedListeners. One array is created and reused
 | 
				
			||||
    // throughout the lifetime of the object.
 | 
				
			||||
    private final Set<K> mSelection = new HashSet<>();
 | 
				
			||||
 | 
				
			||||
    // The current pointer (in absolute positioning from the top of the view).
 | 
				
			||||
    private Point mPointer;
 | 
				
			||||
 | 
				
			||||
    // The bounds of the band selection.
 | 
				
			||||
    private RelativePoint mRelOrigin;
 | 
				
			||||
    private RelativePoint mRelPointer;
 | 
				
			||||
 | 
				
			||||
    private boolean mIsActive;
 | 
				
			||||
 | 
				
			||||
    // Tracks where the band select originated from. This is used to determine where selections
 | 
				
			||||
    // should expand from when Shift+click is used.
 | 
				
			||||
    private int mPositionNearestOrigin = NOT_SET;
 | 
				
			||||
 | 
				
			||||
    private final OnScrollListener mScrollListener;
 | 
				
			||||
 | 
				
			||||
    GridModel(
 | 
				
			||||
            GridHost host,
 | 
				
			||||
            ItemKeyProvider<K> keyProvider,
 | 
				
			||||
            SelectionPredicate<K> selectionPredicate) {
 | 
				
			||||
 | 
				
			||||
        checkArgument(host != null);
 | 
				
			||||
        checkArgument(keyProvider != null);
 | 
				
			||||
        checkArgument(selectionPredicate != null);
 | 
				
			||||
 | 
				
			||||
        mHost = host;
 | 
				
			||||
        mKeyProvider = keyProvider;
 | 
				
			||||
        mSelectionPredicate = selectionPredicate;
 | 
				
			||||
 | 
				
			||||
        mScrollListener = new OnScrollListener() {
 | 
				
			||||
            @Override
 | 
				
			||||
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
 | 
				
			||||
                GridModel.this.onScrolled(recyclerView, dx, dy);
 | 
				
			||||
            }
 | 
				
			||||
        };
 | 
				
			||||
 | 
				
			||||
        mHost.addOnScrollListener(mScrollListener);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Start a band select operation at the given point.
 | 
				
			||||
     *
 | 
				
			||||
     * @param relativeOrigin The origin of the band select operation, relative to the viewport.
 | 
				
			||||
     *                       For example, if the view is scrolled to the bottom, the top-left of
 | 
				
			||||
     *                       the
 | 
				
			||||
     *                       viewport
 | 
				
			||||
     *                       would have a relative origin of (0, 0), even though its absolute point
 | 
				
			||||
     *                       has a higher
 | 
				
			||||
     *                       y-value.
 | 
				
			||||
     */
 | 
				
			||||
    void startCapturing(Point relativeOrigin) {
 | 
				
			||||
        recordVisibleChildren();
 | 
				
			||||
        if (isEmpty()) {
 | 
				
			||||
            // The selection band logic works only if there is at least one visible child.
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        mIsActive = true;
 | 
				
			||||
        mPointer = mHost.createAbsolutePoint(relativeOrigin);
 | 
				
			||||
        mRelOrigin = createRelativePoint(mPointer);
 | 
				
			||||
        mRelPointer = createRelativePoint(mPointer);
 | 
				
			||||
        computeCurrentSelection();
 | 
				
			||||
        notifySelectionChanged();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Ends the band selection.
 | 
				
			||||
     */
 | 
				
			||||
    void stopCapturing() {
 | 
				
			||||
        mIsActive = false;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
 | 
				
			||||
     * opposite the origin.
 | 
				
			||||
     *
 | 
				
			||||
     * @param relativePointer The pointer (opposite of the origin) of the band select operation,
 | 
				
			||||
     *                        relative to the viewport. For example, if the view is scrolled to the
 | 
				
			||||
     *                        bottom, the
 | 
				
			||||
     *                        top-left of the viewport would have a relative origin of (0, 0), even
 | 
				
			||||
     *                        though its
 | 
				
			||||
     *                        absolute point has a higher y-value.
 | 
				
			||||
     */
 | 
				
			||||
    void resizeSelection(Point relativePointer) {
 | 
				
			||||
        mPointer = mHost.createAbsolutePoint(relativePointer);
 | 
				
			||||
        updateModel();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return The adapter position for the item nearest the origin corresponding to the latest
 | 
				
			||||
     * band select operation, or NOT_SET if the selection did not cover any items.
 | 
				
			||||
     */
 | 
				
			||||
    int getPositionNearestOrigin() {
 | 
				
			||||
        return mPositionNearestOrigin;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    void onScrolled(RecyclerView recyclerView, int dx, int dy) {
 | 
				
			||||
        if (!mIsActive) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        mPointer.x += dx;
 | 
				
			||||
        mPointer.y += dy;
 | 
				
			||||
        recordVisibleChildren();
 | 
				
			||||
        updateModel();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Queries the view for all children and records their location metadata.
 | 
				
			||||
     */
 | 
				
			||||
    private void recordVisibleChildren() {
 | 
				
			||||
        for (int i = 0; i < mHost.getVisibleChildCount(); i++) {
 | 
				
			||||
            int adapterPosition = mHost.getAdapterPositionAt(i);
 | 
				
			||||
            // Sometimes the view is not attached, as we notify the multi selection manager
 | 
				
			||||
            // synchronously, while views are attached asynchronously. As a result items which
 | 
				
			||||
            // are in the adapter may not actually have a corresponding view (yet).
 | 
				
			||||
            if (mHost.hasView(adapterPosition)
 | 
				
			||||
                    && mSelectionPredicate.canSetStateAtPosition(adapterPosition, true)
 | 
				
			||||
                    && !mKnownPositions.get(adapterPosition)) {
 | 
				
			||||
                mKnownPositions.put(adapterPosition, true);
 | 
				
			||||
                recordItemData(mHost.getAbsoluteRectForChildViewAt(i), adapterPosition);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Checks if there are any recorded children.
 | 
				
			||||
     */
 | 
				
			||||
    private boolean isEmpty() {
 | 
				
			||||
        return mColumnBounds.size() == 0 || mRowBounds.size() == 0;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Updates the limits lists and column map with the given item metadata.
 | 
				
			||||
     *
 | 
				
			||||
     * @param absoluteChildRect The absolute rectangle for the child view being processed.
 | 
				
			||||
     * @param adapterPosition   The position of the child view being processed.
 | 
				
			||||
     */
 | 
				
			||||
    private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
 | 
				
			||||
        if (mColumnBounds.size() != mHost.getColumnCount()) {
 | 
				
			||||
            // If not all x-limits have been recorded, record this one.
 | 
				
			||||
            recordLimits(
 | 
				
			||||
                    mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right));
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        recordLimits(mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
 | 
				
			||||
 | 
				
			||||
        SparseIntArray columnList = mColumns.get(absoluteChildRect.left);
 | 
				
			||||
        if (columnList == null) {
 | 
				
			||||
            columnList = new SparseIntArray();
 | 
				
			||||
            mColumns.put(absoluteChildRect.left, columnList);
 | 
				
			||||
        }
 | 
				
			||||
        columnList.put(absoluteChildRect.top, adapterPosition);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
 | 
				
			||||
     * does not exist.
 | 
				
			||||
     */
 | 
				
			||||
    private void recordLimits(List<Limits> limitsList, Limits limits) {
 | 
				
			||||
        int index = Collections.binarySearch(limitsList, limits);
 | 
				
			||||
        if (index < 0) {
 | 
				
			||||
            limitsList.add(~index, limits);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Handles a moved pointer; this function determines whether the pointer movement resulted
 | 
				
			||||
     * in a selection change and, if it has, notifies listeners of this change.
 | 
				
			||||
     */
 | 
				
			||||
    private void updateModel() {
 | 
				
			||||
        RelativePoint old = mRelPointer;
 | 
				
			||||
        mRelPointer = createRelativePoint(mPointer);
 | 
				
			||||
        if (old != null && mRelPointer.equals(old)) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        computeCurrentSelection();
 | 
				
			||||
        notifySelectionChanged();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Computes the currently-selected items.
 | 
				
			||||
     */
 | 
				
			||||
    private void computeCurrentSelection() {
 | 
				
			||||
        if (areItemsCoveredByBand(mRelPointer, mRelOrigin)) {
 | 
				
			||||
            updateSelection(computeBounds());
 | 
				
			||||
        } else {
 | 
				
			||||
            mSelection.clear();
 | 
				
			||||
            mPositionNearestOrigin = NOT_SET;
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Notifies all listeners of a selection change. Note that this function simply passes
 | 
				
			||||
     * mSelection, so computeCurrentSelection() should be called before this
 | 
				
			||||
     * function.
 | 
				
			||||
     */
 | 
				
			||||
    private void notifySelectionChanged() {
 | 
				
			||||
        for (SelectionObserver listener : mOnSelectionChangedListeners) {
 | 
				
			||||
            listener.onSelectionChanged(mSelection);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @param rect Rectangle including all covered items.
 | 
				
			||||
     */
 | 
				
			||||
    private void updateSelection(Rect rect) {
 | 
				
			||||
        int columnStart =
 | 
				
			||||
                Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
 | 
				
			||||
 | 
				
			||||
        checkArgument(columnStart >= 0, "Rect doesn't intesect any known column.");
 | 
				
			||||
 | 
				
			||||
        int columnEnd = columnStart;
 | 
				
			||||
 | 
				
			||||
        for (int i = columnStart; i < mColumnBounds.size()
 | 
				
			||||
                && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
 | 
				
			||||
            columnEnd = i;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
 | 
				
			||||
        if (rowStart < 0) {
 | 
				
			||||
            mPositionNearestOrigin = NOT_SET;
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        int rowEnd = rowStart;
 | 
				
			||||
        for (int i = rowStart; i < mRowBounds.size()
 | 
				
			||||
                && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
 | 
				
			||||
            rowEnd = i;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        updateSelection(columnStart, columnEnd, rowStart, rowEnd);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Computes the selection given the previously-computed start- and end-indices for each
 | 
				
			||||
     * row and column.
 | 
				
			||||
     */
 | 
				
			||||
    private void updateSelection(
 | 
				
			||||
            int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
 | 
				
			||||
 | 
				
			||||
        if (BandSelectionHelper.DEBUG) {
 | 
				
			||||
            Log.d(BandSelectionHelper.TAG, String.format(
 | 
				
			||||
                    "updateSelection: %d, %d, %d, %d",
 | 
				
			||||
                    columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        mSelection.clear();
 | 
				
			||||
        for (int column = columnStartIndex; column <= columnEndIndex; column++) {
 | 
				
			||||
            SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
 | 
				
			||||
            for (int row = rowStartIndex; row <= rowEndIndex; row++) {
 | 
				
			||||
                // The default return value for SparseIntArray.get is 0, which is a valid
 | 
				
			||||
                // position. Use a sentry value to prevent erroneously selecting item 0.
 | 
				
			||||
                final int rowKey = mRowBounds.get(row).lowerLimit;
 | 
				
			||||
                int position = items.get(rowKey, NOT_SET);
 | 
				
			||||
                if (position != NOT_SET) {
 | 
				
			||||
                    K key = mKeyProvider.getKey(position);
 | 
				
			||||
                    if (key != null) {
 | 
				
			||||
                        // The adapter inserts items for UI layout purposes that aren't
 | 
				
			||||
                        // associated with files. Those will have a null model ID.
 | 
				
			||||
                        // Don't select them.
 | 
				
			||||
                        if (canSelect(key)) {
 | 
				
			||||
                            mSelection.add(key);
 | 
				
			||||
                        }
 | 
				
			||||
                    }
 | 
				
			||||
                    if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
 | 
				
			||||
                            row, rowStartIndex, rowEndIndex)) {
 | 
				
			||||
                        // If this is the position nearest the origin, record it now so that it
 | 
				
			||||
                        // can be returned by endSelection() later.
 | 
				
			||||
                        mPositionNearestOrigin = position;
 | 
				
			||||
                    }
 | 
				
			||||
                }
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private boolean canSelect(K key) {
 | 
				
			||||
        return mSelectionPredicate.canSetStateForKey(key, true);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return Returns true if the position is the nearest to the origin, or, in the case of the
 | 
				
			||||
     * lower-right corner, whether it is possible that the position is the nearest to the
 | 
				
			||||
     * origin. See comment below for reasoning for this special case.
 | 
				
			||||
     */
 | 
				
			||||
    private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
 | 
				
			||||
            int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
 | 
				
			||||
        int corner = computeCornerNearestOrigin();
 | 
				
			||||
        switch (corner) {
 | 
				
			||||
            case UPPER_LEFT:
 | 
				
			||||
                return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
 | 
				
			||||
            case UPPER_RIGHT:
 | 
				
			||||
                return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
 | 
				
			||||
            case LOWER_LEFT:
 | 
				
			||||
                return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
 | 
				
			||||
            case LOWER_RIGHT:
 | 
				
			||||
                // Note that in some cases, the last row will not have as many items as there
 | 
				
			||||
                // are columns (e.g., if there are 4 items and 3 columns, the second row will
 | 
				
			||||
                // only have one item in the first column). This function is invoked for each
 | 
				
			||||
                // position from left to right, so return true for any position in the bottom
 | 
				
			||||
                // row and only the right-most position in the bottom row will be recorded.
 | 
				
			||||
                return rowIndex == rowEndIndex;
 | 
				
			||||
            default:
 | 
				
			||||
                throw new RuntimeException("Invalid corner type.");
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Listener for changes in which items have been band selected.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract static class SelectionObserver<K> {
 | 
				
			||||
        abstract void onSelectionChanged(Set<K> updatedSelection);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    void addOnSelectionChangedListener(SelectionObserver listener) {
 | 
				
			||||
        mOnSelectionChangedListeners.add(listener);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Called when {@link BandSelectionHelper} is finished with a GridModel.
 | 
				
			||||
     */
 | 
				
			||||
    void onDestroy() {
 | 
				
			||||
        mOnSelectionChangedListeners.clear();
 | 
				
			||||
        // Cleanup listeners to prevent memory leaks.
 | 
				
			||||
        mHost.removeOnScrollListener(mScrollListener);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
 | 
				
			||||
     * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
 | 
				
			||||
     * of item columns and the top- and bottom sides of item rows so that it can be determined
 | 
				
			||||
     * whether the pointer is located within the bounds of an item.
 | 
				
			||||
     */
 | 
				
			||||
    private static class Limits implements Comparable<Limits> {
 | 
				
			||||
        public int lowerLimit;
 | 
				
			||||
        public int upperLimit;
 | 
				
			||||
 | 
				
			||||
        Limits(int lowerLimit, int upperLimit) {
 | 
				
			||||
            this.lowerLimit = lowerLimit;
 | 
				
			||||
            this.upperLimit = upperLimit;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public int compareTo(Limits other) {
 | 
				
			||||
            return lowerLimit - other.lowerLimit;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public int hashCode() {
 | 
				
			||||
            return lowerLimit ^ upperLimit;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public boolean equals(Object other) {
 | 
				
			||||
            if (!(other instanceof Limits)) {
 | 
				
			||||
                return false;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            return ((Limits) other).lowerLimit == lowerLimit
 | 
				
			||||
                    && ((Limits) other).upperLimit == upperLimit;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public String toString() {
 | 
				
			||||
            return "(" + lowerLimit + ", " + upperLimit + ")";
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * The location of a coordinate relative to items. This class represents a general area of the
 | 
				
			||||
     * view as it relates to band selection rather than an explicit point. For example, two
 | 
				
			||||
     * different points within an item are considered to have the same "location" because band
 | 
				
			||||
     * selection originating within the item would select the same items no matter which point
 | 
				
			||||
     * was used. Same goes for points between items as well as those at the very beginning or end
 | 
				
			||||
     * of the view.
 | 
				
			||||
     *
 | 
				
			||||
     * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
 | 
				
			||||
     * advantage of tying the value to the Limits of items along that axis. This allows easy
 | 
				
			||||
     * selection of items within those Limits as opposed to a search through every item to see if a
 | 
				
			||||
     * given coordinate value falls within those Limits.
 | 
				
			||||
     */
 | 
				
			||||
    private static class RelativeCoordinate
 | 
				
			||||
            implements Comparable<RelativeCoordinate> {
 | 
				
			||||
        /**
 | 
				
			||||
         * Location describing points after the last known item.
 | 
				
			||||
         */
 | 
				
			||||
        static final int AFTER_LAST_ITEM = 0;
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Location describing points before the first known item.
 | 
				
			||||
         */
 | 
				
			||||
        static final int BEFORE_FIRST_ITEM = 1;
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Location describing points between two items.
 | 
				
			||||
         */
 | 
				
			||||
        static final int BETWEEN_TWO_ITEMS = 2;
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Location describing points within the limits of one item.
 | 
				
			||||
         */
 | 
				
			||||
        static final int WITHIN_LIMITS = 3;
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
 | 
				
			||||
         * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
 | 
				
			||||
         */
 | 
				
			||||
        public final int type;
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
 | 
				
			||||
         * BETWEEN_TWO_ITEMS.
 | 
				
			||||
         */
 | 
				
			||||
        public Limits limitsBeforeCoordinate;
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
 | 
				
			||||
         */
 | 
				
			||||
        public Limits limitsAfterCoordinate;
 | 
				
			||||
 | 
				
			||||
        // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
 | 
				
			||||
        public Limits mFirstKnownItem;
 | 
				
			||||
        // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
 | 
				
			||||
        public Limits mLastKnownItem;
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * @param limitsList The sorted limits list for the coordinate type. If this
 | 
				
			||||
         *                   CoordinateLocation is an x-value, mXLimitsList should be passed;
 | 
				
			||||
         *                   otherwise,
 | 
				
			||||
         *                   mYLimitsList should be pased.
 | 
				
			||||
         * @param value      The coordinate value.
 | 
				
			||||
         */
 | 
				
			||||
        RelativeCoordinate(List<Limits> limitsList, int value) {
 | 
				
			||||
            int index = Collections.binarySearch(limitsList, new Limits(value, value));
 | 
				
			||||
 | 
				
			||||
            if (index >= 0) {
 | 
				
			||||
                this.type = WITHIN_LIMITS;
 | 
				
			||||
                this.limitsBeforeCoordinate = limitsList.get(index);
 | 
				
			||||
            } else if (~index == 0) {
 | 
				
			||||
                this.type = BEFORE_FIRST_ITEM;
 | 
				
			||||
                this.mFirstKnownItem = limitsList.get(0);
 | 
				
			||||
            } else if (~index == limitsList.size()) {
 | 
				
			||||
                Limits lastLimits = limitsList.get(limitsList.size() - 1);
 | 
				
			||||
                if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
 | 
				
			||||
                    this.type = WITHIN_LIMITS;
 | 
				
			||||
                    this.limitsBeforeCoordinate = lastLimits;
 | 
				
			||||
                } else {
 | 
				
			||||
                    this.type = AFTER_LAST_ITEM;
 | 
				
			||||
                    this.mLastKnownItem = lastLimits;
 | 
				
			||||
                }
 | 
				
			||||
            } else {
 | 
				
			||||
                Limits limitsBeforeIndex = limitsList.get(~index - 1);
 | 
				
			||||
                if (limitsBeforeIndex.lowerLimit <= value
 | 
				
			||||
                        && value <= limitsBeforeIndex.upperLimit) {
 | 
				
			||||
                    this.type = WITHIN_LIMITS;
 | 
				
			||||
                    this.limitsBeforeCoordinate = limitsList.get(~index - 1);
 | 
				
			||||
                } else {
 | 
				
			||||
                    this.type = BETWEEN_TWO_ITEMS;
 | 
				
			||||
                    this.limitsBeforeCoordinate = limitsList.get(~index - 1);
 | 
				
			||||
                    this.limitsAfterCoordinate = limitsList.get(~index);
 | 
				
			||||
                }
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        int toComparisonValue() {
 | 
				
			||||
            if (type == BEFORE_FIRST_ITEM) {
 | 
				
			||||
                return mFirstKnownItem.lowerLimit - 1;
 | 
				
			||||
            } else if (type == AFTER_LAST_ITEM) {
 | 
				
			||||
                return mLastKnownItem.upperLimit + 1;
 | 
				
			||||
            } else if (type == BETWEEN_TWO_ITEMS) {
 | 
				
			||||
                return limitsBeforeCoordinate.upperLimit + 1;
 | 
				
			||||
            } else {
 | 
				
			||||
                return limitsBeforeCoordinate.lowerLimit;
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public int hashCode() {
 | 
				
			||||
            return mFirstKnownItem.lowerLimit
 | 
				
			||||
                    ^ mLastKnownItem.upperLimit
 | 
				
			||||
                    ^ limitsBeforeCoordinate.upperLimit
 | 
				
			||||
                    ^ limitsBeforeCoordinate.lowerLimit;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public boolean equals(Object other) {
 | 
				
			||||
            if (!(other instanceof RelativeCoordinate)) {
 | 
				
			||||
                return false;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
 | 
				
			||||
            return toComparisonValue() == otherCoordinate.toComparisonValue();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public int compareTo(RelativeCoordinate other) {
 | 
				
			||||
            return toComparisonValue() - other.toComparisonValue();
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    RelativePoint createRelativePoint(Point point) {
 | 
				
			||||
        return new RelativePoint(
 | 
				
			||||
                new RelativeCoordinate(mColumnBounds, point.x),
 | 
				
			||||
                new RelativeCoordinate(mRowBounds, point.y));
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * The location of a point relative to the Limits of nearby items; consists of both an x- and
 | 
				
			||||
     * y-RelativeCoordinateLocation.
 | 
				
			||||
     */
 | 
				
			||||
    private static class RelativePoint {
 | 
				
			||||
 | 
				
			||||
        final RelativeCoordinate mX;
 | 
				
			||||
        final RelativeCoordinate mY;
 | 
				
			||||
 | 
				
			||||
        RelativePoint(
 | 
				
			||||
                @NonNull List<Limits> columnLimits,
 | 
				
			||||
                @NonNull List<Limits> rowLimits, Point point) {
 | 
				
			||||
 | 
				
			||||
            this.mX = new RelativeCoordinate(columnLimits, point.x);
 | 
				
			||||
            this.mY = new RelativeCoordinate(rowLimits, point.y);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        RelativePoint(@NonNull RelativeCoordinate x, @NonNull RelativeCoordinate y) {
 | 
				
			||||
            this.mX = x;
 | 
				
			||||
            this.mY = y;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public int hashCode() {
 | 
				
			||||
            return mX.toComparisonValue() ^ mY.toComparisonValue();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public boolean equals(@Nullable Object other) {
 | 
				
			||||
            if (!(other instanceof RelativePoint)) {
 | 
				
			||||
                return false;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            RelativePoint otherPoint = (RelativePoint) other;
 | 
				
			||||
            return mX.equals(otherPoint.mX) && mY.equals(otherPoint.mY);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Generates a rectangle which contains the items selected by the pointer and origin.
 | 
				
			||||
     *
 | 
				
			||||
     * @return The rectangle, or null if no items were selected.
 | 
				
			||||
     */
 | 
				
			||||
    private Rect computeBounds() {
 | 
				
			||||
        Rect rect = new Rect();
 | 
				
			||||
        rect.left = getCoordinateValue(
 | 
				
			||||
                min(mRelOrigin.mX, mRelPointer.mX),
 | 
				
			||||
                mColumnBounds,
 | 
				
			||||
                true);
 | 
				
			||||
        rect.right = getCoordinateValue(
 | 
				
			||||
                max(mRelOrigin.mX, mRelPointer.mX),
 | 
				
			||||
                mColumnBounds,
 | 
				
			||||
                false);
 | 
				
			||||
        rect.top = getCoordinateValue(
 | 
				
			||||
                min(mRelOrigin.mY, mRelPointer.mY),
 | 
				
			||||
                mRowBounds,
 | 
				
			||||
                true);
 | 
				
			||||
        rect.bottom = getCoordinateValue(
 | 
				
			||||
                max(mRelOrigin.mY, mRelPointer.mY),
 | 
				
			||||
                mRowBounds,
 | 
				
			||||
                false);
 | 
				
			||||
        return rect;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Computes the corner of the selection nearest the origin.
 | 
				
			||||
     */
 | 
				
			||||
    private int computeCornerNearestOrigin() {
 | 
				
			||||
        int cornerValue = 0;
 | 
				
			||||
 | 
				
			||||
        if (mRelOrigin.mY.equals(min(mRelOrigin.mY, mRelPointer.mY))) {
 | 
				
			||||
            cornerValue |= UPPER;
 | 
				
			||||
        } else {
 | 
				
			||||
            cornerValue |= LOWER;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (mRelOrigin.mX.equals(min(mRelOrigin.mX, mRelPointer.mX))) {
 | 
				
			||||
            cornerValue |= LEFT;
 | 
				
			||||
        } else {
 | 
				
			||||
            cornerValue |= RIGHT;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        return cornerValue;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private RelativeCoordinate min(
 | 
				
			||||
            @NonNull RelativeCoordinate first, @NonNull RelativeCoordinate second) {
 | 
				
			||||
        return first.compareTo(second) < 0 ? first : second;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private RelativeCoordinate max(
 | 
				
			||||
            @NonNull RelativeCoordinate first, @NonNull RelativeCoordinate second) {
 | 
				
			||||
        return first.compareTo(second) > 0 ? first : second;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
 | 
				
			||||
     * coordinate.
 | 
				
			||||
     */
 | 
				
			||||
    private int getCoordinateValue(
 | 
				
			||||
            @NonNull RelativeCoordinate coordinate,
 | 
				
			||||
            @NonNull List<Limits> limitsList,
 | 
				
			||||
            boolean isStartOfRange) {
 | 
				
			||||
 | 
				
			||||
        switch (coordinate.type) {
 | 
				
			||||
            case RelativeCoordinate.BEFORE_FIRST_ITEM:
 | 
				
			||||
                return limitsList.get(0).lowerLimit;
 | 
				
			||||
            case RelativeCoordinate.AFTER_LAST_ITEM:
 | 
				
			||||
                return limitsList.get(limitsList.size() - 1).upperLimit;
 | 
				
			||||
            case RelativeCoordinate.BETWEEN_TWO_ITEMS:
 | 
				
			||||
                if (isStartOfRange) {
 | 
				
			||||
                    return coordinate.limitsAfterCoordinate.lowerLimit;
 | 
				
			||||
                } else {
 | 
				
			||||
                    return coordinate.limitsBeforeCoordinate.upperLimit;
 | 
				
			||||
                }
 | 
				
			||||
            case RelativeCoordinate.WITHIN_LIMITS:
 | 
				
			||||
                return coordinate.limitsBeforeCoordinate.lowerLimit;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        throw new RuntimeException("Invalid coordinate value.");
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private boolean areItemsCoveredByBand(
 | 
				
			||||
            @NonNull RelativePoint first, @NonNull RelativePoint second) {
 | 
				
			||||
 | 
				
			||||
        return doesCoordinateLocationCoverItems(first.mX, second.mX)
 | 
				
			||||
                && doesCoordinateLocationCoverItems(first.mY, second.mY);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private boolean doesCoordinateLocationCoverItems(
 | 
				
			||||
            @NonNull RelativeCoordinate pointerCoordinate,
 | 
				
			||||
            @NonNull RelativeCoordinate originCoordinate) {
 | 
				
			||||
 | 
				
			||||
        if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM
 | 
				
			||||
                && originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM
 | 
				
			||||
                && originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS
 | 
				
			||||
                && originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS
 | 
				
			||||
                && pointerCoordinate.limitsBeforeCoordinate.equals(
 | 
				
			||||
                originCoordinate.limitsBeforeCoordinate)
 | 
				
			||||
                && pointerCoordinate.limitsAfterCoordinate.equals(
 | 
				
			||||
                originCoordinate.limitsAfterCoordinate)) {
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        return true;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Provides functionality for BandController. Exists primarily to tests that are
 | 
				
			||||
     * fully isolated from RecyclerView.
 | 
				
			||||
     *
 | 
				
			||||
     * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
     */
 | 
				
			||||
    abstract static class GridHost<K> extends BandSelectionHelper.BandHost<K> {
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Remove the listener.
 | 
				
			||||
         *
 | 
				
			||||
         * @param listener
 | 
				
			||||
         */
 | 
				
			||||
        abstract void removeOnScrollListener(@NonNull OnScrollListener listener);
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * @param relativePoint for which to create absolute point.
 | 
				
			||||
         * @return absolute point.
 | 
				
			||||
         */
 | 
				
			||||
        abstract Point createAbsolutePoint(@NonNull Point relativePoint);
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * @param index index of child.
 | 
				
			||||
         * @return rectangle describing child at {@code index}.
 | 
				
			||||
         */
 | 
				
			||||
        abstract Rect getAbsoluteRectForChildViewAt(int index);
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * @param index index of child.
 | 
				
			||||
         * @return child adapter position for the child at {@code index}
 | 
				
			||||
         */
 | 
				
			||||
        abstract int getAdapterPositionAt(int index);
 | 
				
			||||
 | 
				
			||||
        /** @return column count. */
 | 
				
			||||
        abstract int getColumnCount();
 | 
				
			||||
 | 
				
			||||
        /** @return number of children visible in the view. */
 | 
				
			||||
        abstract int getVisibleChildCount();
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * @return true if the item at adapter position is attached to a view.
 | 
				
			||||
         */
 | 
				
			||||
        abstract boolean hasView(int adapterPosition);
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,258 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * The Selection library calls {@link #getItemDetails(MotionEvent)} when it needs
 | 
				
			||||
 * access to information about the area and/or {@link ItemDetails} under a {@link MotionEvent}.
 | 
				
			||||
 * Your implementation must negotiate
 | 
				
			||||
 * {@link RecyclerView.ViewHolder ViewHolder} lookup with the
 | 
				
			||||
 * corresponding RecyclerView instance, and the subsequent conversion of the ViewHolder
 | 
				
			||||
 * instance to an {@link ItemDetails} instance.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Example</b>
 | 
				
			||||
 * <pre>
 | 
				
			||||
 * final class MyDetailsLookup extends ItemDetailsLookup<Uri> {
 | 
				
			||||
 *
 | 
				
			||||
 *   private final RecyclerView mRecyclerView;
 | 
				
			||||
 *
 | 
				
			||||
 *   MyDetailsLookup(RecyclerView recyclerView) {
 | 
				
			||||
 *       mRecyclerView = recyclerView;
 | 
				
			||||
 *   }
 | 
				
			||||
 *
 | 
				
			||||
 *   public ItemDetails<Uri> getItemDetails(MotionEvent e) {
 | 
				
			||||
 *       View view = mRecView.findChildViewUnder(e.getX(), e.getY());
 | 
				
			||||
 *       if (view != null) {
 | 
				
			||||
 *           ViewHolder holder = mRecView.getChildViewHolder(view);
 | 
				
			||||
 *           if (holder instanceof MyHolder) {
 | 
				
			||||
 *               return ((MyHolder) holder).getItemDetails();
 | 
				
			||||
 *           }
 | 
				
			||||
 *       }
 | 
				
			||||
 *       return null;
 | 
				
			||||
 *   }
 | 
				
			||||
 *}
 | 
				
			||||
 * </pre>
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
 */
 | 
				
			||||
public abstract class ItemDetailsLookup<K> {
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return true if there is an item at the event coordinates.
 | 
				
			||||
     */
 | 
				
			||||
    final boolean overItem(@NonNull MotionEvent e) {
 | 
				
			||||
        return getItemPosition(e) != RecyclerView.NO_POSITION;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return true if there is an item w/ a stable ID at the event coordinates.
 | 
				
			||||
     */
 | 
				
			||||
    final boolean overItemWithSelectionKey(@NonNull MotionEvent e) {
 | 
				
			||||
        return overItem(e) && hasSelectionKey(getItemDetails(e));
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return true if the event coordinates are in an area of the item
 | 
				
			||||
     * that can result in dragging the item. List items frequently have a white
 | 
				
			||||
     * area that is not draggable allowing band selection to be initiated
 | 
				
			||||
     * in that area.
 | 
				
			||||
     */
 | 
				
			||||
    final boolean inItemDragRegion(@NonNull MotionEvent e) {
 | 
				
			||||
        return overItem(e) && getItemDetails(e).inDragRegion(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return true if the event coordinates are in a "selection hot spot"
 | 
				
			||||
     * region of an item. Contact in these regions result in immediate
 | 
				
			||||
     * selection, even when there is no existing selection.
 | 
				
			||||
     */
 | 
				
			||||
    final boolean inItemSelectRegion(@NonNull MotionEvent e) {
 | 
				
			||||
        return overItem(e) && getItemDetails(e).inSelectionHotspot(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return the adapter position of the item at the event coordinates.
 | 
				
			||||
     */
 | 
				
			||||
    final int getItemPosition(@NonNull MotionEvent e) {
 | 
				
			||||
        @Nullable ItemDetails<?> item = getItemDetails(e);
 | 
				
			||||
        return item != null
 | 
				
			||||
                ? item.getPosition()
 | 
				
			||||
                : RecyclerView.NO_POSITION;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private static boolean hasSelectionKey(@Nullable ItemDetails<?> item) {
 | 
				
			||||
        return item != null && item.getSelectionKey() != null;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private static boolean hasPosition(@Nullable ItemDetails<?> item) {
 | 
				
			||||
        return item != null && item.getPosition() != RecyclerView.NO_POSITION;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return the ItemDetails for the item under the event, or null.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract @Nullable ItemDetails<K> getItemDetails(@NonNull MotionEvent e);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * An ItemDetails implementation provides the selection library with access to information
 | 
				
			||||
     * about a specific RecyclerView item. This class is a key component in controling
 | 
				
			||||
     * the behaviors of the selection library in the context of a specific activity.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * <b>Selection Hotspot</b>
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * This is an optional feature identifying an area within a view that
 | 
				
			||||
     * is single-tap to select. Ordinarily a single tap on an item when there is no
 | 
				
			||||
     * existing selection will result in that item being activated. If the tap
 | 
				
			||||
     * occurs within the "selection hotspot" the item will instead be selected.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * See {@link OnItemActivatedListener} for details on handling item activation.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * <b>Drag Region</b>
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * The selection library provides support for mouse driven band selection. The "lasso"
 | 
				
			||||
     * typically associated with mouse selection can be started only in an empty
 | 
				
			||||
     * area of the RecyclerView (an area where the item position == RecyclerView#NO_POSITION,
 | 
				
			||||
     * or where RecyclerView#findChildViewUnder returns null). But in many instances
 | 
				
			||||
     * the item views presented by RecyclerView will contain areas that may be perceived
 | 
				
			||||
     * by the user as being empty. The user may expect to be able to initiate band
 | 
				
			||||
     * selection in these empty areas.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * The "drag region" concept exists in large part to accommodate this user expectation.
 | 
				
			||||
     * Drag region is the content in an item view that the user doesn't otherwise
 | 
				
			||||
     * perceive to be empty or part of the background of recycler view.
 | 
				
			||||
     *
 | 
				
			||||
     * Take for example a traditional single column layout where
 | 
				
			||||
     * the view layout width is "match_parent":
 | 
				
			||||
     * <pre>
 | 
				
			||||
     * -------------------------------------------------------
 | 
				
			||||
     * | [icon]  A string label.   ...empty space...         |
 | 
				
			||||
     * -------------------------------------------------------
 | 
				
			||||
     *   < ---  drag region  --> < --treated as background-->
 | 
				
			||||
     *</pre>
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * Further more, within a drag region, a mouse click and drag will immediately
 | 
				
			||||
     * initiate drag and drop (if supported by your configuration).
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * As user expectations around touch and mouse input differ substantially,
 | 
				
			||||
     * "drag region" has no effect on handling of touch input.
 | 
				
			||||
     *
 | 
				
			||||
     * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract static class ItemDetails<K> {
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Returns the adapter position of the item. See
 | 
				
			||||
         * {@link RecyclerView.ViewHolder#getAdapterPosition() ViewHolder.getAdapterPosition}
 | 
				
			||||
         *
 | 
				
			||||
         * @return the position of an item.
 | 
				
			||||
         */
 | 
				
			||||
        public abstract int getPosition();
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * @return true if the item has a selection key.
 | 
				
			||||
         */
 | 
				
			||||
        public boolean hasSelectionKey() {
 | 
				
			||||
            return getSelectionKey() != null;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * @return the selection key of an item.
 | 
				
			||||
         */
 | 
				
			||||
        public abstract @Nullable K getSelectionKey();
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Areas are often included in a view that behave similar to checkboxes, such
 | 
				
			||||
         * as the icon to the left of an email message. "selection
 | 
				
			||||
         * hotspot" provides a mechanism to identify such regions, and for the
 | 
				
			||||
         * library to directly translate taps in these regions into a change
 | 
				
			||||
         * in selection state.
 | 
				
			||||
         *
 | 
				
			||||
         * @return true if the event is in an area of the item that should be
 | 
				
			||||
         * directly interpreted as a user wishing to select the item. This
 | 
				
			||||
         * is useful for checkboxes and other UI affordances focused on enabling
 | 
				
			||||
         * selection.
 | 
				
			||||
         */
 | 
				
			||||
        public boolean inSelectionHotspot(@NonNull MotionEvent e) {
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * "Item Drag Region" identifies areas of an item that are not considered when the library
 | 
				
			||||
         * evaluates whether or not to initiate band-selection for mouse input. The drag region
 | 
				
			||||
         * will usually correspond to an area of an item that represents user visible content.
 | 
				
			||||
         * Mouse driven band selection operations are only ever initiated in non-drag-regions.
 | 
				
			||||
         * This is a consideration as many layouts may not include empty space between
 | 
				
			||||
         * RecyclerView items where band selection can be initiated.
 | 
				
			||||
         *
 | 
				
			||||
         * <p>
 | 
				
			||||
         * For example. You may present a single column list of contact names in a
 | 
				
			||||
         * RecyclerView instance in which the individual view items expand to fill all
 | 
				
			||||
         * available space.
 | 
				
			||||
         * But within the expanded view item after the contact name there may be empty space that a
 | 
				
			||||
         * user would reasonably expect to initiate band selection. When a MotionEvent occurs
 | 
				
			||||
         * in such an area, you should return identify this as NOT in a drag region.
 | 
				
			||||
         *
 | 
				
			||||
         * <p>
 | 
				
			||||
         * Further more, within a drag region, a mouse click and drag will immediately
 | 
				
			||||
         * initiate drag and drop (if supported by your configuration).
 | 
				
			||||
         *
 | 
				
			||||
         * @return true if the item is in an area of the item that can result in dragging
 | 
				
			||||
         * the item. List items frequently have a white area that is not draggable allowing
 | 
				
			||||
         * mouse driven band selection to be initiated in that area.
 | 
				
			||||
         */
 | 
				
			||||
        public boolean inDragRegion(@NonNull MotionEvent e) {
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public boolean equals(@Nullable Object obj) {
 | 
				
			||||
            return (obj instanceof ItemDetails)
 | 
				
			||||
                    && isEqualTo((ItemDetails) obj);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        private boolean isEqualTo(@NonNull ItemDetails other) {
 | 
				
			||||
            K key = getSelectionKey();
 | 
				
			||||
            boolean sameKeys = false;
 | 
				
			||||
            if (key == null) {
 | 
				
			||||
                sameKeys = other.getSelectionKey() == null;
 | 
				
			||||
            } else {
 | 
				
			||||
                sameKeys = key.equals(other.getSelectionKey());
 | 
				
			||||
            }
 | 
				
			||||
            return sameKeys && this.getPosition() == other.getPosition();
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public int hashCode() {
 | 
				
			||||
            return getPosition() >>> 8;
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,85 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.IntDef;
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
 | 
				
			||||
import java.lang.annotation.Retention;
 | 
				
			||||
import java.lang.annotation.RetentionPolicy;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Provides selection library access to stable selection keys identifying items
 | 
				
			||||
 * presented by a {@link RecyclerView RecyclerView} instance.
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
 */
 | 
				
			||||
public abstract class ItemKeyProvider<K> {
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Provides access to all data, regardless of whether it is bound to a view or not.
 | 
				
			||||
     * Key providers with this access type enjoy support for enhanced features like:
 | 
				
			||||
     * SHIFT+click range selection, and band selection.
 | 
				
			||||
     */
 | 
				
			||||
    public static final int SCOPE_MAPPED = 0;
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Provides access to cached data based for items that were recently bound in the view.
 | 
				
			||||
     * Employing this provider will result in a reduced feature-set, as some
 | 
				
			||||
     * features like SHIFT+click range selection and band selection are dependent
 | 
				
			||||
     * on mapped access.
 | 
				
			||||
     */
 | 
				
			||||
    public static final int SCOPE_CACHED = 1;
 | 
				
			||||
 | 
				
			||||
    @IntDef({
 | 
				
			||||
            SCOPE_MAPPED,
 | 
				
			||||
            SCOPE_CACHED
 | 
				
			||||
    })
 | 
				
			||||
    @Retention(RetentionPolicy.SOURCE)
 | 
				
			||||
    public @interface Scope {}
 | 
				
			||||
 | 
				
			||||
    private final @Scope int mScope;
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Creates a new provider with the given scope.
 | 
				
			||||
     *
 | 
				
			||||
     * @param scope Scope can't be changed at runtime.
 | 
				
			||||
     */
 | 
				
			||||
    protected ItemKeyProvider(@Scope int scope) {
 | 
				
			||||
        checkArgument(scope == SCOPE_MAPPED || scope == SCOPE_CACHED);
 | 
				
			||||
 | 
				
			||||
        mScope = scope;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    final boolean hasAccess(@Scope int scope) {
 | 
				
			||||
        return scope == mScope;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return The selection key at the given adapter position, or null.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract @Nullable K getKey(int position);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return the position corresponding to the selection key, or RecyclerView.NO_POSITION.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract int getPosition(@NonNull K key);
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,120 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import android.graphics.Point;
 | 
				
			||||
import android.view.KeyEvent;
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Utility methods for working with {@link MotionEvent} instances.
 | 
				
			||||
 */
 | 
				
			||||
final class MotionEvents {
 | 
				
			||||
 | 
				
			||||
    private MotionEvents() {}
 | 
				
			||||
 | 
				
			||||
    static boolean isMouseEvent(@NonNull MotionEvent e) {
 | 
				
			||||
        return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isTouchEvent(@NonNull MotionEvent e) {
 | 
				
			||||
        return e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isActionMove(@NonNull MotionEvent e) {
 | 
				
			||||
        return e.getActionMasked() == MotionEvent.ACTION_MOVE;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isActionDown(@NonNull MotionEvent e) {
 | 
				
			||||
        return e.getActionMasked() == MotionEvent.ACTION_DOWN;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isActionUp(@NonNull MotionEvent e) {
 | 
				
			||||
        return e.getActionMasked() == MotionEvent.ACTION_UP;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isActionPointerUp(@NonNull MotionEvent e) {
 | 
				
			||||
        return e.getActionMasked() == MotionEvent.ACTION_POINTER_UP;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @SuppressWarnings("unused")
 | 
				
			||||
    static boolean isActionPointerDown(@NonNull MotionEvent e) {
 | 
				
			||||
        return e.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isActionCancel(@NonNull MotionEvent e) {
 | 
				
			||||
        return e.getActionMasked() == MotionEvent.ACTION_CANCEL;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static Point getOrigin(@NonNull MotionEvent e) {
 | 
				
			||||
        return new Point((int) e.getX(), (int) e.getY());
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isPrimaryMouseButtonPressed(@NonNull MotionEvent e) {
 | 
				
			||||
        return isButtonPressed(e, MotionEvent.BUTTON_PRIMARY);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isSecondaryMouseButtonPressed(@NonNull MotionEvent e) {
 | 
				
			||||
        return isButtonPressed(e, MotionEvent.BUTTON_SECONDARY);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isTertiaryMouseButtonPressed(@NonNull MotionEvent e) {
 | 
				
			||||
        return isButtonPressed(e, MotionEvent.BUTTON_TERTIARY);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    // NOTE: Can replace this with MotionEvent.isButtonPressed once targeting 21 or higher.
 | 
				
			||||
    private static boolean isButtonPressed(MotionEvent e, int button) {
 | 
				
			||||
        if (button == 0) {
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
        return (e.getButtonState() & button) == button;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isShiftKeyPressed(@NonNull MotionEvent e) {
 | 
				
			||||
        return hasBit(e.getMetaState(), KeyEvent.META_SHIFT_ON);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isCtrlKeyPressed(@NonNull MotionEvent e) {
 | 
				
			||||
        return hasBit(e.getMetaState(), KeyEvent.META_CTRL_ON);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isAltKeyPressed(@NonNull MotionEvent e) {
 | 
				
			||||
        return hasBit(e.getMetaState(), KeyEvent.META_ALT_ON);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean isTouchpadScroll(@NonNull MotionEvent e) {
 | 
				
			||||
        // Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons
 | 
				
			||||
        // returned.
 | 
				
			||||
        return isMouseEvent(e) && isActionMove(e) && e.getButtonState() == 0;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Returns true if the event is a drag event (which is presumbaly, but not
 | 
				
			||||
     * explicitly required to be a mouse event).
 | 
				
			||||
     * @param e
 | 
				
			||||
     */
 | 
				
			||||
    static boolean isPointerDragEvent(MotionEvent e) {
 | 
				
			||||
        return isPrimaryMouseButtonPressed(e)
 | 
				
			||||
                && isActionMove(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private static boolean hasBit(int metaState, int bit) {
 | 
				
			||||
        return (metaState & bit) != 0;
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,112 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
import static androidx.core.util.Preconditions.checkState;
 | 
				
			||||
 | 
				
			||||
import android.view.GestureDetector.SimpleOnGestureListener;
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Base class for handlers that can be registered w/ {@link GestureRouter}.
 | 
				
			||||
 */
 | 
				
			||||
abstract class MotionInputHandler<K> extends SimpleOnGestureListener {
 | 
				
			||||
 | 
				
			||||
    protected final SelectionTracker<K> mSelectionTracker;
 | 
				
			||||
 | 
				
			||||
    private final ItemKeyProvider<K> mKeyProvider;
 | 
				
			||||
    private final FocusDelegate<K> mFocusDelegate;
 | 
				
			||||
 | 
				
			||||
    MotionInputHandler(
 | 
				
			||||
            @NonNull SelectionTracker<K> selectionTracker,
 | 
				
			||||
            @NonNull ItemKeyProvider<K> keyProvider,
 | 
				
			||||
            @NonNull FocusDelegate<K> focusDelegate) {
 | 
				
			||||
 | 
				
			||||
        checkArgument(selectionTracker != null);
 | 
				
			||||
        checkArgument(keyProvider != null);
 | 
				
			||||
        checkArgument(focusDelegate != null);
 | 
				
			||||
 | 
				
			||||
        mSelectionTracker = selectionTracker;
 | 
				
			||||
        mKeyProvider = keyProvider;
 | 
				
			||||
        mFocusDelegate = focusDelegate;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    final boolean selectItem(@NonNull ItemDetails<K> details) {
 | 
				
			||||
        checkArgument(details != null);
 | 
				
			||||
        checkArgument(hasPosition(details));
 | 
				
			||||
        checkArgument(hasSelectionKey(details));
 | 
				
			||||
 | 
				
			||||
        if (mSelectionTracker.select(details.getSelectionKey())) {
 | 
				
			||||
            mSelectionTracker.anchorRange(details.getPosition());
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // we set the focus on this doc so it will be the origin for keyboard events or shift+clicks
 | 
				
			||||
        // if there is only a single item selected, otherwise clear focus
 | 
				
			||||
        if (mSelectionTracker.getSelection().size() == 1) {
 | 
				
			||||
            mFocusDelegate.focusItem(details);
 | 
				
			||||
        } else {
 | 
				
			||||
            mFocusDelegate.clearFocus();
 | 
				
			||||
        }
 | 
				
			||||
        return true;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    protected final boolean focusItem(@NonNull ItemDetails<K> details) {
 | 
				
			||||
        checkArgument(details != null);
 | 
				
			||||
        checkArgument(hasSelectionKey(details));
 | 
				
			||||
 | 
				
			||||
        mSelectionTracker.clearSelection();
 | 
				
			||||
        mFocusDelegate.focusItem(details);
 | 
				
			||||
        return true;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    protected final void extendSelectionRange(@NonNull ItemDetails<K> details) {
 | 
				
			||||
        checkState(mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED));
 | 
				
			||||
        checkArgument(hasPosition(details));
 | 
				
			||||
        checkArgument(hasSelectionKey(details));
 | 
				
			||||
 | 
				
			||||
        mSelectionTracker.extendRange(details.getPosition());
 | 
				
			||||
        mFocusDelegate.focusItem(details);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    final boolean isRangeExtension(@NonNull MotionEvent e) {
 | 
				
			||||
        return MotionEvents.isShiftKeyPressed(e)
 | 
				
			||||
                && mSelectionTracker.isRangeActive()
 | 
				
			||||
                // Without full corpus access we can't reliably implement range
 | 
				
			||||
                // as a user can scroll *anywhere* then SHIFT+click.
 | 
				
			||||
                && mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    boolean shouldClearSelection(@NonNull MotionEvent e, @NonNull ItemDetails<K> item) {
 | 
				
			||||
        return !MotionEvents.isCtrlKeyPressed(e)
 | 
				
			||||
                && !item.inSelectionHotspot(e)
 | 
				
			||||
                && !mSelectionTracker.isSelected(item.getSelectionKey());
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean hasSelectionKey(@Nullable ItemDetails<?> item) {
 | 
				
			||||
        return item != null && item.getSelectionKey() != null;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static boolean hasPosition(@Nullable ItemDetails<?> item) {
 | 
				
			||||
        return item != null && item.getPosition() != RecyclerView.NO_POSITION;
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,224 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
import static androidx.core.util.Preconditions.checkState;
 | 
				
			||||
import static androidx.recyclerview.selection.Shared.DEBUG;
 | 
				
			||||
import static androidx.recyclerview.selection.Shared.VERBOSE;
 | 
				
			||||
 | 
				
			||||
import android.util.Log;
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * A MotionInputHandler that provides the high-level glue for mouse driven selection. This
 | 
				
			||||
 * class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper}
 | 
				
			||||
 * to implement the primary policies around mouse input.
 | 
				
			||||
 */
 | 
				
			||||
final class MouseInputHandler<K> extends MotionInputHandler<K> {
 | 
				
			||||
 | 
				
			||||
    private static final String TAG = "MouseInputDelegate";
 | 
				
			||||
 | 
				
			||||
    private final ItemDetailsLookup<K> mDetailsLookup;
 | 
				
			||||
    private final OnContextClickListener mOnContextClickListener;
 | 
				
			||||
    private final OnItemActivatedListener<K> mOnItemActivatedListener;
 | 
				
			||||
    private final FocusDelegate<K> mFocusDelegate;
 | 
				
			||||
 | 
				
			||||
    // The event has been handled in onSingleTapUp
 | 
				
			||||
    private boolean mHandledTapUp;
 | 
				
			||||
    // true when the previous event has consumed a right click motion event
 | 
				
			||||
    private boolean mHandledOnDown;
 | 
				
			||||
 | 
				
			||||
    MouseInputHandler(
 | 
				
			||||
            @NonNull SelectionTracker<K> selectionTracker,
 | 
				
			||||
            @NonNull ItemKeyProvider<K> keyProvider,
 | 
				
			||||
            @NonNull ItemDetailsLookup<K> detailsLookup,
 | 
				
			||||
            @NonNull OnContextClickListener onContextClickListener,
 | 
				
			||||
            @NonNull OnItemActivatedListener<K> onItemActivatedListener,
 | 
				
			||||
            @NonNull FocusDelegate<K> focusDelegate) {
 | 
				
			||||
 | 
				
			||||
        super(selectionTracker, keyProvider, focusDelegate);
 | 
				
			||||
 | 
				
			||||
        checkArgument(detailsLookup != null);
 | 
				
			||||
        checkArgument(onContextClickListener != null);
 | 
				
			||||
        checkArgument(onItemActivatedListener != null);
 | 
				
			||||
 | 
				
			||||
        mDetailsLookup = detailsLookup;
 | 
				
			||||
        mOnContextClickListener = onContextClickListener;
 | 
				
			||||
        mOnItemActivatedListener = onItemActivatedListener;
 | 
				
			||||
        mFocusDelegate = focusDelegate;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onDown(@NonNull MotionEvent e) {
 | 
				
			||||
        if (VERBOSE) Log.v(TAG, "Delegated onDown event.");
 | 
				
			||||
        if ((MotionEvents.isAltKeyPressed(e) && MotionEvents.isPrimaryMouseButtonPressed(e))
 | 
				
			||||
                || MotionEvents.isSecondaryMouseButtonPressed(e)) {
 | 
				
			||||
            mHandledOnDown = true;
 | 
				
			||||
            return onRightClick(e);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        return false;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
 | 
				
			||||
            float distanceX, float distanceY) {
 | 
				
			||||
        // Don't scroll content window in response to mouse drag
 | 
				
			||||
        // If it's two-finger trackpad scrolling, we want to scroll
 | 
				
			||||
        return !MotionEvents.isTouchpadScroll(e2);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onSingleTapUp(@NonNull MotionEvent e) {
 | 
				
			||||
        // See b/27377794. Since we don't get a button state back from UP events, we have to
 | 
				
			||||
        // explicitly save this state to know whether something was previously handled by
 | 
				
			||||
        // DOWN events or not.
 | 
				
			||||
        if (mHandledOnDown) {
 | 
				
			||||
            if (VERBOSE) Log.v(TAG, "Ignoring onSingleTapUp, previously handled in onDown.");
 | 
				
			||||
            mHandledOnDown = false;
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (!mDetailsLookup.overItemWithSelectionKey(e)) {
 | 
				
			||||
            if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
 | 
				
			||||
            mSelectionTracker.clearSelection();
 | 
				
			||||
            mFocusDelegate.clearFocus();
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
 | 
				
			||||
            if (DEBUG) Log.d(TAG, "Ignoring middle click");
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (mSelectionTracker.hasSelection()) {
 | 
				
			||||
            onItemClick(e, mDetailsLookup.getItemDetails(e));
 | 
				
			||||
            mHandledTapUp = true;
 | 
				
			||||
            return true;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        return false;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    // tap on an item when there is an existing selection. We could extend
 | 
				
			||||
    // a selection, we could clear selection (then launch)
 | 
				
			||||
    private void onItemClick(@NonNull MotionEvent e, @NonNull ItemDetails<K> item) {
 | 
				
			||||
        checkState(mSelectionTracker.hasSelection());
 | 
				
			||||
        checkArgument(item != null);
 | 
				
			||||
 | 
				
			||||
        if (isRangeExtension(e)) {
 | 
				
			||||
            extendSelectionRange(item);
 | 
				
			||||
        } else {
 | 
				
			||||
            if (shouldClearSelection(e, item)) {
 | 
				
			||||
                mSelectionTracker.clearSelection();
 | 
				
			||||
            }
 | 
				
			||||
            if (mSelectionTracker.isSelected(item.getSelectionKey())) {
 | 
				
			||||
                if (mSelectionTracker.deselect(item.getSelectionKey())) {
 | 
				
			||||
                    mFocusDelegate.clearFocus();
 | 
				
			||||
                }
 | 
				
			||||
            } else {
 | 
				
			||||
                selectOrFocusItem(item, e);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
 | 
				
			||||
        if (mHandledTapUp) {
 | 
				
			||||
            if (VERBOSE) {
 | 
				
			||||
                Log.v(TAG,
 | 
				
			||||
                        "Ignoring onSingleTapConfirmed, previously handled in onSingleTapUp.");
 | 
				
			||||
            }
 | 
				
			||||
            mHandledTapUp = false;
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (mSelectionTracker.hasSelection()) {
 | 
				
			||||
            return false;  // should have been handled by onSingleTapUp.
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (!mDetailsLookup.overItem(e)) {
 | 
				
			||||
            if (DEBUG) Log.d(TAG, "Ignoring Confirmed Tap on non-item.");
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
 | 
				
			||||
            if (DEBUG) Log.d(TAG, "Ignoring middle click");
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
 | 
				
			||||
        if (item == null || !item.hasSelectionKey()) {
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (mFocusDelegate.hasFocusedItem() && MotionEvents.isShiftKeyPressed(e)) {
 | 
				
			||||
            mSelectionTracker.startRange(mFocusDelegate.getFocusedPosition());
 | 
				
			||||
            mSelectionTracker.extendRange(item.getPosition());
 | 
				
			||||
        } else {
 | 
				
			||||
            selectOrFocusItem(item, e);
 | 
				
			||||
        }
 | 
				
			||||
        return true;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onDoubleTap(@NonNull MotionEvent e) {
 | 
				
			||||
        mHandledTapUp = false;
 | 
				
			||||
 | 
				
			||||
        if (!mDetailsLookup.overItemWithSelectionKey(e)) {
 | 
				
			||||
            if (DEBUG) Log.d(TAG, "Ignoring DoubleTap on non-model-backed item.");
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
 | 
				
			||||
            if (DEBUG) Log.d(TAG, "Ignoring middle click");
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
 | 
				
			||||
        return (item != null) && mOnItemActivatedListener.onItemActivated(item, e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private boolean onRightClick(@NonNull MotionEvent e) {
 | 
				
			||||
        if (mDetailsLookup.overItemWithSelectionKey(e)) {
 | 
				
			||||
            @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
 | 
				
			||||
            if (item != null && !mSelectionTracker.isSelected(item.getSelectionKey())) {
 | 
				
			||||
                mSelectionTracker.clearSelection();
 | 
				
			||||
                selectItem(item);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // We always delegate final handling of the event,
 | 
				
			||||
        // since the handler might want to show a context menu
 | 
				
			||||
        // in an empty area or some other weirdo view.
 | 
				
			||||
        return mOnContextClickListener.onContextClick(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private void selectOrFocusItem(@NonNull ItemDetails<K> item, @NonNull MotionEvent e) {
 | 
				
			||||
        if (item.inSelectionHotspot(e) || MotionEvents.isCtrlKeyPressed(e)) {
 | 
				
			||||
            selectItem(item);
 | 
				
			||||
        } else {
 | 
				
			||||
            focusItem(item);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,76 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Subclass of {@link Selection} exposing public support for mutating the underlying
 | 
				
			||||
 * selection data. This is useful for clients of {@link SelectionTracker} that wish to
 | 
				
			||||
 * manipulate a copy of selection data obtained via
 | 
				
			||||
 * {@link SelectionTracker#copySelection(MutableSelection)}.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * While the {@link Selection} class is not intrinsically immutable, it is not mutable
 | 
				
			||||
 * by non-library code. Furthermore the value returned from {@link SelectionTracker#getSelection()}
 | 
				
			||||
 * is a live view of the underlying selection, mutable by the library itself.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * {@link MutableSelection} allows clients to obtain a mutable copy of the Selection
 | 
				
			||||
 * state held by the selection library. This is useful in situations where a stable
 | 
				
			||||
 * snapshot of the selection is required.
 | 
				
			||||
 *
 | 
				
			||||
 *
 | 
				
			||||
 * <p><b>Example</b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <pre>
 | 
				
			||||
 * MutableSelection snapshot = new MutableSelection();
 | 
				
			||||
 * selectionTracker.copySelection(snapshot);
 | 
				
			||||
 *
 | 
				
			||||
 * // Clear the user visible selection.
 | 
				
			||||
 * selectionTracker.clearSelection();
 | 
				
			||||
 * // tracker.getSelection().isEmpty() will be true.
 | 
				
			||||
 * // shapshot has a copy of the previous selection.
 | 
				
			||||
 * </pre>
 | 
				
			||||
 *
 | 
				
			||||
 * @see android.text.Selection
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
 */
 | 
				
			||||
public final class MutableSelection<K> extends Selection<K> {
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean add(@NonNull K key) {
 | 
				
			||||
        return super.add(key);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean remove(@NonNull K key) {
 | 
				
			||||
        return super.remove(key);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void copyFrom(@NonNull Selection<K> source) {
 | 
				
			||||
        super.copyFrom(source);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void clear() {
 | 
				
			||||
        super.clear();
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,41 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Override methods in this class to provide application specific behaviors
 | 
				
			||||
 * related to mouse input.
 | 
				
			||||
 */
 | 
				
			||||
/**
 | 
				
			||||
 * Register an OnContextClickListener to be notified when a context click
 | 
				
			||||
 * occurs.
 | 
				
			||||
 */
 | 
				
			||||
public interface OnContextClickListener {
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Called when user performs a context click, usually via mouse pointer
 | 
				
			||||
     * right-click.
 | 
				
			||||
     *
 | 
				
			||||
     * @param e the event associated with the click.
 | 
				
			||||
     * @return true if the event was handled.
 | 
				
			||||
     */
 | 
				
			||||
    boolean onContextClick(@NonNull MotionEvent e);
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,73 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
 | 
				
			||||
 | 
				
			||||
import android.content.ClipData;
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
import android.view.View;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Register an OnDragInitiatedListener to be notified when user intent to perform drag and drop
 | 
				
			||||
 * operations on an item or items has been detected. Handle these events using {@link View}
 | 
				
			||||
 * support for Drag and drop.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * See {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)}
 | 
				
			||||
 * for details.
 | 
				
			||||
 */
 | 
				
			||||
public interface OnDragInitiatedListener {
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Called when user intent to perform a drag and drop operation has been detected.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * The following circumstances are considered to be expressing drag and drop intent:
 | 
				
			||||
     *
 | 
				
			||||
     * <ol>
 | 
				
			||||
     *     <li>Long press on selected item.</li>
 | 
				
			||||
     *     <li>Click and drag in the {@link ItemDetails#inDragRegion(MotionEvent) drag region}
 | 
				
			||||
     *     of selected item with a pointer device.</li>
 | 
				
			||||
     *     <li>Click and drag in drag region of un-selected item with a pointer device.</li>
 | 
				
			||||
     * </ol>
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * The RecyclerView item at the coordinates of the MotionEvent is not supplied as a parameter
 | 
				
			||||
     * to this method as there may be multiple items selected or no items selected (as may be
 | 
				
			||||
     * the case in pointer drive drag and drop.)
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * Obtain the current list of selected items from
 | 
				
			||||
     * {@link SelectionTracker#copySelection(MutableSelection)}. If there is no selection
 | 
				
			||||
     * get the item under the event using {@link ItemDetailsLookup#getItemDetails(MotionEvent)}.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * Drag region used with pointer devices is specified by
 | 
				
			||||
     * {@link ItemDetails#inDragRegion(MotionEvent)}
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * See {@link android.view.View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)}
 | 
				
			||||
     * for details on drag and drop implementation.
 | 
				
			||||
     *
 | 
				
			||||
     * @param e the event associated with the drag.
 | 
				
			||||
     * @return true if drag and drop was initiated.
 | 
				
			||||
     */
 | 
				
			||||
    boolean onDragInitiated(@NonNull MotionEvent e);
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,43 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Register an OnItemActivatedListener to be notified when an item is activated
 | 
				
			||||
 * (tapped or double clicked).
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
 */
 | 
				
			||||
public interface OnItemActivatedListener<K> {
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Called when an item is "activated". An item is activated, for example, when no selection
 | 
				
			||||
     * exists and the user taps an item with her finger, or double clicks an item with a
 | 
				
			||||
     * pointing device like a Mouse.
 | 
				
			||||
     *
 | 
				
			||||
     * @param item details of the item.
 | 
				
			||||
     * @param e the event associated with item.
 | 
				
			||||
     *
 | 
				
			||||
     * @return true if the event was handled.
 | 
				
			||||
     */
 | 
				
			||||
    boolean onItemActivated(@NonNull ItemDetails<K> item, @NonNull MotionEvent e);
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,131 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
import static androidx.core.util.Preconditions.checkState;
 | 
				
			||||
import static androidx.recyclerview.selection.Shared.DEBUG;
 | 
				
			||||
 | 
				
			||||
import android.util.Log;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.MainThread;
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
 | 
				
			||||
import java.util.ArrayList;
 | 
				
			||||
import java.util.List;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * OperationMonitor provides a mechanism to coordinate application
 | 
				
			||||
 * logic with ongoing user selection activities (such as active band selection
 | 
				
			||||
 * and active gesture selection).
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * The host {@link android.app.Activity} or {@link android.app.Fragment} should avoid changing
 | 
				
			||||
 * {@link RecyclerView.Adapter Adapter} data while there
 | 
				
			||||
 * are active selection operations, as this can result in a poor user experience.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * To know when an operation is active listen to changes using an {@link OnChangeListener}.
 | 
				
			||||
 */
 | 
				
			||||
public final class OperationMonitor {
 | 
				
			||||
 | 
				
			||||
    private static final String TAG = "OperationMonitor";
 | 
				
			||||
 | 
				
			||||
    private int mNumOps = 0;
 | 
				
			||||
    private List<OnChangeListener> mListeners = new ArrayList<>();
 | 
				
			||||
 | 
				
			||||
    @MainThread
 | 
				
			||||
    synchronized void start() {
 | 
				
			||||
        mNumOps++;
 | 
				
			||||
 | 
				
			||||
        if (mNumOps == 1) {
 | 
				
			||||
            for (OnChangeListener l : mListeners) {
 | 
				
			||||
                l.onChanged();
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + ".");
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @MainThread
 | 
				
			||||
    synchronized void stop() {
 | 
				
			||||
        checkState(mNumOps > 0);
 | 
				
			||||
 | 
				
			||||
        mNumOps--;
 | 
				
			||||
        if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mNumOps + ".");
 | 
				
			||||
 | 
				
			||||
        if (mNumOps == 0) {
 | 
				
			||||
            for (OnChangeListener l : mListeners) {
 | 
				
			||||
                l.onChanged();
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return true if there are any running operations.
 | 
				
			||||
     */
 | 
				
			||||
    @SuppressWarnings("unused")
 | 
				
			||||
    public synchronized boolean isStarted() {
 | 
				
			||||
        return mNumOps > 0;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Registers supplied listener to be notified when operation status changes.
 | 
				
			||||
     * @param listener
 | 
				
			||||
     */
 | 
				
			||||
    public void addListener(@NonNull OnChangeListener listener) {
 | 
				
			||||
        checkArgument(listener != null);
 | 
				
			||||
        mListeners.add(listener);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Unregisters listener for further notifications.
 | 
				
			||||
     * @param listener
 | 
				
			||||
     */
 | 
				
			||||
    public void removeListener(@NonNull OnChangeListener listener) {
 | 
				
			||||
        checkArgument(listener != null);
 | 
				
			||||
        mListeners.remove(listener);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Allows other selection code to perform a precondition check asserting the state is locked.
 | 
				
			||||
     */
 | 
				
			||||
    void checkStarted() {
 | 
				
			||||
        checkState(mNumOps > 0);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Allows other selection code to perform a precondition check asserting the state is unlocked.
 | 
				
			||||
     */
 | 
				
			||||
    void checkStopped() {
 | 
				
			||||
        checkState(mNumOps == 0);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Listen to changes in operation status. Authors should avoid
 | 
				
			||||
     * changing the Adapter model while there are active operations.
 | 
				
			||||
     */
 | 
				
			||||
    public interface OnChangeListener {
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Called when operation status changes. Call {@link OperationMonitor#isStarted()}
 | 
				
			||||
         * to determine the current status.
 | 
				
			||||
         */
 | 
				
			||||
        void onChanged();
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,75 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2018 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * OnItemTouchListener that delegates drag events to a drag listener,
 | 
				
			||||
 * else sends event to fallback {@link OnItemTouchListener}.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>See {@link OnDragInitiatedListener} for details on implementing drag and drop.
 | 
				
			||||
 */
 | 
				
			||||
final class PointerDragEventInterceptor implements OnItemTouchListener {
 | 
				
			||||
 | 
				
			||||
    private final ItemDetailsLookup mEventDetailsLookup;
 | 
				
			||||
    private final OnDragInitiatedListener mDragListener;
 | 
				
			||||
    private @Nullable OnItemTouchListener mDelegate;
 | 
				
			||||
 | 
				
			||||
    PointerDragEventInterceptor(
 | 
				
			||||
            ItemDetailsLookup eventDetailsLookup,
 | 
				
			||||
            OnDragInitiatedListener dragListener,
 | 
				
			||||
            @Nullable OnItemTouchListener delegate) {
 | 
				
			||||
 | 
				
			||||
        checkArgument(eventDetailsLookup != null);
 | 
				
			||||
        checkArgument(dragListener != null);
 | 
				
			||||
 | 
				
			||||
        mEventDetailsLookup = eventDetailsLookup;
 | 
				
			||||
        mDragListener = dragListener;
 | 
				
			||||
        mDelegate = delegate;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
 | 
				
			||||
        if (MotionEvents.isPointerDragEvent(e) && mEventDetailsLookup.inItemDragRegion(e)) {
 | 
				
			||||
            return mDragListener.onDragInitiated(e);
 | 
				
			||||
        } else if (mDelegate != null) {
 | 
				
			||||
            return mDelegate.onInterceptTouchEvent(rv, e);
 | 
				
			||||
        }
 | 
				
			||||
        return false;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
 | 
				
			||||
        if (mDelegate != null) {
 | 
				
			||||
            mDelegate.onTouchEvent(rv, e);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
 | 
				
			||||
        if (mDelegate != null) {
 | 
				
			||||
            mDelegate.onRequestDisallowInterceptTouchEvent(disallowIntercept);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,190 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
import static androidx.recyclerview.selection.Shared.DEBUG;
 | 
				
			||||
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
 | 
				
			||||
 | 
				
			||||
import android.util.Log;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.IntDef;
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
 | 
				
			||||
import java.lang.annotation.Retention;
 | 
				
			||||
import java.lang.annotation.RetentionPolicy;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Class providing support for managing range selections.
 | 
				
			||||
 */
 | 
				
			||||
final class Range {
 | 
				
			||||
 | 
				
			||||
    static final int TYPE_PRIMARY = 0;
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * "Provisional" selection represents a overlay on the primary selection. A provisional
 | 
				
			||||
     * selection maybe be eventually added to the primary selection, or it may be abandoned.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * E.g. BandSelectionHelper creates a provisional selection while a user is actively
 | 
				
			||||
     * selecting items with a band. GestureSelectionHelper creates a provisional selection
 | 
				
			||||
     * while a user is active selecting via gesture.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * Provisionally selected items are considered to be selected in
 | 
				
			||||
     * {@link Selection#contains(String)} and related methods. A provisional may be abandoned or
 | 
				
			||||
     * merged into the promary selection.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * A provisional selection may intersect with the primary selection, however clearing the
 | 
				
			||||
     * provisional selection will not affect the primary selection where the two may intersect.
 | 
				
			||||
     */
 | 
				
			||||
    static final int TYPE_PROVISIONAL = 1;
 | 
				
			||||
    @IntDef({
 | 
				
			||||
            TYPE_PRIMARY,
 | 
				
			||||
            TYPE_PROVISIONAL
 | 
				
			||||
    })
 | 
				
			||||
    @Retention(RetentionPolicy.SOURCE)
 | 
				
			||||
    @interface RangeType {}
 | 
				
			||||
 | 
				
			||||
    private static final String TAG = "Range";
 | 
				
			||||
 | 
				
			||||
    private final Callbacks mCallbacks;
 | 
				
			||||
    private final int mBegin;
 | 
				
			||||
    private int mEnd = NO_POSITION;
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Creates a new range anchored at {@code position}.
 | 
				
			||||
     *
 | 
				
			||||
     * @param position
 | 
				
			||||
     * @param callbacks
 | 
				
			||||
     */
 | 
				
			||||
    Range(int position, @NonNull Callbacks callbacks) {
 | 
				
			||||
        mBegin = position;
 | 
				
			||||
        mCallbacks = callbacks;
 | 
				
			||||
        if (DEBUG) Log.d(TAG, "Creating new Range anchored @ " + position);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    void extendRange(int position, @RangeType int type) {
 | 
				
			||||
        checkArgument(position != NO_POSITION, "Position cannot be NO_POSITION.");
 | 
				
			||||
 | 
				
			||||
        if (mEnd == NO_POSITION || mEnd == mBegin) {
 | 
				
			||||
            // Reset mEnd so it can be established in establishRange.
 | 
				
			||||
            mEnd = NO_POSITION;
 | 
				
			||||
            establishRange(position, type);
 | 
				
			||||
        } else {
 | 
				
			||||
            reviseRange(position, type);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private void establishRange(int position, @RangeType int type) {
 | 
				
			||||
        checkArgument(mEnd == NO_POSITION, "End has already been set.");
 | 
				
			||||
 | 
				
			||||
        mEnd = position;
 | 
				
			||||
 | 
				
			||||
        if (position > mBegin) {
 | 
				
			||||
            if (DEBUG) log(type, "Establishing initial range at @ " + position);
 | 
				
			||||
            updateRange(mBegin + 1, position, true, type);
 | 
				
			||||
        } else if (position < mBegin) {
 | 
				
			||||
            if (DEBUG) log(type, "Establishing initial range at @ " + position);
 | 
				
			||||
            updateRange(position, mBegin - 1, true, type);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private void reviseRange(int position, @RangeType int type) {
 | 
				
			||||
        checkArgument(mEnd != NO_POSITION, "End must already be set.");
 | 
				
			||||
        checkArgument(mBegin != mEnd, "Beging and end point to same position.");
 | 
				
			||||
 | 
				
			||||
        if (position == mEnd) {
 | 
				
			||||
            if (DEBUG) log(type, "Ignoring no-op revision for range @ " + position);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (mEnd > mBegin) {
 | 
				
			||||
            reviseAscending(position, type);
 | 
				
			||||
        } else if (mEnd < mBegin) {
 | 
				
			||||
            reviseDescending(position, type);
 | 
				
			||||
        }
 | 
				
			||||
        // the "else" case is covered by checkState at beginning of method.
 | 
				
			||||
 | 
				
			||||
        mEnd = position;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Updates an existing ascending selection.
 | 
				
			||||
     */
 | 
				
			||||
    private void reviseAscending(int position, @RangeType int type) {
 | 
				
			||||
        if (DEBUG) log(type, "*ascending* Revising range @ " + position);
 | 
				
			||||
 | 
				
			||||
        if (position < mEnd) {
 | 
				
			||||
            if (position < mBegin) {
 | 
				
			||||
                updateRange(mBegin + 1, mEnd, false, type);
 | 
				
			||||
                updateRange(position, mBegin - 1, true, type);
 | 
				
			||||
            } else {
 | 
				
			||||
                updateRange(position + 1, mEnd, false, type);
 | 
				
			||||
            }
 | 
				
			||||
        } else if (position > mEnd) {   // Extending the range...
 | 
				
			||||
            updateRange(mEnd + 1, position, true, type);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private void reviseDescending(int position, @RangeType int type) {
 | 
				
			||||
        if (DEBUG) log(type, "*descending* Revising range @ " + position);
 | 
				
			||||
 | 
				
			||||
        if (position > mEnd) {
 | 
				
			||||
            if (position > mBegin) {
 | 
				
			||||
                updateRange(mEnd, mBegin - 1, false, type);
 | 
				
			||||
                updateRange(mBegin + 1, position, true, type);
 | 
				
			||||
            } else {
 | 
				
			||||
                updateRange(mEnd, position - 1, false, type);
 | 
				
			||||
            }
 | 
				
			||||
        } else if (position < mEnd) {   // Extending the range...
 | 
				
			||||
            updateRange(position, mEnd - 1, true, type);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Try to set selection state for all elements in range. Not that callbacks can cancel
 | 
				
			||||
     * selection of specific items, so some or even all items may not reflect the desired state
 | 
				
			||||
     * after the update is complete.
 | 
				
			||||
     *
 | 
				
			||||
     * @param begin    Adapter position for range start (inclusive).
 | 
				
			||||
     * @param end      Adapter position for range end (inclusive).
 | 
				
			||||
     * @param selected New selection state.
 | 
				
			||||
     */
 | 
				
			||||
    private void updateRange(
 | 
				
			||||
            int begin, int end, boolean selected, @RangeType int type) {
 | 
				
			||||
        mCallbacks.updateForRange(begin, end, selected, type);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public String toString() {
 | 
				
			||||
        return "Range{begin=" + mBegin + ", end=" + mEnd + "}";
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private void log(@RangeType int type, String message) {
 | 
				
			||||
        String opType = type == TYPE_PRIMARY ? "PRIMARY" : "PROVISIONAL";
 | 
				
			||||
        Log.d(TAG, String.valueOf(this) + ": " + message + " (" + opType + ")");
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /*
 | 
				
			||||
     * @see {@link DefaultSelectionTracker#updateForRange(int, int , boolean, int)}.
 | 
				
			||||
     */
 | 
				
			||||
    abstract static class Callbacks {
 | 
				
			||||
        abstract void updateForRange(
 | 
				
			||||
                int begin, int end, boolean selected, @RangeType int type);
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,247 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
 | 
				
			||||
import java.util.HashMap;
 | 
				
			||||
import java.util.HashSet;
 | 
				
			||||
import java.util.Iterator;
 | 
				
			||||
import java.util.Map;
 | 
				
			||||
import java.util.Set;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Object representing a "primary" selection and a "provisional" selection.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * This class tracks selected items by managing two sets:
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Primary Selection</b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Primary selection consists of items selected by a user. This represents the selection
 | 
				
			||||
 * "at rest", as the selection does not contains items that are in a "provisional" selected
 | 
				
			||||
 * state created by way of an ongoing gesture or band operation.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Provisional Selection</b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Provisional selections are selections which are interim in nature.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Provisional selection exists to address issues where a transitory selection might
 | 
				
			||||
 * momentarily intersect with a previously established selection resulting in a some
 | 
				
			||||
 * or all of the established selection being erased. Such situations may arise
 | 
				
			||||
 * when band selection is being performed in "additive" mode (e.g. SHIFT or CTRL is pressed
 | 
				
			||||
 * on the keyboard prior to mouse down), or when there's an active gesture selection
 | 
				
			||||
 * (which can be initiated by long pressing an unselected item while there is an
 | 
				
			||||
 * existing selection).
 | 
				
			||||
 *
 | 
				
			||||
 * @see MutableSelection
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
 */
 | 
				
			||||
public class Selection<K> implements Iterable<K> {
 | 
				
			||||
 | 
				
			||||
    // NOTE: Not currently private as DefaultSelectionTracker directly manipulates values.
 | 
				
			||||
    final Set<K> mSelection;
 | 
				
			||||
    final Set<K> mProvisionalSelection;
 | 
				
			||||
 | 
				
			||||
    Selection() {
 | 
				
			||||
        mSelection = new HashSet<>();
 | 
				
			||||
        mProvisionalSelection = new HashSet<>();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Used by {@link StorageStrategy} when restoring selection.
 | 
				
			||||
     */
 | 
				
			||||
    Selection(@NonNull Set<K> selection) {
 | 
				
			||||
        mSelection = selection;
 | 
				
			||||
        mProvisionalSelection = new HashSet<>();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @param key
 | 
				
			||||
     * @return true if the position is currently selected.
 | 
				
			||||
     */
 | 
				
			||||
    public boolean contains(@Nullable K key) {
 | 
				
			||||
        return mSelection.contains(key) || mProvisionalSelection.contains(key);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Returns an {@link Iterator} that iterators over the selection, *excluding*
 | 
				
			||||
     * any provisional selection.
 | 
				
			||||
     *
 | 
				
			||||
     * {@inheritDoc}
 | 
				
			||||
     */
 | 
				
			||||
    @Override
 | 
				
			||||
    public Iterator<K> iterator() {
 | 
				
			||||
        return mSelection.iterator();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return size of the selection including both final and provisional selected items.
 | 
				
			||||
     */
 | 
				
			||||
    public int size() {
 | 
				
			||||
        return mSelection.size() + mProvisionalSelection.size();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return true if the selection is empty.
 | 
				
			||||
     */
 | 
				
			||||
    public boolean isEmpty() {
 | 
				
			||||
        return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Sets the provisional selection, which is a temporary selection that can be saved,
 | 
				
			||||
     * canceled, or adjusted at a later time. When a new provision selection is applied, the old
 | 
				
			||||
     * one (if it exists) is abandoned.
 | 
				
			||||
     * @return Map of ids added or removed. Added ids have a value of true, removed are false.
 | 
				
			||||
     */
 | 
				
			||||
    Map<K, Boolean> setProvisionalSelection(@NonNull Set<K> newSelection) {
 | 
				
			||||
        Map<K, Boolean> delta = new HashMap<>();
 | 
				
			||||
 | 
				
			||||
        for (K key: mProvisionalSelection) {
 | 
				
			||||
            // Mark each item that used to be in the provisional selection
 | 
				
			||||
            // but is not in the new provisional selection.
 | 
				
			||||
            if (!newSelection.contains(key) && !mSelection.contains(key)) {
 | 
				
			||||
                delta.put(key, false);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        for (K key: mSelection) {
 | 
				
			||||
            // Mark each item that used to be in the selection but is unsaved and not in the new
 | 
				
			||||
            // provisional selection.
 | 
				
			||||
            if (!newSelection.contains(key)) {
 | 
				
			||||
                delta.put(key, false);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        for (K key: newSelection) {
 | 
				
			||||
            // Mark each item that was not previously in the selection but is in the new
 | 
				
			||||
            // provisional selection.
 | 
				
			||||
            if (!mSelection.contains(key) && !mProvisionalSelection.contains(key)) {
 | 
				
			||||
                delta.put(key, true);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // Now, iterate through the changes and actually add/remove them to/from the current
 | 
				
			||||
        // selection. This could not be done in the previous loops because changing the size of
 | 
				
			||||
        // the selection mid-iteration changes iteration order erroneously.
 | 
				
			||||
        for (Map.Entry<K, Boolean> entry: delta.entrySet()) {
 | 
				
			||||
            K key = entry.getKey();
 | 
				
			||||
            if (entry.getValue()) {
 | 
				
			||||
                mProvisionalSelection.add(key);
 | 
				
			||||
            } else {
 | 
				
			||||
                mProvisionalSelection.remove(key);
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        return delta;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Saves the existing provisional selection. Once the provisional selection is saved,
 | 
				
			||||
     * subsequent provisional selections which are different from this existing one cannot
 | 
				
			||||
     * cause items in this existing provisional selection to become deselected.
 | 
				
			||||
     */
 | 
				
			||||
    void mergeProvisionalSelection() {
 | 
				
			||||
        mSelection.addAll(mProvisionalSelection);
 | 
				
			||||
        mProvisionalSelection.clear();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Abandons the existing provisional selection so that all items provisionally selected are
 | 
				
			||||
     * now deselected.
 | 
				
			||||
     */
 | 
				
			||||
    void clearProvisionalSelection() {
 | 
				
			||||
        mProvisionalSelection.clear();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Adds a new item to the primary selection.
 | 
				
			||||
     *
 | 
				
			||||
     * @return true if the operation resulted in a modification to the selection.
 | 
				
			||||
     */
 | 
				
			||||
    boolean add(@NonNull K key) {
 | 
				
			||||
        return mSelection.add(key);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Removes an item from the primary selection.
 | 
				
			||||
     *
 | 
				
			||||
     * @return true if the operation resulted in a modification to the selection.
 | 
				
			||||
     */
 | 
				
			||||
    boolean remove(@NonNull K key) {
 | 
				
			||||
        return mSelection.remove(key);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Clears the primary selection. The provisional selection, if any, is unaffected.
 | 
				
			||||
     */
 | 
				
			||||
    void clear() {
 | 
				
			||||
        mSelection.clear();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Clones primary and provisional selection from supplied {@link Selection}.
 | 
				
			||||
     * Does not copy active range data.
 | 
				
			||||
     */
 | 
				
			||||
    void copyFrom(@NonNull Selection<K> source) {
 | 
				
			||||
        mSelection.clear();
 | 
				
			||||
        mSelection.addAll(source.mSelection);
 | 
				
			||||
 | 
				
			||||
        mProvisionalSelection.clear();
 | 
				
			||||
        mProvisionalSelection.addAll(source.mProvisionalSelection);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public String toString() {
 | 
				
			||||
        if (size() <= 0) {
 | 
				
			||||
            return "size=0, items=[]";
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        StringBuilder buffer = new StringBuilder(size() * 28);
 | 
				
			||||
        buffer.append("Selection{")
 | 
				
			||||
            .append("primary{size=" + mSelection.size())
 | 
				
			||||
            .append(", entries=" + mSelection)
 | 
				
			||||
            .append("}, provisional{size=" + mProvisionalSelection.size())
 | 
				
			||||
            .append(", entries=" + mProvisionalSelection)
 | 
				
			||||
            .append("}}");
 | 
				
			||||
        return buffer.toString();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public int hashCode() {
 | 
				
			||||
        return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean equals(Object other) {
 | 
				
			||||
        return (this == other)
 | 
				
			||||
                || (other instanceof Selection && isEqualTo((Selection) other));
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private boolean isEqualTo(Selection other) {
 | 
				
			||||
        return mSelection.equals(other.mSelection)
 | 
				
			||||
                && mProvisionalSelection.equals(other.mProvisionalSelection);
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,81 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Utility class for creating SelectionPredicate instances. Provides default
 | 
				
			||||
 * implementations for common cases like "single selection" and "select anything".
 | 
				
			||||
 */
 | 
				
			||||
public final class SelectionPredicates {
 | 
				
			||||
 | 
				
			||||
    private SelectionPredicates() {}
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Returns a selection predicate that allows multiples items to be selected, without
 | 
				
			||||
     * any restrictions on which items can be selected.
 | 
				
			||||
     *
 | 
				
			||||
     * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
     * @return
 | 
				
			||||
     */
 | 
				
			||||
    public static <K> SelectionPredicate<K> createSelectAnything() {
 | 
				
			||||
        return new SelectionPredicate<K>() {
 | 
				
			||||
            @Override
 | 
				
			||||
            public boolean canSetStateForKey(@NonNull K key, boolean nextState) {
 | 
				
			||||
                return true;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Override
 | 
				
			||||
            public boolean canSetStateAtPosition(int position, boolean nextState) {
 | 
				
			||||
                return true;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Override
 | 
				
			||||
            public boolean canSelectMultiple() {
 | 
				
			||||
                return true;
 | 
				
			||||
            }
 | 
				
			||||
        };
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Returns a selection predicate that allows a single item to be selected, without
 | 
				
			||||
     * any restrictions on which item can be selected.
 | 
				
			||||
     *
 | 
				
			||||
     * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
     * @return
 | 
				
			||||
     */
 | 
				
			||||
    public static <K> SelectionPredicate<K> createSelectSingleAnything() {
 | 
				
			||||
        return new SelectionPredicate<K>() {
 | 
				
			||||
            @Override
 | 
				
			||||
            public boolean canSetStateForKey(@NonNull K key, boolean nextState) {
 | 
				
			||||
                return true;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Override
 | 
				
			||||
            public boolean canSetStateAtPosition(int position, boolean nextState) {
 | 
				
			||||
                return true;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Override
 | 
				
			||||
            public boolean canSelectMultiple() {
 | 
				
			||||
                return false;
 | 
				
			||||
            }
 | 
				
			||||
        };
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,817 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
 | 
				
			||||
import android.content.Context;
 | 
				
			||||
import android.os.Bundle;
 | 
				
			||||
import android.os.Parcelable;
 | 
				
			||||
import android.view.GestureDetector;
 | 
				
			||||
import android.view.HapticFeedbackConstants;
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.DrawableRes;
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 | 
				
			||||
 | 
				
			||||
import java.util.Set;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * SelectionTracker provides support for managing a selection of items in a RecyclerView instance.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * This class provides support for managing a "primary" set of selected items,
 | 
				
			||||
 * in addition to a "provisional" set of selected items using conventional
 | 
				
			||||
 * {@link java.util.Collections}-like methods.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Create an instance of SelectionTracker using {@link Builder SelectionTracker.Builder}.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Inspecting the current selection</b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * The underlying selection is described by the {@link Selection} class.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * A live view of the current selection can be obtained using {@link #getSelection}. Changes made
 | 
				
			||||
 * to the selection using SelectionTracker will be immediately reflected in this instance.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * To obtain a stable snapshot of the selection use {@link #copySelection(MutableSelection)}.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Selection state for an individual item can be obtained using {@link #isSelected(Object)}.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Provisional Selection</b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Provisional selection exists to address issues where a transitory selection might
 | 
				
			||||
 * momentarily intersect with a previously established selection resulting in a some
 | 
				
			||||
 * or all of the established selection being erased. Such situations may arise
 | 
				
			||||
 * when band selection is being performed in "additive" mode (e.g. SHIFT or CTRL is pressed
 | 
				
			||||
 * on the keyboard prior to mouse down), or when there's an active gesture selection
 | 
				
			||||
 * (which can be initiated by long pressing an unselected item while there is an
 | 
				
			||||
 * existing selection).
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * A provisional selection can be abandoned, or merged into the primary selection.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Enforcing selection policies</b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Which items can be selected by the user is a matter of policy in an Application.
 | 
				
			||||
 * Developers supply these policies by way of {@link SelectionPredicate}.
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
 */
 | 
				
			||||
public abstract class SelectionTracker<K> {
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * This value is included in the payload when SelectionTracker notifies RecyclerView
 | 
				
			||||
     * of changes to selection. Look for this value in the {@code payload}
 | 
				
			||||
     * Object argument supplied to
 | 
				
			||||
     * {@link RecyclerView.Adapter#onBindViewHolder
 | 
				
			||||
     *     Adapter#onBindViewHolder}.
 | 
				
			||||
     * If present the call is occurring in response to a selection state change.
 | 
				
			||||
     * This would be a good opportunity to animate changes between unselected and selected state.
 | 
				
			||||
     * When state is being restored, this argument will not be present.
 | 
				
			||||
     */
 | 
				
			||||
    public static final String SELECTION_CHANGED_MARKER = "Selection-Changed";
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Adds {@code observer} to be notified when changes to selection occur.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * Use an observer to track attributes about the selection and
 | 
				
			||||
     * update the UI to reflect the state of the selection. For example, an author
 | 
				
			||||
     * may use an observer to control the enabled status of menu items,
 | 
				
			||||
     * or to initiate {@link android.view.ActionMode}.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract void addObserver(SelectionObserver observer);
 | 
				
			||||
 | 
				
			||||
    /** @return true if has a selection */
 | 
				
			||||
    public abstract boolean hasSelection();
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Returns a Selection object that provides a live view on the current selection.
 | 
				
			||||
     *
 | 
				
			||||
     * @return The current selection.
 | 
				
			||||
     * @see #copySelection(MutableSelection) on how to get a snapshot
 | 
				
			||||
     * of the selection that will not reflect future changes
 | 
				
			||||
     * to selection.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract Selection<K> getSelection();
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Updates {@code dest} to reflect the current selection.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract void copySelection(@NonNull MutableSelection<K> dest);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return true if the item specified by its id is selected. Shorthand for
 | 
				
			||||
     * {@code getSelection().contains(K)}.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract boolean isSelected(@Nullable K key);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Restores the selected state of specified items. Used in cases such as restore the selection
 | 
				
			||||
     * after rotation etc. Provisional selection is not restored.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * This affords clients the ability to restore selection from selection saved
 | 
				
			||||
     * in Activity state.
 | 
				
			||||
     *
 | 
				
			||||
     * @see StorageStrategy details on selection state support.
 | 
				
			||||
     *
 | 
				
			||||
     * @param selection selection being restored.
 | 
				
			||||
     */
 | 
				
			||||
    protected abstract void restoreSelection(@NonNull Selection<K> selection);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Clears both primary and provisional selections.
 | 
				
			||||
     *
 | 
				
			||||
     * @return true if primary selection changed.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract boolean clearSelection();
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Sets the selected state of the specified items if permitted after consulting
 | 
				
			||||
     * SelectionPredicate.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract boolean setItemsSelected(@NonNull Iterable<K> keys, boolean selected);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Attempts to select an item.
 | 
				
			||||
     *
 | 
				
			||||
     * @return true if the item was selected. False if the item could not be selected, or was
 | 
				
			||||
     * was already selected.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract boolean select(@NonNull K key);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Attempts to deselect an item.
 | 
				
			||||
     *
 | 
				
			||||
     * @return true if the item was deselected. False if the item could not be deselected, or was
 | 
				
			||||
     * was already un-selected.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract boolean deselect(@NonNull K key);
 | 
				
			||||
 | 
				
			||||
    abstract AdapterDataObserver getAdapterDataObserver();
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Attempts to establish a range selection at {@code position}, selecting the item
 | 
				
			||||
     * at {@code position} if needed.
 | 
				
			||||
     *
 | 
				
			||||
     * @param position The "anchor" position for the range. Subsequent range operations
 | 
				
			||||
     *                 (primarily keyboard and mouse based operations like SHIFT + click)
 | 
				
			||||
     *                 work with the established anchor point to define selection ranges.
 | 
				
			||||
     */
 | 
				
			||||
    abstract void startRange(int position);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Sets the end point for the active range selection.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * This function should only be called when a range selection is active
 | 
				
			||||
     * (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
 | 
				
			||||
     * selected after consulting SelectionPredicate.
 | 
				
			||||
     *
 | 
				
			||||
     * @param position  The new end position for the selection range.
 | 
				
			||||
     * @throws IllegalStateException if a range selection is not active. Range selection
 | 
				
			||||
     *         must have been started by a call to {@link #startRange(int)}.
 | 
				
			||||
     */
 | 
				
			||||
    abstract void extendRange(int position);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Clears an in-progress range selection. Provisional range selection established
 | 
				
			||||
     * using {@link #extendProvisionalRange(int)} will be cleared (unless
 | 
				
			||||
     * {@link #mergeProvisionalSelection()} is called first.)
 | 
				
			||||
     */
 | 
				
			||||
    abstract void endRange();
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return Whether or not there is a current range selection active.
 | 
				
			||||
     */
 | 
				
			||||
    abstract boolean isRangeActive();
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Establishes the "anchor" at which a selection range begins. This "anchor" is consulted
 | 
				
			||||
     * when determining how to extend, and modify selection ranges. Calling this when a
 | 
				
			||||
     * range selection is active will reset the range selection.
 | 
				
			||||
     *
 | 
				
			||||
     * TODO: Reconcile this with startRange. Maybe just docs need to be updated.
 | 
				
			||||
     *
 | 
				
			||||
     * @param position the anchor position. Must already be selected.
 | 
				
			||||
     */
 | 
				
			||||
    abstract void anchorRange(int position);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Creates a provisional selection from anchor to {@code position}.
 | 
				
			||||
     *
 | 
				
			||||
     * @param position the end point.
 | 
				
			||||
     */
 | 
				
			||||
    abstract void extendProvisionalRange(int position);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Sets the provisional selection, replacing any existing selection.
 | 
				
			||||
     * @param newSelection
 | 
				
			||||
     */
 | 
				
			||||
    abstract void setProvisionalSelection(@NonNull Set<K> newSelection);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Clears any existing provisional selection
 | 
				
			||||
     */
 | 
				
			||||
    abstract void clearProvisionalSelection();
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Converts the provisional selection into primary selection, then clears
 | 
				
			||||
     * provisional selection.
 | 
				
			||||
     */
 | 
				
			||||
    abstract void mergeProvisionalSelection();
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Preserves selection, if any. Call this method from Activity#onSaveInstanceState
 | 
				
			||||
     *
 | 
				
			||||
     * @param state Bundle instance supplied to onSaveInstanceState.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract void onSaveInstanceState(@NonNull Bundle state);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Restores selection from previously saved state. Call this method from
 | 
				
			||||
     * Activity#onCreate.
 | 
				
			||||
     *
 | 
				
			||||
     * @param state Bundle instance supplied to onCreate.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract void onRestoreInstanceState(@Nullable Bundle state);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Observer class providing access to information about Selection state changes.
 | 
				
			||||
     *
 | 
				
			||||
     * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract static class SelectionObserver<K> {
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Called when the state of an item has been changed.
 | 
				
			||||
         */
 | 
				
			||||
        public void onItemStateChanged(@NonNull K key, boolean selected) {
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Called when the underlying data set has changed. After this method is called
 | 
				
			||||
         * SelectionTracker will traverse the existing selection,
 | 
				
			||||
         * calling {@link #onItemStateChanged(K, boolean)} for each selected item,
 | 
				
			||||
         * and deselecting any items that cannot be selected given the updated data-set
 | 
				
			||||
         * (and after consulting SelectionPredicate).
 | 
				
			||||
         */
 | 
				
			||||
        public void onSelectionRefresh() {
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Called immediately after completion of any set of changes, excluding
 | 
				
			||||
         * those resulting in calls to {@link #onSelectionRefresh()} and
 | 
				
			||||
         * {@link #onSelectionRestored()}.
 | 
				
			||||
         */
 | 
				
			||||
        public void onSelectionChanged() {
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Called immediately after selection is restored.
 | 
				
			||||
         * {@link #onItemStateChanged(K, boolean)} will *not* be called
 | 
				
			||||
         * for individual items in the selection.
 | 
				
			||||
         */
 | 
				
			||||
        public void onSelectionRestored() {
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Implement SelectionPredicate to control when items can be selected or unselected.
 | 
				
			||||
     * See {@link Builder#withSelectionPredicate(SelectionPredicate)}.
 | 
				
			||||
     *
 | 
				
			||||
     * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
     */
 | 
				
			||||
    public abstract static class SelectionPredicate<K> {
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Validates a change to selection for a specific key.
 | 
				
			||||
         *
 | 
				
			||||
         * @param key the item key
 | 
				
			||||
         * @param nextState the next potential selected/unselected state
 | 
				
			||||
         * @return true if the item at {@code id} can be set to {@code nextState}.
 | 
				
			||||
         */
 | 
				
			||||
        public abstract boolean canSetStateForKey(@NonNull K key, boolean nextState);
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Validates a change to selection for a specific position. If necessary
 | 
				
			||||
         * use {@link ItemKeyProvider} to identy associated key.
 | 
				
			||||
         *
 | 
				
			||||
         * @param position the item position
 | 
				
			||||
         * @param nextState the next potential selected/unselected state
 | 
				
			||||
         * @return true if the item at {@code id} can be set to {@code nextState}.
 | 
				
			||||
         */
 | 
				
			||||
        public abstract boolean canSetStateAtPosition(int position, boolean nextState);
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Permits restriction to single selection mode. Single selection mode has
 | 
				
			||||
         * unique behaviors in that it'll deselect an item already selected
 | 
				
			||||
         * in order to select the new item.
 | 
				
			||||
         *
 | 
				
			||||
         * <p>
 | 
				
			||||
         * In order to limit the number of items that can be selected,
 | 
				
			||||
         * use {@link #canSetStateForKey(Object, boolean)} and
 | 
				
			||||
         * {@link #canSetStateAtPosition(int, boolean)}.
 | 
				
			||||
         *
 | 
				
			||||
         * @return true if more than a single item can be selected.
 | 
				
			||||
         */
 | 
				
			||||
        public abstract boolean canSelectMultiple();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Builder is the primary mechanism for create a {@link SelectionTracker} that
 | 
				
			||||
     * can be used with your RecyclerView. Once installed, users will be able to create and
 | 
				
			||||
     * manipulate selection using a variety of intuitive techniques like tap, gesture,
 | 
				
			||||
     * and mouse lasso.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * Example usage:
 | 
				
			||||
     * <pre>SelectionTracker<Uri> tracker = new SelectionTracker.Builder<>(
 | 
				
			||||
     *        "my-uri-selection",
 | 
				
			||||
     *        recyclerView,
 | 
				
			||||
     *        new DemoStableIdProvider(recyclerView.getAdapter()),
 | 
				
			||||
     *        new MyDetailsLookup(recyclerView),
 | 
				
			||||
     *        StorageStrategy.createParcelableStorage(Uri.class))
 | 
				
			||||
     *        .build();
 | 
				
			||||
     *</pre>
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * <b>Restricting which items can be selected and limiting selection size</b>
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * {@link SelectionPredicate} provides a mechanism to restrict which Items can be selected,
 | 
				
			||||
     * to limit the number of items that can be selected, as well as allowing the selection
 | 
				
			||||
     * code to be placed into "single select" mode, which as the name indicates, constrains
 | 
				
			||||
     * the selection size to a single item.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>Configuring the tracker for single single selection support can be done
 | 
				
			||||
     * by supplying {@link SelectionPredicates#createSelectSingleAnything()}.
 | 
				
			||||
     *
 | 
				
			||||
     * SelectionTracker<String> tracker = new SelectionTracker.Builder<>(
 | 
				
			||||
     *        "my-string-selection",
 | 
				
			||||
     *        recyclerView,
 | 
				
			||||
     *        new DemoStableIdProvider(recyclerView.getAdapter()),
 | 
				
			||||
     *        new MyDetailsLookup(recyclerView),
 | 
				
			||||
     *        StorageStrategy.createStringStorage())
 | 
				
			||||
     *        .withSelectionPredicate(SelectionPredicates#createSelectSingleAnything())
 | 
				
			||||
     *        .build();
 | 
				
			||||
     *</pre>
 | 
				
			||||
     * <p>
 | 
				
			||||
     * <b>Retaining state across Android lifecycle events</b>
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * Support for storage/persistence of selection must be configured and invoked manually
 | 
				
			||||
     * owing to its reliance on Activity lifecycle events.
 | 
				
			||||
     * Failure to include support for selection storage will result in the active selection
 | 
				
			||||
     * being lost when the Activity receives a configuration change (e.g. rotation)
 | 
				
			||||
     * or when the application process is destroyed by the OS to reclaim resources.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * <b>Key Type</b>
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * Developers must decide on the key type used to identify selected items. Support
 | 
				
			||||
     * is provided for three types: {@link Parcelable}, {@link String}, and {@link Long}.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * {@link Parcelable}: Any Parcelable type can be used as the selection key. This is especially
 | 
				
			||||
     * useful in conjunction with {@link android.net.Uri} as the Android URI implementation is both
 | 
				
			||||
     * parcelable and makes for a natural stable selection key for values represented by
 | 
				
			||||
     * the Android Content Provider framework. If items in your view are associated with
 | 
				
			||||
     * stable {@code content://} uris, you should use Uri for your key type.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * {@link String}: Use String when a string based stable identifier is available.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * {@link Long}: Use Long when RecyclerView's long stable ids are
 | 
				
			||||
     * already in use. It comes with some limitations, however, as access to stable ids
 | 
				
			||||
     * at runtime is limited. Band selection support is not available when using the default
 | 
				
			||||
     * long key storage implementation. See {@link StableIdKeyProvider} for details.
 | 
				
			||||
     *
 | 
				
			||||
     * <p>
 | 
				
			||||
     * Usage:
 | 
				
			||||
     *
 | 
				
			||||
     * <pre>
 | 
				
			||||
     * private SelectionTracker<Uri> mTracker;
 | 
				
			||||
     *
 | 
				
			||||
     * public void onCreate(Bundle savedInstanceState) {
 | 
				
			||||
     *   // See above for details on constructing a SelectionTracker instance.
 | 
				
			||||
     *
 | 
				
			||||
     *   if (savedInstanceState != null) {
 | 
				
			||||
     *      mTracker.onRestoreInstanceState(savedInstanceState);
 | 
				
			||||
     *   }
 | 
				
			||||
     * }
 | 
				
			||||
     *
 | 
				
			||||
     * protected void onSaveInstanceState(Bundle outState) {
 | 
				
			||||
     *     super.onSaveInstanceState(outState);
 | 
				
			||||
     *     mTracker.onSaveInstanceState(outState);
 | 
				
			||||
     * }
 | 
				
			||||
     * </pre>
 | 
				
			||||
     *
 | 
				
			||||
     * @param <K> Selection key type. Built in support is provided for {@link String},
 | 
				
			||||
     *           {@link Long}, and {@link Parcelable}. {@link StorageStrategy}
 | 
				
			||||
     *           provides factory methods for each type:
 | 
				
			||||
     *           {@link StorageStrategy#createStringStorage()},
 | 
				
			||||
     *           {@link StorageStrategy#createParcelableStorage(Class)},
 | 
				
			||||
     *           {@link StorageStrategy#createLongStorage()}
 | 
				
			||||
     */
 | 
				
			||||
    public static final class Builder<K> {
 | 
				
			||||
 | 
				
			||||
        final RecyclerView mRecyclerView;
 | 
				
			||||
        private final RecyclerView.Adapter<?> mAdapter;
 | 
				
			||||
        private final Context mContext;
 | 
				
			||||
        private final String mSelectionId;
 | 
				
			||||
        private final StorageStrategy<K> mStorage;
 | 
				
			||||
 | 
				
			||||
        SelectionPredicate<K> mSelectionPredicate =
 | 
				
			||||
                SelectionPredicates.createSelectAnything();
 | 
				
			||||
        private OperationMonitor mMonitor = new OperationMonitor();
 | 
				
			||||
        private ItemKeyProvider<K> mKeyProvider;
 | 
				
			||||
        private ItemDetailsLookup<K> mDetailsLookup;
 | 
				
			||||
 | 
				
			||||
        private FocusDelegate<K> mFocusDelegate = FocusDelegate.dummy();
 | 
				
			||||
 | 
				
			||||
        private OnItemActivatedListener<K> mOnItemActivatedListener;
 | 
				
			||||
        private OnDragInitiatedListener mOnDragInitiatedListener;
 | 
				
			||||
        private OnContextClickListener mOnContextClickListener;
 | 
				
			||||
 | 
				
			||||
        private BandPredicate mBandPredicate;
 | 
				
			||||
        private int mBandOverlayId = eu.faircode.email.R.drawable.selection_band_overlay;
 | 
				
			||||
 | 
				
			||||
        private int[] mGestureToolTypes = new int[] {
 | 
				
			||||
                MotionEvent.TOOL_TYPE_FINGER,
 | 
				
			||||
                MotionEvent.TOOL_TYPE_UNKNOWN
 | 
				
			||||
        };
 | 
				
			||||
 | 
				
			||||
        private int[] mPointerToolTypes = new int[] {
 | 
				
			||||
                MotionEvent.TOOL_TYPE_MOUSE
 | 
				
			||||
        };
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Creates a new SelectionTracker.Builder useful for configuring and creating
 | 
				
			||||
         * a new SelectionTracker for use with your {@link RecyclerView}.
 | 
				
			||||
         *
 | 
				
			||||
         * @param selectionId A unique string identifying this selection in the context
 | 
				
			||||
         *        of the activity or fragment.
 | 
				
			||||
         * @param recyclerView the owning RecyclerView
 | 
				
			||||
         * @param keyProvider the source of selection keys
 | 
				
			||||
         * @param detailsLookup the source of information about RecyclerView items.
 | 
				
			||||
         * @param storage Strategy for type-safe storage of selection state in
 | 
				
			||||
         *        {@link Bundle}.
 | 
				
			||||
         */
 | 
				
			||||
        public Builder(
 | 
				
			||||
                @NonNull String selectionId,
 | 
				
			||||
                @NonNull RecyclerView recyclerView,
 | 
				
			||||
                @NonNull ItemKeyProvider<K> keyProvider,
 | 
				
			||||
                @NonNull ItemDetailsLookup<K> detailsLookup,
 | 
				
			||||
                @NonNull StorageStrategy<K> storage) {
 | 
				
			||||
 | 
				
			||||
            checkArgument(selectionId != null);
 | 
				
			||||
            checkArgument(!selectionId.trim().isEmpty());
 | 
				
			||||
            checkArgument(recyclerView != null);
 | 
				
			||||
 | 
				
			||||
            mSelectionId = selectionId;
 | 
				
			||||
            mRecyclerView = recyclerView;
 | 
				
			||||
            mContext = recyclerView.getContext();
 | 
				
			||||
            mAdapter = recyclerView.getAdapter();
 | 
				
			||||
 | 
				
			||||
            checkArgument(mAdapter != null);
 | 
				
			||||
            checkArgument(keyProvider != null);
 | 
				
			||||
            checkArgument(detailsLookup != null);
 | 
				
			||||
            checkArgument(storage != null);
 | 
				
			||||
 | 
				
			||||
            mDetailsLookup = detailsLookup;
 | 
				
			||||
            mKeyProvider = keyProvider;
 | 
				
			||||
            mStorage = storage;
 | 
				
			||||
 | 
				
			||||
            mBandPredicate = new BandPredicate.NonDraggableArea(mRecyclerView, detailsLookup);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Install selection predicate.
 | 
				
			||||
         *
 | 
				
			||||
         * @param predicate the predicate to be used.
 | 
				
			||||
         * @return this
 | 
				
			||||
         */
 | 
				
			||||
        public Builder<K> withSelectionPredicate(
 | 
				
			||||
                @NonNull SelectionPredicate<K> predicate) {
 | 
				
			||||
 | 
				
			||||
            checkArgument(predicate != null);
 | 
				
			||||
            mSelectionPredicate = predicate;
 | 
				
			||||
            return this;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Add operation monitor allowing access to information about active
 | 
				
			||||
         * operations (like band selection and gesture selection).
 | 
				
			||||
         *
 | 
				
			||||
         * @param monitor the monitor to be used
 | 
				
			||||
         * @return this
 | 
				
			||||
         */
 | 
				
			||||
        public Builder<K> withOperationMonitor(
 | 
				
			||||
                @NonNull OperationMonitor monitor) {
 | 
				
			||||
 | 
				
			||||
            checkArgument(monitor != null);
 | 
				
			||||
            mMonitor = monitor;
 | 
				
			||||
            return this;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Add focus delegate to interact with selection related focus changes.
 | 
				
			||||
         *
 | 
				
			||||
         * @param delegate the delegate to be used
 | 
				
			||||
         * @return this
 | 
				
			||||
         */
 | 
				
			||||
        public Builder<K> withFocusDelegate(@NonNull FocusDelegate<K> delegate) {
 | 
				
			||||
            checkArgument(delegate != null);
 | 
				
			||||
            mFocusDelegate = delegate;
 | 
				
			||||
            return this;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Adds an item activation listener. Respond to taps/enter/double-click on items.
 | 
				
			||||
         *
 | 
				
			||||
         * @param listener the listener to be used
 | 
				
			||||
         * @return this
 | 
				
			||||
         */
 | 
				
			||||
        public Builder<K> withOnItemActivatedListener(
 | 
				
			||||
                @NonNull OnItemActivatedListener<K> listener) {
 | 
				
			||||
 | 
				
			||||
            checkArgument(listener != null);
 | 
				
			||||
 | 
				
			||||
            mOnItemActivatedListener = listener;
 | 
				
			||||
            return this;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Adds a context click listener. Respond to right-click.
 | 
				
			||||
         *
 | 
				
			||||
         * @param listener the listener to be used
 | 
				
			||||
         * @return this
 | 
				
			||||
         */
 | 
				
			||||
        public Builder<K> withOnContextClickListener(
 | 
				
			||||
                @NonNull OnContextClickListener listener) {
 | 
				
			||||
 | 
				
			||||
            checkArgument(listener != null);
 | 
				
			||||
 | 
				
			||||
            mOnContextClickListener = listener;
 | 
				
			||||
            return this;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Adds a drag initiated listener. Add support for drag and drop.
 | 
				
			||||
         *
 | 
				
			||||
         * @param listener the listener to be used
 | 
				
			||||
         * @return this
 | 
				
			||||
         */
 | 
				
			||||
        public Builder<K> withOnDragInitiatedListener(
 | 
				
			||||
                @NonNull OnDragInitiatedListener listener) {
 | 
				
			||||
 | 
				
			||||
            checkArgument(listener != null);
 | 
				
			||||
 | 
				
			||||
            mOnDragInitiatedListener = listener;
 | 
				
			||||
            return this;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Replaces default tap and gesture tool-types. Defaults are:
 | 
				
			||||
         * {@link MotionEvent#TOOL_TYPE_FINGER} and {@link MotionEvent#TOOL_TYPE_UNKNOWN}.
 | 
				
			||||
         *
 | 
				
			||||
         * @param toolTypes the tool types to be used
 | 
				
			||||
         * @return this
 | 
				
			||||
         */
 | 
				
			||||
        public Builder<K> withGestureTooltypes(int... toolTypes) {
 | 
				
			||||
            mGestureToolTypes = toolTypes;
 | 
				
			||||
            return this;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Replaces default band overlay.
 | 
				
			||||
         *
 | 
				
			||||
         * @param bandOverlayId
 | 
				
			||||
         * @return this
 | 
				
			||||
         */
 | 
				
			||||
        public Builder<K> withBandOverlay(@DrawableRes int bandOverlayId) {
 | 
				
			||||
            mBandOverlayId = bandOverlayId;
 | 
				
			||||
            return this;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Replaces default band predicate.
 | 
				
			||||
         * @param bandPredicate
 | 
				
			||||
         * @return this
 | 
				
			||||
         */
 | 
				
			||||
        public Builder<K> withBandPredicate(@NonNull BandPredicate bandPredicate) {
 | 
				
			||||
            checkArgument(bandPredicate != null);
 | 
				
			||||
 | 
				
			||||
            mBandPredicate = bandPredicate;
 | 
				
			||||
            return this;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Replaces default pointer tool-types. Pointer tools
 | 
				
			||||
         * are associated with band selection, and certain
 | 
				
			||||
         * drag and drop behaviors. Defaults are:
 | 
				
			||||
         * {@link MotionEvent#TOOL_TYPE_MOUSE}.
 | 
				
			||||
         *
 | 
				
			||||
         * @param toolTypes the tool types to be used
 | 
				
			||||
         * @return this
 | 
				
			||||
         */
 | 
				
			||||
        public Builder<K> withPointerTooltypes(int... toolTypes) {
 | 
				
			||||
            mPointerToolTypes = toolTypes;
 | 
				
			||||
            return this;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Prepares and returns a SelectionTracker.
 | 
				
			||||
         *
 | 
				
			||||
         * @return this
 | 
				
			||||
         */
 | 
				
			||||
        public SelectionTracker<K> build() {
 | 
				
			||||
 | 
				
			||||
            SelectionTracker<K> tracker = new DefaultSelectionTracker<>(
 | 
				
			||||
                    mSelectionId, mKeyProvider, mSelectionPredicate, mStorage);
 | 
				
			||||
 | 
				
			||||
            // Event glue between RecyclerView and SelectionTracker keeps the classes separate
 | 
				
			||||
            // so that a SelectionTracker can be shared across RecyclerView instances that
 | 
				
			||||
            // represent the same data in different ways.
 | 
				
			||||
            EventBridge.install(mAdapter, tracker, mKeyProvider);
 | 
				
			||||
 | 
				
			||||
            AutoScroller scroller =
 | 
				
			||||
                    new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecyclerView));
 | 
				
			||||
 | 
				
			||||
            // Setup basic input handling, with the touch handler as the default consumer
 | 
				
			||||
            // of events. If mouse handling is configured as well, the mouse input
 | 
				
			||||
            // related handlers will intercept mouse input events.
 | 
				
			||||
 | 
				
			||||
            // GestureRouter is responsible for routing GestureDetector events
 | 
				
			||||
            // to tool-type specific handlers.
 | 
				
			||||
            GestureRouter<MotionInputHandler> gestureRouter = new GestureRouter<>();
 | 
				
			||||
            GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
 | 
				
			||||
 | 
				
			||||
            // TouchEventRouter takes its name from RecyclerView#OnItemTouchListener.
 | 
				
			||||
            // Despite "Touch" being in the name, it receives events for all types of tools.
 | 
				
			||||
            // This class is responsible for routing events to tool-type specific handlers,
 | 
				
			||||
            // and if not handled by a handler, on to a GestureDetector for analysis.
 | 
				
			||||
            TouchEventRouter eventRouter = new TouchEventRouter(gestureDetector);
 | 
				
			||||
 | 
				
			||||
            // GestureSelectionHelper provides logic that interprets a combination
 | 
				
			||||
            // of motions and gestures in order to provide gesture driven selection support
 | 
				
			||||
            // when used in conjunction with RecyclerView.
 | 
				
			||||
            final GestureSelectionHelper gestureHelper = GestureSelectionHelper.create(
 | 
				
			||||
                    tracker, mDetailsLookup, mRecyclerView, scroller, mMonitor);
 | 
				
			||||
 | 
				
			||||
            // Finally hook the framework up to listening to recycle view events.
 | 
				
			||||
            mRecyclerView.addOnItemTouchListener(eventRouter);
 | 
				
			||||
 | 
				
			||||
            // But before you move on, there's more work to do. Event plumbing has been
 | 
				
			||||
            // installed, but we haven't registered any of our helpers or callbacks.
 | 
				
			||||
            // Helpers contain predefined logic converting events into selection related events.
 | 
				
			||||
            // Callbacks provide developers the ability to reponspond to other types of
 | 
				
			||||
            // events (like "activate" a tapped item). This is broken up into two main
 | 
				
			||||
            // suites, one for "touch" and one for "mouse", though both can and should (usually)
 | 
				
			||||
            // be configured to handle other types of input (to satisfy user expectation).);
 | 
				
			||||
 | 
				
			||||
            // Internally, the code doesn't permit nullable listeners, so we lazily
 | 
				
			||||
            // initialize dummy instances if the developer didn't supply a real listener.
 | 
				
			||||
            mOnDragInitiatedListener = (mOnDragInitiatedListener != null)
 | 
				
			||||
                    ? mOnDragInitiatedListener
 | 
				
			||||
                    : new OnDragInitiatedListener() {
 | 
				
			||||
                        @Override
 | 
				
			||||
                        public boolean onDragInitiated(@NonNull MotionEvent e) {
 | 
				
			||||
                            return false;
 | 
				
			||||
                        }
 | 
				
			||||
                    };
 | 
				
			||||
 | 
				
			||||
            mOnItemActivatedListener = (mOnItemActivatedListener != null)
 | 
				
			||||
                    ? mOnItemActivatedListener
 | 
				
			||||
                    : new OnItemActivatedListener<K>() {
 | 
				
			||||
                        @Override
 | 
				
			||||
                        public boolean onItemActivated(
 | 
				
			||||
                                @NonNull ItemDetailsLookup.ItemDetails<K> item,
 | 
				
			||||
                                @NonNull MotionEvent e) {
 | 
				
			||||
                            return false;
 | 
				
			||||
                        }
 | 
				
			||||
                    };
 | 
				
			||||
 | 
				
			||||
            mOnContextClickListener = (mOnContextClickListener != null)
 | 
				
			||||
                    ? mOnContextClickListener
 | 
				
			||||
                    : new OnContextClickListener() {
 | 
				
			||||
                        @Override
 | 
				
			||||
                        public boolean onContextClick(@NonNull MotionEvent e) {
 | 
				
			||||
                            return false;
 | 
				
			||||
                        }
 | 
				
			||||
                    };
 | 
				
			||||
 | 
				
			||||
            // Provides high level glue for binding touch events
 | 
				
			||||
            // and gestures to selection framework.
 | 
				
			||||
            TouchInputHandler<K> touchHandler = new TouchInputHandler<K>(
 | 
				
			||||
                    tracker,
 | 
				
			||||
                    mKeyProvider,
 | 
				
			||||
                    mDetailsLookup,
 | 
				
			||||
                    mSelectionPredicate,
 | 
				
			||||
                    new Runnable() {
 | 
				
			||||
                        @Override
 | 
				
			||||
                        public void run() {
 | 
				
			||||
                            if (mSelectionPredicate.canSelectMultiple()) {
 | 
				
			||||
                                try {
 | 
				
			||||
                                    gestureHelper.start();
 | 
				
			||||
                                } catch (IllegalStateException ex) {
 | 
				
			||||
                                    ex.printStackTrace();
 | 
				
			||||
                                }
 | 
				
			||||
                            }
 | 
				
			||||
                        }
 | 
				
			||||
                    },
 | 
				
			||||
                    mOnDragInitiatedListener,
 | 
				
			||||
                    mOnItemActivatedListener,
 | 
				
			||||
                    mFocusDelegate,
 | 
				
			||||
                    new Runnable() {
 | 
				
			||||
                        @Override
 | 
				
			||||
                        public void run() {
 | 
				
			||||
                            mRecyclerView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
 | 
				
			||||
                        }
 | 
				
			||||
                    });
 | 
				
			||||
 | 
				
			||||
            for (int toolType : mGestureToolTypes) {
 | 
				
			||||
                gestureRouter.register(toolType, touchHandler);
 | 
				
			||||
                eventRouter.register(toolType, gestureHelper);
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            // Provides high level glue for binding mouse events and gestures
 | 
				
			||||
            // to selection framework.
 | 
				
			||||
            MouseInputHandler<K> mouseHandler = new MouseInputHandler<>(
 | 
				
			||||
                    tracker,
 | 
				
			||||
                    mKeyProvider,
 | 
				
			||||
                    mDetailsLookup,
 | 
				
			||||
                    mOnContextClickListener,
 | 
				
			||||
                    mOnItemActivatedListener,
 | 
				
			||||
                    mFocusDelegate);
 | 
				
			||||
 | 
				
			||||
            for (int toolType : mPointerToolTypes) {
 | 
				
			||||
                gestureRouter.register(toolType, mouseHandler);
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Nullable BandSelectionHelper bandHelper = null;
 | 
				
			||||
 | 
				
			||||
            // Band selection not supported in single select mode, or when key access
 | 
				
			||||
            // is limited to anything less than the entire corpus.
 | 
				
			||||
            if (mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED)
 | 
				
			||||
                    && mSelectionPredicate.canSelectMultiple()) {
 | 
				
			||||
                // BandSelectionHelper provides support for band selection on-top of a RecyclerView
 | 
				
			||||
                // instance. Given the recycling nature of RecyclerView BandSelectionController
 | 
				
			||||
                // necessarily models and caches list/grid information as the user's pointer
 | 
				
			||||
                // interacts with the item in the RecyclerView. Selectable items that intersect
 | 
				
			||||
                // with the band, both on and off screen, are selected.
 | 
				
			||||
                bandHelper = BandSelectionHelper.create(
 | 
				
			||||
                        mRecyclerView,
 | 
				
			||||
                        scroller,
 | 
				
			||||
                        mBandOverlayId,
 | 
				
			||||
                        mKeyProvider,
 | 
				
			||||
                        tracker,
 | 
				
			||||
                        mSelectionPredicate,
 | 
				
			||||
                        mBandPredicate,
 | 
				
			||||
                        mFocusDelegate,
 | 
				
			||||
                        mMonitor);
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            OnItemTouchListener pointerEventHandler = new PointerDragEventInterceptor(
 | 
				
			||||
                    mDetailsLookup, mOnDragInitiatedListener, bandHelper);
 | 
				
			||||
 | 
				
			||||
            for (int toolType : mPointerToolTypes) {
 | 
				
			||||
                eventRouter.register(toolType, pointerEventHandler);
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            return tracker;
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,28 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Shared constants used in this package. Disable DEBUG and VERBOSE prior to releases.
 | 
				
			||||
 */
 | 
				
			||||
final class Shared {
 | 
				
			||||
 | 
				
			||||
    static final boolean DEBUG = false;
 | 
				
			||||
    static final boolean VERBOSE = false;
 | 
				
			||||
 | 
				
			||||
    private Shared() {}
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,110 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import android.util.SparseArray;
 | 
				
			||||
import android.view.View;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
 | 
				
			||||
 | 
				
			||||
import java.util.HashMap;
 | 
				
			||||
import java.util.Map;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * An {@link ItemKeyProvider} that provides stable ids by way of cached
 | 
				
			||||
 * {@link RecyclerView.Adapter} stable ids. Items enter the cache as they are laid out by
 | 
				
			||||
 * RecyclerView, and are removed from the cache as they are recycled.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * There are trade-offs with this implementation as it necessarily auto-boxes {@code long}
 | 
				
			||||
 * stable id values into {@code Long} values for use as selection keys. The core Selection API
 | 
				
			||||
 * uses a parameterized key type to permit other keys (such as Strings or URIs).
 | 
				
			||||
 */
 | 
				
			||||
public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
 | 
				
			||||
 | 
				
			||||
    private final SparseArray<Long> mPositionToKey = new SparseArray<>();
 | 
				
			||||
    private final Map<Long, Integer> mKeyToPosition = new HashMap<Long, Integer>();
 | 
				
			||||
    private final RecyclerView mRecyclerView;
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Creates a new key provider that uses cached {@code long} stable ids associated
 | 
				
			||||
     * with the RecyclerView items.
 | 
				
			||||
     *
 | 
				
			||||
     * @param recyclerView the owner RecyclerView
 | 
				
			||||
     */
 | 
				
			||||
    public StableIdKeyProvider(@NonNull RecyclerView recyclerView) {
 | 
				
			||||
 | 
				
			||||
        // Since this provide is based on stable ids based on whats laid out in the window
 | 
				
			||||
        // we can only satisfy "window" scope key access.
 | 
				
			||||
        super(SCOPE_CACHED);
 | 
				
			||||
 | 
				
			||||
        mRecyclerView = recyclerView;
 | 
				
			||||
 | 
				
			||||
        mRecyclerView.addOnChildAttachStateChangeListener(
 | 
				
			||||
                new OnChildAttachStateChangeListener() {
 | 
				
			||||
                    @Override
 | 
				
			||||
                    public void onChildViewAttachedToWindow(View view) {
 | 
				
			||||
                        onAttached(view);
 | 
				
			||||
                    }
 | 
				
			||||
 | 
				
			||||
                    @Override
 | 
				
			||||
                    public void onChildViewDetachedFromWindow(View view) {
 | 
				
			||||
                        onDetached(view);
 | 
				
			||||
                    }
 | 
				
			||||
                }
 | 
				
			||||
        );
 | 
				
			||||
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    void onAttached(@NonNull View view) {
 | 
				
			||||
        RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view);
 | 
				
			||||
        int position = holder.getAdapterPosition();
 | 
				
			||||
        long id = holder.getItemId();
 | 
				
			||||
        if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
 | 
				
			||||
            mPositionToKey.put(position, id);
 | 
				
			||||
            mKeyToPosition.put(id, position);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    void onDetached(@NonNull View view) {
 | 
				
			||||
        RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view);
 | 
				
			||||
        int position = holder.getAdapterPosition();
 | 
				
			||||
        long id = holder.getItemId();
 | 
				
			||||
        if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
 | 
				
			||||
            mPositionToKey.delete(position);
 | 
				
			||||
            mKeyToPosition.remove(id);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public @Nullable Long getKey(int position) {
 | 
				
			||||
        return mPositionToKey.get(position, null);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public int getPosition(@NonNull Long key) {
 | 
				
			||||
        if (mKeyToPosition.containsKey(key)) {
 | 
				
			||||
            return mKeyToPosition.get(key);
 | 
				
			||||
        }
 | 
				
			||||
        return RecyclerView.NO_POSITION;
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,230 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2018 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
 | 
				
			||||
import android.os.Bundle;
 | 
				
			||||
import android.os.Parcelable;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.annotation.VisibleForTesting;
 | 
				
			||||
 | 
				
			||||
import java.util.ArrayList;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Strategy for storing keys in saved state. Extend this class when using custom
 | 
				
			||||
 * key types that aren't supported by default. Prefer use of builtin storage strategies:
 | 
				
			||||
 * {@link #createStringStorage()}, {@link #createLongStorage()},
 | 
				
			||||
 * {@link #createParcelableStorage(Class)}.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * See
 | 
				
			||||
 * {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
 | 
				
			||||
 * for more detailed advice on which key type to use for your selection keys.
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. Built in support is provided for String, Long, and Parcelable
 | 
				
			||||
 *           types. Use the respective factory method to create a StorageStrategy instance
 | 
				
			||||
 *           appropriate to the desired type.
 | 
				
			||||
 *           {@link #createStringStorage()},
 | 
				
			||||
 *           {@link #createParcelableStorage(Class)},
 | 
				
			||||
 *           {@link #createLongStorage()}
 | 
				
			||||
 */
 | 
				
			||||
public abstract class StorageStrategy<K> {
 | 
				
			||||
 | 
				
			||||
    @VisibleForTesting
 | 
				
			||||
    static final String SELECTION_ENTRIES = "androidx.recyclerview.selection.entries";
 | 
				
			||||
 | 
				
			||||
    @VisibleForTesting
 | 
				
			||||
    static final String SELECTION_KEY_TYPE = "androidx.recyclerview.selection.type";
 | 
				
			||||
 | 
				
			||||
    private final Class<K> mType;
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Creates a new instance.
 | 
				
			||||
     *
 | 
				
			||||
     * @param type the key type class that is being used.
 | 
				
			||||
     */
 | 
				
			||||
    public StorageStrategy(@NonNull Class<K> type) {
 | 
				
			||||
        checkArgument(type != null);
 | 
				
			||||
        mType = type;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Create a {@link Selection} from supplied {@link Bundle}.
 | 
				
			||||
     *
 | 
				
			||||
     * @param state Bundle instance that may contain parceled Selection instance.
 | 
				
			||||
     * @return
 | 
				
			||||
     */
 | 
				
			||||
    public abstract @Nullable Selection<K> asSelection(@NonNull Bundle state);
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Creates a {@link Bundle} from supplied {@link Selection}.
 | 
				
			||||
     *
 | 
				
			||||
     * @param selection The selection to asBundle.
 | 
				
			||||
     * @return
 | 
				
			||||
     */
 | 
				
			||||
    public abstract @NonNull Bundle asBundle(@NonNull Selection<K> selection);
 | 
				
			||||
 | 
				
			||||
    String getKeyTypeName() {
 | 
				
			||||
        return mType.getCanonicalName();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return StorageStrategy suitable for use with {@link Parcelable} keys
 | 
				
			||||
     * (like {@link android.net.Uri}).
 | 
				
			||||
     */
 | 
				
			||||
    public static <K extends Parcelable> StorageStrategy<K> createParcelableStorage(Class<K> type) {
 | 
				
			||||
        return new ParcelableStorageStrategy(type);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return StorageStrategy suitable for use with {@link String} keys.
 | 
				
			||||
     */
 | 
				
			||||
    public static StorageStrategy<String> createStringStorage() {
 | 
				
			||||
        return new StringStorageStrategy();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @return StorageStrategy suitable for use with {@link Long} keys.
 | 
				
			||||
     */
 | 
				
			||||
    public static StorageStrategy<Long> createLongStorage() {
 | 
				
			||||
        return new LongStorageStrategy();
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private static class StringStorageStrategy extends StorageStrategy<String> {
 | 
				
			||||
 | 
				
			||||
        StringStorageStrategy() {
 | 
				
			||||
            super(String.class);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public @Nullable Selection<String> asSelection(@NonNull Bundle state) {
 | 
				
			||||
 | 
				
			||||
            String keyType = state.getString(SELECTION_KEY_TYPE, null);
 | 
				
			||||
            if (keyType == null || !keyType.equals(getKeyTypeName())) {
 | 
				
			||||
                return null;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Nullable ArrayList<String> stored = state.getStringArrayList(SELECTION_ENTRIES);
 | 
				
			||||
            if (stored == null) {
 | 
				
			||||
                return null;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            Selection<String> selection = new Selection<>();
 | 
				
			||||
            selection.mSelection.addAll(stored);
 | 
				
			||||
            return selection;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public @NonNull Bundle asBundle(@NonNull Selection<String> selection) {
 | 
				
			||||
 | 
				
			||||
            Bundle bundle = new Bundle();
 | 
				
			||||
 | 
				
			||||
            bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
 | 
				
			||||
 | 
				
			||||
            ArrayList<String> value = new ArrayList<>(selection.size());
 | 
				
			||||
            value.addAll(selection.mSelection);
 | 
				
			||||
            bundle.putStringArrayList(SELECTION_ENTRIES, value);
 | 
				
			||||
 | 
				
			||||
            return bundle;
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private static class LongStorageStrategy extends StorageStrategy<Long> {
 | 
				
			||||
 | 
				
			||||
        LongStorageStrategy() {
 | 
				
			||||
            super(Long.class);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public @Nullable Selection<Long> asSelection(@NonNull Bundle state) {
 | 
				
			||||
            String keyType = state.getString(SELECTION_KEY_TYPE, null);
 | 
				
			||||
            if (keyType == null || !keyType.equals(getKeyTypeName())) {
 | 
				
			||||
                return null;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Nullable long[] stored = state.getLongArray(SELECTION_ENTRIES);
 | 
				
			||||
            if (stored == null) {
 | 
				
			||||
                return null;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            Selection<Long> selection = new Selection<>();
 | 
				
			||||
            for (long key : stored) {
 | 
				
			||||
                selection.mSelection.add(key);
 | 
				
			||||
            }
 | 
				
			||||
            return selection;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public @NonNull Bundle asBundle(@NonNull Selection<Long> selection) {
 | 
				
			||||
 | 
				
			||||
            Bundle bundle = new Bundle();
 | 
				
			||||
            bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
 | 
				
			||||
 | 
				
			||||
            long[] value = new long[selection.size()];
 | 
				
			||||
            int i = 0;
 | 
				
			||||
            for (Long key : selection) {
 | 
				
			||||
                value[i++] = key;
 | 
				
			||||
            }
 | 
				
			||||
            bundle.putLongArray(SELECTION_ENTRIES, value);
 | 
				
			||||
 | 
				
			||||
            return bundle;
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private static class ParcelableStorageStrategy<K extends Parcelable>
 | 
				
			||||
            extends StorageStrategy<K> {
 | 
				
			||||
 | 
				
			||||
        ParcelableStorageStrategy(Class<K> type) {
 | 
				
			||||
            super(type);
 | 
				
			||||
            checkArgument(Parcelable.class.isAssignableFrom(type));
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public @Nullable Selection<K> asSelection(@NonNull Bundle state) {
 | 
				
			||||
 | 
				
			||||
            String keyType = state.getString(SELECTION_KEY_TYPE, null);
 | 
				
			||||
            if (keyType == null || !keyType.equals(getKeyTypeName())) {
 | 
				
			||||
                return null;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            @Nullable ArrayList<K> stored = state.getParcelableArrayList(SELECTION_ENTRIES);
 | 
				
			||||
            if (stored == null) {
 | 
				
			||||
                return null;
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            Selection<K> selection = new Selection<>();
 | 
				
			||||
            selection.mSelection.addAll(stored);
 | 
				
			||||
            return selection;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        public @NonNull Bundle asBundle(@NonNull Selection<K> selection) {
 | 
				
			||||
 | 
				
			||||
            Bundle bundle = new Bundle();
 | 
				
			||||
            bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
 | 
				
			||||
 | 
				
			||||
            ArrayList<K> value = new ArrayList<>(selection.size());
 | 
				
			||||
            value.addAll(selection.mSelection);
 | 
				
			||||
            bundle.putParcelableArrayList(SELECTION_ENTRIES, value);
 | 
				
			||||
 | 
				
			||||
            return bundle;
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,74 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
import static androidx.core.util.Preconditions.checkState;
 | 
				
			||||
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
 | 
				
			||||
import java.util.Arrays;
 | 
				
			||||
import java.util.List;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Registry for tool specific event handler. This provides map like functionality,
 | 
				
			||||
 * along with fallback to a default handler, while avoiding auto-boxing of tool
 | 
				
			||||
 * type values that would be necessitated where a Map used.
 | 
				
			||||
 *
 | 
				
			||||
 * @param <T> type of item being registered.
 | 
				
			||||
 */
 | 
				
			||||
final class ToolHandlerRegistry<T> {
 | 
				
			||||
 | 
				
			||||
    // Currently there are four known input types. ERASER is the last one, so has the
 | 
				
			||||
    // highest value. UNKNOWN is zero, so we add one. This allows delegates to be
 | 
				
			||||
    // registered by type, and avoid the auto-boxing that would be necessary were we
 | 
				
			||||
    // to store delegates in a Map<Integer, Delegate>.
 | 
				
			||||
    private static final int NUM_INPUT_TYPES = MotionEvent.TOOL_TYPE_ERASER + 1;
 | 
				
			||||
 | 
				
			||||
    private final List<T> mHandlers = Arrays.asList(null, null, null, null, null);
 | 
				
			||||
    private final T mDefault;
 | 
				
			||||
 | 
				
			||||
    ToolHandlerRegistry(@NonNull T defaultDelegate) {
 | 
				
			||||
        checkArgument(defaultDelegate != null);
 | 
				
			||||
        mDefault = defaultDelegate;
 | 
				
			||||
 | 
				
			||||
        // Initialize all values to null.
 | 
				
			||||
        for (int i = 0; i < NUM_INPUT_TYPES; i++) {
 | 
				
			||||
            mHandlers.set(i, null);
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @param toolType
 | 
				
			||||
     * @param delegate the delegate, or null to unregister.
 | 
				
			||||
     * @throws IllegalStateException if an tooltype handler is already registered.
 | 
				
			||||
     */
 | 
				
			||||
    void set(int toolType, @Nullable T delegate) {
 | 
				
			||||
        checkArgument(toolType >= 0 && toolType <= MotionEvent.TOOL_TYPE_ERASER);
 | 
				
			||||
        checkState(mHandlers.get(toolType) == null);
 | 
				
			||||
 | 
				
			||||
        mHandlers.set(toolType, delegate);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    T get(@NonNull MotionEvent e) {
 | 
				
			||||
        T d = mHandlers.get(e.getToolType(0));
 | 
				
			||||
        return d != null ? d : mDefault;
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,113 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
 | 
				
			||||
import android.view.GestureDetector;
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * A class responsible for routing MotionEvents to tool-type specific handlers,
 | 
				
			||||
 * and if not handled by a handler, on to a {@link GestureDetector} for further
 | 
				
			||||
 * processing.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * TouchEventRouter takes its name from
 | 
				
			||||
 * {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch"
 | 
				
			||||
 * being in the name, it receives MotionEvents for all types of tools.
 | 
				
			||||
 */
 | 
				
			||||
final class TouchEventRouter implements OnItemTouchListener {
 | 
				
			||||
 | 
				
			||||
    private static final String TAG = "TouchEventRouter";
 | 
				
			||||
 | 
				
			||||
    private final GestureDetector mDetector;
 | 
				
			||||
    private final ToolHandlerRegistry<OnItemTouchListener> mDelegates;
 | 
				
			||||
 | 
				
			||||
    TouchEventRouter(
 | 
				
			||||
            @NonNull GestureDetector detector, @NonNull OnItemTouchListener defaultDelegate) {
 | 
				
			||||
 | 
				
			||||
        checkArgument(detector != null);
 | 
				
			||||
        checkArgument(defaultDelegate != null);
 | 
				
			||||
 | 
				
			||||
        mDetector = detector;
 | 
				
			||||
        mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    TouchEventRouter(@NonNull GestureDetector detector) {
 | 
				
			||||
        this(
 | 
				
			||||
                detector,
 | 
				
			||||
                // Supply a fallback listener does nothing...because the caller
 | 
				
			||||
                // didn't supply a fallback.
 | 
				
			||||
                new OnItemTouchListener() {
 | 
				
			||||
                    @Override
 | 
				
			||||
                    public boolean onInterceptTouchEvent(
 | 
				
			||||
                            @NonNull RecyclerView unused, @NonNull MotionEvent e) {
 | 
				
			||||
 | 
				
			||||
                        return false;
 | 
				
			||||
                    }
 | 
				
			||||
 | 
				
			||||
                    @Override
 | 
				
			||||
                    public void onTouchEvent(
 | 
				
			||||
                            @NonNull RecyclerView unused, @NonNull MotionEvent e) {
 | 
				
			||||
                    }
 | 
				
			||||
 | 
				
			||||
                    @Override
 | 
				
			||||
                    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
 | 
				
			||||
                    }
 | 
				
			||||
                });
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * @param toolType See MotionEvent for details on available types.
 | 
				
			||||
     * @param delegate An {@link OnItemTouchListener} to receive events
 | 
				
			||||
     *     of {@code toolType}.
 | 
				
			||||
     */
 | 
				
			||||
    void register(int toolType, @NonNull OnItemTouchListener delegate) {
 | 
				
			||||
        checkArgument(delegate != null);
 | 
				
			||||
        mDelegates.set(toolType, delegate);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
 | 
				
			||||
        boolean handled = mDelegates.get(e).onInterceptTouchEvent(rv, e);
 | 
				
			||||
 | 
				
			||||
        // Forward all events to UserInputHandler.
 | 
				
			||||
        // This is necessary since UserInputHandler needs to always see the first DOWN event. Or
 | 
				
			||||
        // else all future UP events will be tossed.
 | 
				
			||||
        handled |= mDetector.onTouchEvent(e);
 | 
				
			||||
 | 
				
			||||
        return handled;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
 | 
				
			||||
        mDelegates.get(e).onTouchEvent(rv, e);
 | 
				
			||||
 | 
				
			||||
        // Note: even though this event is being handled as part of gestures such as drag and band,
 | 
				
			||||
        // continue forwarding to the GestureDetector. The detector needs to see the entire cluster
 | 
				
			||||
        // of events in order to properly interpret other gestures, such as long press.
 | 
				
			||||
        mDetector.onTouchEvent(e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,152 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
 | 
				
			||||
import android.util.Log;
 | 
				
			||||
import android.view.MotionEvent;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
 | 
				
			||||
import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * A MotionInputHandler that provides the high-level glue for touch driven selection. This class
 | 
				
			||||
 * works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} to
 | 
				
			||||
 * to implement the primary policies around touch input.
 | 
				
			||||
 *
 | 
				
			||||
 * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
 | 
				
			||||
 */
 | 
				
			||||
final class TouchInputHandler<K> extends MotionInputHandler<K> {
 | 
				
			||||
 | 
				
			||||
    private static final String TAG = "TouchInputDelegate";
 | 
				
			||||
    private static final boolean DEBUG = false;
 | 
				
			||||
 | 
				
			||||
    private final ItemDetailsLookup<K> mDetailsLookup;
 | 
				
			||||
    private final SelectionPredicate<K> mSelectionPredicate;
 | 
				
			||||
    private final OnItemActivatedListener<K> mOnItemActivatedListener;
 | 
				
			||||
    private final OnDragInitiatedListener mOnDragInitiatedListener;
 | 
				
			||||
    private final Runnable mGestureStarter;
 | 
				
			||||
    private final Runnable mHapticPerformer;
 | 
				
			||||
 | 
				
			||||
    TouchInputHandler(
 | 
				
			||||
            @NonNull SelectionTracker<K> selectionTracker,
 | 
				
			||||
            @NonNull ItemKeyProvider<K> keyProvider,
 | 
				
			||||
            @NonNull ItemDetailsLookup<K> detailsLookup,
 | 
				
			||||
            @NonNull SelectionPredicate<K> selectionPredicate,
 | 
				
			||||
            @NonNull Runnable gestureStarter,
 | 
				
			||||
            @NonNull OnDragInitiatedListener onDragInitiatedListener,
 | 
				
			||||
            @NonNull OnItemActivatedListener<K> onItemActivatedListener,
 | 
				
			||||
            @NonNull FocusDelegate<K> focusDelegate,
 | 
				
			||||
            @NonNull Runnable hapticPerformer) {
 | 
				
			||||
 | 
				
			||||
        super(selectionTracker, keyProvider, focusDelegate);
 | 
				
			||||
 | 
				
			||||
        checkArgument(detailsLookup != null);
 | 
				
			||||
        checkArgument(selectionPredicate != null);
 | 
				
			||||
        checkArgument(gestureStarter != null);
 | 
				
			||||
        checkArgument(onItemActivatedListener != null);
 | 
				
			||||
        checkArgument(onDragInitiatedListener != null);
 | 
				
			||||
        checkArgument(hapticPerformer != null);
 | 
				
			||||
 | 
				
			||||
        mDetailsLookup = detailsLookup;
 | 
				
			||||
        mSelectionPredicate = selectionPredicate;
 | 
				
			||||
        mGestureStarter = gestureStarter;
 | 
				
			||||
        mOnItemActivatedListener = onItemActivatedListener;
 | 
				
			||||
        mOnDragInitiatedListener = onDragInitiatedListener;
 | 
				
			||||
        mHapticPerformer = hapticPerformer;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public boolean onSingleTapUp(@NonNull MotionEvent e) {
 | 
				
			||||
        if (!mDetailsLookup.overItemWithSelectionKey(e)) {
 | 
				
			||||
            if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
 | 
				
			||||
            mSelectionTracker.clearSelection();
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
 | 
				
			||||
        // Should really not be null at this point, but...
 | 
				
			||||
        if (item == null) {
 | 
				
			||||
            return false;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (mSelectionTracker.hasSelection()) {
 | 
				
			||||
            if (isRangeExtension(e)) {
 | 
				
			||||
                extendSelectionRange(item);
 | 
				
			||||
            } else if (mSelectionTracker.isSelected(item.getSelectionKey())) {
 | 
				
			||||
                mSelectionTracker.deselect(item.getSelectionKey());
 | 
				
			||||
            } else {
 | 
				
			||||
                selectItem(item);
 | 
				
			||||
            }
 | 
				
			||||
 | 
				
			||||
            return true;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // Touch events select if they occur in the selection hotspot,
 | 
				
			||||
        // otherwise they activate.
 | 
				
			||||
        return item.inSelectionHotspot(e)
 | 
				
			||||
                ? selectItem(item)
 | 
				
			||||
                : mOnItemActivatedListener.onItemActivated(item, e);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void onLongPress(@NonNull MotionEvent e) {
 | 
				
			||||
        if (!mDetailsLookup.overItemWithSelectionKey(e)) {
 | 
				
			||||
            if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item.");
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
 | 
				
			||||
        // Should really not be null at this point, but...
 | 
				
			||||
        if (item == null) {
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        boolean handled = false;
 | 
				
			||||
 | 
				
			||||
        if (isRangeExtension(e)) {
 | 
				
			||||
            extendSelectionRange(item);
 | 
				
			||||
            handled = true;
 | 
				
			||||
        } else {
 | 
				
			||||
            if (!mSelectionTracker.isSelected(item.getSelectionKey())
 | 
				
			||||
                    && mSelectionPredicate.canSetStateForKey(item.getSelectionKey(), true)) {
 | 
				
			||||
                // If we cannot select it, we didn't apply anchoring - therefore should not
 | 
				
			||||
                // start gesture selection
 | 
				
			||||
                if (selectItem(item)) {
 | 
				
			||||
                    // And finally if the item was selected && we can select multiple
 | 
				
			||||
                    // we kick off gesture selection.
 | 
				
			||||
                    if (mSelectionPredicate.canSelectMultiple()) {
 | 
				
			||||
                        mGestureStarter.run();
 | 
				
			||||
                    }
 | 
				
			||||
                    handled = true;
 | 
				
			||||
                }
 | 
				
			||||
            } else {
 | 
				
			||||
                // We only initiate drag and drop on long press for touch to allow regular
 | 
				
			||||
                // touch-based scrolling
 | 
				
			||||
                mOnDragInitiatedListener.onDragInitiated(e);
 | 
				
			||||
                handled = true;
 | 
				
			||||
            }
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (handled) {
 | 
				
			||||
            mHapticPerformer.run();
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,273 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2017 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 androidx.recyclerview.selection;
 | 
				
			||||
 | 
				
			||||
import static androidx.core.util.Preconditions.checkArgument;
 | 
				
			||||
import static androidx.core.util.Preconditions.checkState;
 | 
				
			||||
import static androidx.recyclerview.selection.Shared.DEBUG;
 | 
				
			||||
import static androidx.recyclerview.selection.Shared.VERBOSE;
 | 
				
			||||
 | 
				
			||||
import android.graphics.Point;
 | 
				
			||||
import android.util.Log;
 | 
				
			||||
 | 
				
			||||
import androidx.annotation.NonNull;
 | 
				
			||||
import androidx.annotation.Nullable;
 | 
				
			||||
import androidx.annotation.VisibleForTesting;
 | 
				
			||||
import androidx.core.view.ViewCompat;
 | 
				
			||||
import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Provides auto-scrolling upon request when user's interaction with the application
 | 
				
			||||
 * introduces a natural intent to scroll. Used by BandSelectionHelper and GestureSelectionHelper,
 | 
				
			||||
 * to provide auto scrolling when user is performing selection operations.
 | 
				
			||||
 */
 | 
				
			||||
final class ViewAutoScroller extends AutoScroller {
 | 
				
			||||
 | 
				
			||||
    private static final String TAG = "ViewAutoScroller";
 | 
				
			||||
 | 
				
			||||
    // ratio used to calculate the top/bottom hotspot region; used with view height
 | 
				
			||||
    private static final float DEFAULT_SCROLL_THRESHOLD_RATIO = 0.125f;
 | 
				
			||||
    private static final int MAX_SCROLL_STEP = 70;
 | 
				
			||||
 | 
				
			||||
    private final float mScrollThresholdRatio;
 | 
				
			||||
 | 
				
			||||
    private final ScrollHost mHost;
 | 
				
			||||
    private final Runnable mRunner;
 | 
				
			||||
 | 
				
			||||
    private @Nullable Point mOrigin;
 | 
				
			||||
    private @Nullable Point mLastLocation;
 | 
				
			||||
    private boolean mPassedInitialMotionThreshold;
 | 
				
			||||
 | 
				
			||||
    ViewAutoScroller(@NonNull ScrollHost scrollHost) {
 | 
				
			||||
        this(scrollHost, DEFAULT_SCROLL_THRESHOLD_RATIO);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @VisibleForTesting
 | 
				
			||||
    ViewAutoScroller(@NonNull ScrollHost scrollHost, float scrollThresholdRatio) {
 | 
				
			||||
 | 
				
			||||
        checkArgument(scrollHost != null);
 | 
				
			||||
 | 
				
			||||
        mHost = scrollHost;
 | 
				
			||||
        mScrollThresholdRatio = scrollThresholdRatio;
 | 
				
			||||
 | 
				
			||||
        mRunner = new Runnable() {
 | 
				
			||||
            @Override
 | 
				
			||||
            public void run() {
 | 
				
			||||
                runScroll();
 | 
				
			||||
            }
 | 
				
			||||
        };
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void reset() {
 | 
				
			||||
        mHost.removeCallback(mRunner);
 | 
				
			||||
        mOrigin = null;
 | 
				
			||||
        mLastLocation = null;
 | 
				
			||||
        mPassedInitialMotionThreshold = false;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    @Override
 | 
				
			||||
    public void scroll(@NonNull Point location) {
 | 
				
			||||
        mLastLocation = location;
 | 
				
			||||
 | 
				
			||||
        // See #aboveMotionThreshold for details on how we track initial location.
 | 
				
			||||
        if (mOrigin == null) {
 | 
				
			||||
            mOrigin = location;
 | 
				
			||||
            if (VERBOSE) Log.v(TAG, "Origin @ " + mOrigin);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (VERBOSE) Log.v(TAG, "Current location @ " + mLastLocation);
 | 
				
			||||
 | 
				
			||||
        mHost.runAtNextFrame(mRunner);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Attempts to smooth-scroll the view at the given UI frame. Application should be
 | 
				
			||||
     * responsible to do any clean up (such as unsubscribing scrollListeners) after the run has
 | 
				
			||||
     * finished, and re-run this method on the next UI frame if applicable.
 | 
				
			||||
     */
 | 
				
			||||
    @SuppressWarnings("WeakerAccess") /* synthetic access */
 | 
				
			||||
    void runScroll() {
 | 
				
			||||
        if (DEBUG) checkState(mLastLocation != null);
 | 
				
			||||
 | 
				
			||||
        if (VERBOSE) Log.v(TAG, "Running in background using event location @ " + mLastLocation);
 | 
				
			||||
 | 
				
			||||
        // 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 top/bottom threshold is added here so that the view still scrolls when the
 | 
				
			||||
        // pointer are in these buffer pixels.
 | 
				
			||||
        int pixelsPastView = 0;
 | 
				
			||||
 | 
				
			||||
        final int verticalThreshold = (int) (mHost.getViewHeight()
 | 
				
			||||
                * mScrollThresholdRatio);
 | 
				
			||||
 | 
				
			||||
        if (mLastLocation.y <= verticalThreshold) {
 | 
				
			||||
            pixelsPastView = mLastLocation.y - verticalThreshold;
 | 
				
			||||
        } else if (mLastLocation.y >= mHost.getViewHeight()
 | 
				
			||||
                - verticalThreshold) {
 | 
				
			||||
            pixelsPastView = mLastLocation.y - mHost.getViewHeight()
 | 
				
			||||
                    + verticalThreshold;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        if (pixelsPastView == 0) {
 | 
				
			||||
            // If the operation that started the scrolling is no longer inactive, or if it is active
 | 
				
			||||
            // but not at the edge of the view, no scrolling is necessary.
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // We're in one of the endzones. Now determine if there's enough of a difference
 | 
				
			||||
        // from the orgin to take any action. Basically if a user has somehow initiated
 | 
				
			||||
        // selection, but is hovering at or near their initial contact point, we don't
 | 
				
			||||
        // scroll. This avoids a situation where the user initiates selection in an "endzone"
 | 
				
			||||
        // only to have scrolling start automatically.
 | 
				
			||||
        if (!mPassedInitialMotionThreshold && !aboveMotionThreshold(mLastLocation)) {
 | 
				
			||||
            if (VERBOSE) Log.v(TAG, "Ignoring event below motion threshold.");
 | 
				
			||||
            return;
 | 
				
			||||
        }
 | 
				
			||||
        mPassedInitialMotionThreshold = true;
 | 
				
			||||
 | 
				
			||||
        if (pixelsPastView > verticalThreshold) {
 | 
				
			||||
            pixelsPastView = verticalThreshold;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        // Compute the number of pixels to scroll, and scroll that many pixels.
 | 
				
			||||
        final int numPixels = computeScrollDistance(pixelsPastView);
 | 
				
			||||
        mHost.scrollBy(numPixels);
 | 
				
			||||
 | 
				
			||||
        // Replace any existing scheduled jobs with the latest and greatest..
 | 
				
			||||
        mHost.removeCallback(mRunner);
 | 
				
			||||
        mHost.runAtNextFrame(mRunner);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    private boolean aboveMotionThreshold(@NonNull Point location) {
 | 
				
			||||
        // We reuse the scroll threshold to calculate a much smaller area
 | 
				
			||||
        // in which we ignore motion initially.
 | 
				
			||||
        int motionThreshold =
 | 
				
			||||
                (int) ((mHost.getViewHeight() * mScrollThresholdRatio)
 | 
				
			||||
                        * (mScrollThresholdRatio * 2));
 | 
				
			||||
        return Math.abs(mOrigin.y - location.y) >= motionThreshold;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Computes the number of pixels to scroll based on how far the pointer is past the end
 | 
				
			||||
     * of the region. Roughly based on ItemTouchHelper's algorithm for computing the number of
 | 
				
			||||
     * pixels to scroll when an item is dragged to the end of a view.
 | 
				
			||||
     * @return
 | 
				
			||||
     */
 | 
				
			||||
    @VisibleForTesting
 | 
				
			||||
    int computeScrollDistance(int pixelsPastView) {
 | 
				
			||||
        final int topBottomThreshold =
 | 
				
			||||
                (int) (mHost.getViewHeight() * mScrollThresholdRatio);
 | 
				
			||||
 | 
				
			||||
        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 top/bottom scrolling hotspot of the view.
 | 
				
			||||
        final float outOfBoundsRatio = Math.min(
 | 
				
			||||
                1.0f, (float) absPastView / topBottomThreshold);
 | 
				
			||||
        // Interpolate this ratio and use it to compute the maximum scroll that should be
 | 
				
			||||
        // possible for this step.
 | 
				
			||||
        final int cappedScrollStep =
 | 
				
			||||
                (int) (direction * MAX_SCROLL_STEP * smoothOutOfBoundsRatio(outOfBoundsRatio));
 | 
				
			||||
 | 
				
			||||
        // If the final number of pixels to scroll ends up being 0, the view should still
 | 
				
			||||
        // scroll at least one pixel.
 | 
				
			||||
        return cappedScrollStep != 0 ? cappedScrollStep : direction;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * 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 threshold does little to no
 | 
				
			||||
     * scrolling, while drags that are near the edge of the view does a lot of
 | 
				
			||||
     * scrolling. The equation y=x^10 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, 10);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Used by to calculate the proper amount of pixels to scroll given time passed
 | 
				
			||||
     * since scroll started, and to properly scroll / proper listener clean up if necessary.
 | 
				
			||||
     *
 | 
				
			||||
     * Callback used by scroller to perform UI tasks, such as scrolling and rerunning at next UI
 | 
				
			||||
     * cycle.
 | 
				
			||||
     */
 | 
				
			||||
    abstract static class ScrollHost {
 | 
				
			||||
        /**
 | 
				
			||||
         * @return height of the view.
 | 
				
			||||
         */
 | 
				
			||||
        abstract int getViewHeight();
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * @param dy distance to scroll.
 | 
				
			||||
         */
 | 
				
			||||
        abstract void scrollBy(int dy);
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * @param r schedule runnable to be run at next convenient time.
 | 
				
			||||
         */
 | 
				
			||||
        abstract void runAtNextFrame(@NonNull Runnable r);
 | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * @param r remove runnable from being run.
 | 
				
			||||
         */
 | 
				
			||||
        abstract void removeCallback(@NonNull Runnable r);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    static ScrollHost createScrollHost(final RecyclerView recyclerView) {
 | 
				
			||||
        return new RuntimeHost(recyclerView);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Tracks location of last surface contact as reported by RecyclerView.
 | 
				
			||||
     */
 | 
				
			||||
    private static final class RuntimeHost extends ScrollHost {
 | 
				
			||||
 | 
				
			||||
        private final RecyclerView mRecyclerView;
 | 
				
			||||
 | 
				
			||||
        RuntimeHost(@NonNull RecyclerView recyclerView) {
 | 
				
			||||
            mRecyclerView = recyclerView;
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        void runAtNextFrame(@NonNull Runnable r) {
 | 
				
			||||
            ViewCompat.postOnAnimation(mRecyclerView, r);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        void removeCallback(@NonNull Runnable r) {
 | 
				
			||||
            mRecyclerView.removeCallbacks(r);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        void scrollBy(int dy) {
 | 
				
			||||
            if (VERBOSE) Log.v(TAG, "Scrolling view by: " + dy);
 | 
				
			||||
            mRecyclerView.scrollBy(0, dy);
 | 
				
			||||
        }
 | 
				
			||||
 | 
				
			||||
        @Override
 | 
				
			||||
        int getViewHeight() {
 | 
				
			||||
            return mRecyclerView.getHeight();
 | 
				
			||||
        }
 | 
				
			||||
    }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,129 @@
 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2018 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.
 | 
				
			||||
 */
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * A {@link androidx.recyclerview.widget.RecyclerView RecyclerView} addon library providing
 | 
				
			||||
 * support for item selection. The library provides support for both touch
 | 
				
			||||
 * and mouse driven selection. Developers retain control over the visual representation,
 | 
				
			||||
 * and the policies controlling selection behavior (like which items are eligible
 | 
				
			||||
 * for selection, and how many items can be selected.)
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Want to add selection support to your RecyclerView? Here's how you do it:
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Determine which selection key type to use, then build your KeyProvider</b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Developers must decide on the key type used to identify selected items. Support
 | 
				
			||||
 * is provided for three types: {@link android.os.Parcelable Parcelable},
 | 
				
			||||
 * {@link java.lang.String String}, and {@link java.lang.Long Long}.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * See
 | 
				
			||||
 * {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
 | 
				
			||||
 * for more detailed advice on which key type to use for your selection keys.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Implement {@link androidx.recyclerview.selection.ItemDetailsLookup ItemDetailsLookup}
 | 
				
			||||
 * </b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * This class provides the selection library code necessary access to information about
 | 
				
			||||
 * items associated with {@link android.view.MotionEvent}. This will likely
 | 
				
			||||
 * depend on concrete {@link androidx.recyclerview.widget.RecyclerView.ViewHolder
 | 
				
			||||
 * RecyclerView.ViewHolder} type employed by your application.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Update views used in RecyclerView to reflect selected state</b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * When the user selects an item the library will record that in
 | 
				
			||||
 * {@link androidx.recyclerview.selection.SelectionTracker SelectionTracker}
 | 
				
			||||
 * then notify RecyclerView that the state of the item has changed. This
 | 
				
			||||
 * will ultimately cause the value to be rebound by way of
 | 
				
			||||
 * {@link androidx.recyclerview.widget.RecyclerView.Adapter#onBindViewHolder
 | 
				
			||||
 *     RecyclerView.Adapter#onBindViewHolder}. The item must then be updated
 | 
				
			||||
 *     to reflect the new selection status. Without this
 | 
				
			||||
 *     the user will not *see* that the item has been selected.
 | 
				
			||||
 *
 | 
				
			||||
 * <ul>
 | 
				
			||||
 *     <li>In Adapter#onBindViewHolder, set the "activated" status on view.
 | 
				
			||||
 *     Note that the status should be "activated" not "selected".
 | 
				
			||||
 *     See <a href="https://developer.android.com/reference/android/view/View.html#setActivated(boolean)">
 | 
				
			||||
 *         View.html#setActivated</a> for details.
 | 
				
			||||
 *     <li>Update the styling of the view to represent the activated status. This can be done
 | 
				
			||||
 *     with a
 | 
				
			||||
 *     <a href="https://developer.android.com/guide/topics/resources/color-list-resource.html">
 | 
				
			||||
 *         color state list</a>.
 | 
				
			||||
 * </ul>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Use {@link androidx.appcompat.view.ActionMode ActionMode} when there is a selection</b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Register a {@link androidx.recyclerview.selection.SelectionTracker.SelectionObserver}
 | 
				
			||||
 * to be notified when selection changes. When a selection is first created, start
 | 
				
			||||
 * {@link androidx.appcompat.view.ActionMode ActionMode} to represent this to the user,
 | 
				
			||||
 * and provide selection specific actions.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Interpreted secondary actions: Drag and Drop, and Item Activation</b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * At the end of the event processing pipeline the library may determine that the user
 | 
				
			||||
 * is attempting to activate an item by tapping it, or is attempting to drag and drop
 | 
				
			||||
 * an item or set of selected items. React to these interpretations by registering a
 | 
				
			||||
 * respective listener. See
 | 
				
			||||
 * {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
 | 
				
			||||
 * for details.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Assemble everything with
 | 
				
			||||
 * {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
 | 
				
			||||
 * </b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * Example usage (with {@code Long} selection keys:
 | 
				
			||||
 * <pre>SelectionTracker<Long> tracker = new SelectionTracker.Builder<>(
 | 
				
			||||
 *        "my-selection-id",
 | 
				
			||||
 *        recyclerView,
 | 
				
			||||
 *        new StableIdKeyProvider(recyclerView),
 | 
				
			||||
 *        new MyDetailsLookup(recyclerView),
 | 
				
			||||
 *        StorageStrategy.createLongStorage())
 | 
				
			||||
 *        .build();
 | 
				
			||||
 *</pre>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>In order to build a SelectionTracker instance the supplied RecyclerView must be initialized
 | 
				
			||||
 * with an Adapter. Given this fact, you will probably need to inject the SelectionTracker
 | 
				
			||||
 * instance into your RecyclerView.Adapter after the Adapter is created, as it will be necessary
 | 
				
			||||
 * to consult selected status using SelectionTracker from the onBindViewHolder method.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * <b>Include Selection in Activity lifecycle events</b>
 | 
				
			||||
 *
 | 
				
			||||
 * <p>
 | 
				
			||||
 * In order to preserve state the author must the selection library in handling
 | 
				
			||||
 * of Activity lifecycle events. See SelectionTracker#onSaveInstanceState
 | 
				
			||||
 * and SelectionTracker#onRestoreInstanceState.
 | 
				
			||||
 *
 | 
				
			||||
 * <p>A unique selection id must be supplied to
 | 
				
			||||
 * {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
 | 
				
			||||
 * constructor. This is necessary as an activity or fragment may have multiple distinct
 | 
				
			||||
 * selectable lists that may both need to be persisted in saved state.
 | 
				
			||||
 */
 | 
				
			||||
 | 
				
			||||
package androidx.recyclerview.selection;
 | 
				
			||||
@ -0,0 +1,22 @@
 | 
				
			||||
<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||
<!--
 | 
				
			||||
  ~ Copyright 2017 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
 | 
				
			||||
  -->
 | 
				
			||||
 | 
				
			||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||
       android:shape="rectangle">
 | 
				
			||||
    <solid android:color="#339999ff" />
 | 
				
			||||
    <stroke android:width="1dp" android:color="#44000000" />
 | 
				
			||||
</shape>
 | 
				
			||||
					Loading…
					
					
				
		Reference in new issue