commit
0372df129e
@ -0,0 +1,983 @@ |
||||
/* |
||||
* Copyright 2020 Mupceet |
||||
* |
||||
* 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 io.legado.app.ui.widget.recycler; |
||||
|
||||
import android.content.res.Resources; |
||||
import android.text.TextUtils; |
||||
import android.util.DisplayMetrics; |
||||
import android.util.Log; |
||||
import android.util.TypedValue; |
||||
import android.view.MotionEvent; |
||||
import android.view.View; |
||||
|
||||
import androidx.annotation.NonNull; |
||||
import androidx.annotation.Nullable; |
||||
import androidx.core.view.ViewCompat; |
||||
import androidx.recyclerview.widget.GridLayoutManager; |
||||
import androidx.recyclerview.widget.RecyclerView; |
||||
|
||||
import java.util.HashSet; |
||||
import java.util.Locale; |
||||
import java.util.Set; |
||||
|
||||
import io.legado.app.BuildConfig; |
||||
|
||||
/** |
||||
* @author mupceet |
||||
*/ |
||||
public class DragSelectTouchHelper { |
||||
private static final String TAG = "DSTH"; |
||||
private static final float MAX_HOTSPOT_RATIO = 0.5f; |
||||
private static final EdgeType DEFAULT_EDGE_TYPE = EdgeType.INSIDE_EXTEND; |
||||
private static final float DEFAULT_HOTSPOT_RATIO = 0.2f; |
||||
private static final int DEFAULT_HOTSPOT_OFFSET = 0; |
||||
private static final int DEFAULT_MAX_SCROLL_VELOCITY = 10; |
||||
private static final int SELECT_STATE_NORMAL = 0x00; |
||||
|
||||
/* |
||||
* !autoChangeMode +-------------------+ inactiveSelect() |
||||
* +------------------------------------> | | <--------------------+ |
||||
* | | Normal | | |
||||
* | activeDragSelect(position) | | activeSlideSelect() | |
||||
* | +------------------------------ | | ----------+ | |
||||
* | v +-------------------+ v | |
||||
* +-------------------+ autoChangeMode +-----------------------+ |
||||
* | Drag From Disable | ----------------------------------------------> | | |
||||
* +-------------------+ | | |
||||
* | | | | |
||||
* | | activeDragSelect(position) && allowDragInSlide | Slide | |
||||
* | | <---------------------------------------------- | | |
||||
* | Drag From Slide | | | |
||||
* | | | | |
||||
* | | ----------------------------------------------> | | |
||||
* +-------------------+ +-----------------------+ |
||||
*/ |
||||
|
||||
private static final int SELECT_STATE_SLIDE = 0x01; |
||||
private static final int SELECT_STATE_DRAG_FROM_NORMAL = 0x10; |
||||
private static final int SELECT_STATE_DRAG_FROM_SLIDE = 0x11; |
||||
private final DisplayMetrics mDisplayMetrics; |
||||
/** |
||||
* Start of the slide area. |
||||
*/ |
||||
private float mSlideAreaLeft; |
||||
/** |
||||
* End of the slide area. |
||||
*/ |
||||
private float mSlideAreaRight; |
||||
/** |
||||
* The hotspot height by the ratio of RecyclerView. |
||||
*/ |
||||
private float mHotspotHeightRatio; |
||||
/** |
||||
* The hotspot height. |
||||
*/ |
||||
private float mHotspotHeight = 0f; |
||||
/** |
||||
* The hotspot offset. |
||||
*/ |
||||
private float mHotspotOffset; |
||||
/** |
||||
* Whether should continue scrolling when move outside top hotspot region. |
||||
*/ |
||||
private boolean mScrollAboveTopRegion; |
||||
/** |
||||
* Whether should continue scrolling when move outside bottom hotspot region. |
||||
*/ |
||||
private boolean mScrollBelowBottomRegion; |
||||
/** |
||||
* The maximum velocity of auto scrolling. |
||||
*/ |
||||
private int mMaximumVelocity; |
||||
/** |
||||
* Whether should auto enter slide mode after drag select finished. |
||||
*/ |
||||
private boolean mShouldAutoChangeState; |
||||
/** |
||||
* Whether can drag selection in slide select mode. |
||||
*/ |
||||
private boolean mIsAllowDragInSlideState; |
||||
private RecyclerView mRecyclerView = null; |
||||
/** |
||||
* The coordinate of hotspot area. |
||||
*/ |
||||
private float mTopRegionFrom = -1f; |
||||
private float mTopRegionTo = -1f; |
||||
private float mBottomRegionFrom = -1f; |
||||
private float mBottomRegionTo = -1f; |
||||
private final View.OnLayoutChangeListener mOnLayoutChangeListener = new View.OnLayoutChangeListener() { |
||||
@Override |
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom, |
||||
int oldLeft, int oldTop, int oldRight, int oldBottom) { |
||||
if (oldLeft != left || oldRight != right || oldTop != top || oldBottom != bottom) { |
||||
if (v == mRecyclerView) { |
||||
Logger.i("onLayoutChange:new: " |
||||
+ left + " " + top + " " + right + " " + bottom); |
||||
Logger.i("onLayoutChange:old: " |
||||
+ oldLeft + " " + oldTop + " " + oldRight + " " + oldBottom); |
||||
init(bottom - top); |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
/** |
||||
* The current mode of selection. |
||||
*/ |
||||
private int mSelectState = SELECT_STATE_NORMAL; |
||||
/** |
||||
* Whether is in top hotspot area. |
||||
*/ |
||||
private boolean mIsInTopHotspot = false; |
||||
/** |
||||
* Whether is in bottom hotspot area. |
||||
*/ |
||||
private boolean mIsInBottomHotspot = false; |
||||
/** |
||||
* Indicates automatically scroll. |
||||
*/ |
||||
private boolean mIsScrolling; |
||||
/** |
||||
* The actual speed of the current moment. |
||||
*/ |
||||
private int mScrollDistance = 0; |
||||
/** |
||||
* The reference coordinate for the action start, used to avoid reverse scrolling. |
||||
*/ |
||||
private float mDownY = Float.MIN_VALUE; |
||||
/** |
||||
* The reference coordinates for the last action. |
||||
*/ |
||||
private float mLastX = Float.MIN_VALUE; |
||||
private float mLastY = Float.MIN_VALUE; |
||||
/** |
||||
* The selected items position. |
||||
*/ |
||||
private int mStart = RecyclerView.NO_POSITION; |
||||
private int mEnd = RecyclerView.NO_POSITION; |
||||
private int mLastRealStart = RecyclerView.NO_POSITION; |
||||
private int mLastRealEnd = RecyclerView.NO_POSITION; |
||||
private int mSlideStateStartPosition = RecyclerView.NO_POSITION; |
||||
private boolean mHaveCalledSelectStart = false; |
||||
/** |
||||
* Developer callback which controls the behavior of DragSelectTouchHelper. |
||||
*/ |
||||
@NonNull |
||||
private Callback mCallback; |
||||
private Runnable mScrollRunnable = new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
if (mIsScrolling) { |
||||
scrollBy(mScrollDistance); |
||||
ViewCompat.postOnAnimation(mRecyclerView, mScrollRunnable); |
||||
} |
||||
} |
||||
}; |
||||
private final RecyclerView.OnItemTouchListener mOnItemTouchListener = new RecyclerView.OnItemTouchListener() { |
||||
@Override |
||||
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { |
||||
Logger.d("onInterceptTouchEvent: x:" + e.getX() + ",y:" + e.getY() |
||||
+ ", " + MotionEvent.actionToString(e.getAction())); |
||||
RecyclerView.Adapter<?> adapter = rv.getAdapter(); |
||||
if (adapter == null || adapter.getItemCount() == 0) { |
||||
return false; |
||||
} |
||||
boolean intercept = false; |
||||
int action = e.getAction(); |
||||
int actionMask = action & MotionEvent.ACTION_MASK; |
||||
// It seems that it's unnecessary to process multiple pointers.
|
||||
switch (actionMask) { |
||||
case MotionEvent.ACTION_DOWN: |
||||
mDownY = e.getY(); |
||||
// call the selection start's callback before moving
|
||||
if (mSelectState == SELECT_STATE_SLIDE && isInSlideArea(e)) { |
||||
mSlideStateStartPosition = getItemPosition(rv, e); |
||||
if (mSlideStateStartPosition != RecyclerView.NO_POSITION) { |
||||
mCallback.onSelectStart(mSlideStateStartPosition); |
||||
mHaveCalledSelectStart = true; |
||||
} |
||||
intercept = true; |
||||
} |
||||
break; |
||||
case MotionEvent.ACTION_MOVE: |
||||
if (mSelectState == SELECT_STATE_DRAG_FROM_NORMAL |
||||
|| mSelectState == SELECT_STATE_DRAG_FROM_SLIDE) { |
||||
Logger.i("onInterceptTouchEvent: drag mode move"); |
||||
intercept = true; |
||||
} |
||||
break; |
||||
case MotionEvent.ACTION_UP: |
||||
if (mSelectState == SELECT_STATE_DRAG_FROM_NORMAL |
||||
|| mSelectState == SELECT_STATE_DRAG_FROM_SLIDE) { |
||||
intercept = true; |
||||
} |
||||
// fall through
|
||||
case MotionEvent.ACTION_CANCEL: |
||||
// finger is lifted before moving
|
||||
if (mSlideStateStartPosition != RecyclerView.NO_POSITION) { |
||||
selectFinished(mSlideStateStartPosition); |
||||
mSlideStateStartPosition = RecyclerView.NO_POSITION; |
||||
} |
||||
// selection has triggered
|
||||
if (mStart != RecyclerView.NO_POSITION) { |
||||
selectFinished(mEnd); |
||||
} |
||||
break; |
||||
default: |
||||
// do nothing
|
||||
} |
||||
// Intercept only when the selection is triggered
|
||||
Logger.d("intercept result: " + intercept); |
||||
return intercept; |
||||
} |
||||
|
||||
@Override |
||||
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { |
||||
if (!isActivated()) { |
||||
return; |
||||
} |
||||
Logger.d("onTouchEvent: x:" + e.getX() + ",y:" + e.getY() |
||||
+ ", " + MotionEvent.actionToString(e.getAction())); |
||||
int action = e.getAction(); |
||||
int actionMask = action & MotionEvent.ACTION_MASK; |
||||
switch (actionMask) { |
||||
case MotionEvent.ACTION_MOVE: |
||||
if (mSlideStateStartPosition != RecyclerView.NO_POSITION) { |
||||
selectFirstItem(mSlideStateStartPosition); |
||||
// selection is triggered
|
||||
mSlideStateStartPosition = RecyclerView.NO_POSITION; |
||||
Logger.i("onTouchEvent: after slide mode down"); |
||||
} |
||||
processAutoScroll(e); |
||||
if (!mIsInTopHotspot && !mIsInBottomHotspot) { |
||||
updateSelectedRange(rv, e); |
||||
} |
||||
break; |
||||
case MotionEvent.ACTION_CANCEL: |
||||
case MotionEvent.ACTION_UP: |
||||
selectFinished(mEnd); |
||||
break; |
||||
default: |
||||
// do nothing
|
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { |
||||
if (disallowIntercept) { |
||||
inactiveSelect(); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
public DragSelectTouchHelper(@NonNull Callback callback) { |
||||
mCallback = callback; |
||||
mDisplayMetrics = Resources.getSystem().getDisplayMetrics(); |
||||
setHotspotRatio(DEFAULT_HOTSPOT_RATIO); |
||||
setHotspotOffset(DEFAULT_HOTSPOT_OFFSET); |
||||
setMaximumVelocity(DEFAULT_MAX_SCROLL_VELOCITY); |
||||
setEdgeType(DEFAULT_EDGE_TYPE); |
||||
setAutoEnterSlideState(false); |
||||
setAllowDragInSlideState(false); |
||||
setSlideArea(0, 0); |
||||
} |
||||
|
||||
/** |
||||
* Attaches the DragSelectTouchHelper to the provided RecyclerView. If TouchHelper is already |
||||
* attached to a RecyclerView, it will first detach from the previous one. You can call this |
||||
* method with {@code null} to detach it from the current RecyclerView. |
||||
* |
||||
* @param recyclerView The RecyclerView instance to which you want to add this helper or |
||||
* {@code null} if you want to remove DragSelectTouchHelper from the |
||||
* current RecyclerView. |
||||
*/ |
||||
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { |
||||
if (mRecyclerView == recyclerView) { |
||||
return; // nothing to do
|
||||
} |
||||
if (mRecyclerView != null) { |
||||
mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); |
||||
} |
||||
mRecyclerView = recyclerView; |
||||
if (mRecyclerView != null) { |
||||
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); |
||||
mRecyclerView.addOnLayoutChangeListener(mOnLayoutChangeListener); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Activate the slide selection mode. |
||||
*/ |
||||
public void activeSlideSelect() { |
||||
activeSelectInternal(RecyclerView.NO_POSITION); |
||||
} |
||||
|
||||
/** |
||||
* Activate the selection mode with selected item position. Normally called on long press. |
||||
* |
||||
* @param position Indicates the position of selected item. |
||||
*/ |
||||
public void activeDragSelect(int position) { |
||||
activeSelectInternal(position); |
||||
} |
||||
|
||||
/** |
||||
* Exit the selection mode. |
||||
*/ |
||||
public void inactiveSelect() { |
||||
if (isActivated()) { |
||||
selectFinished(mEnd); |
||||
} else { |
||||
selectFinished(RecyclerView.NO_POSITION); |
||||
} |
||||
Logger.logSelectStateChange(mSelectState, SELECT_STATE_NORMAL); |
||||
mSelectState = SELECT_STATE_NORMAL; |
||||
} |
||||
|
||||
/** |
||||
* To determine whether it is in the selection mode. |
||||
* |
||||
* @return true if is in the selection mode. |
||||
*/ |
||||
public boolean isActivated() { |
||||
return (mSelectState != SELECT_STATE_NORMAL); |
||||
} |
||||
|
||||
/** |
||||
* Sets hotspot height by ratio of RecyclerView. |
||||
* |
||||
* @param ratio range (0, 0.5). |
||||
* @return The select helper, which may used to chain setter calls. |
||||
*/ |
||||
public DragSelectTouchHelper setHotspotRatio(float ratio) { |
||||
mHotspotHeightRatio = ratio; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets hotspot height. |
||||
* |
||||
* @param hotspotHeight hotspot height which unit is dp. |
||||
* @return The select helper, which may used to chain setter calls. |
||||
*/ |
||||
public DragSelectTouchHelper setHotspotHeight(int hotspotHeight) { |
||||
mHotspotHeight = dp2px(hotspotHeight); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets hotspot offset. It don't need to be set if no special requirement. |
||||
* |
||||
* @param hotspotOffset hotspot offset which unit is dp. |
||||
* @return The select helper, which may used to chain setter calls. |
||||
*/ |
||||
public DragSelectTouchHelper setHotspotOffset(int hotspotOffset) { |
||||
mHotspotOffset = dp2px(hotspotOffset); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the activation edge type, one of: |
||||
* <ul> |
||||
* <li>{@link EdgeType#INSIDE} for edges that respond to touches inside |
||||
* the bounds of the host view. If touch moves outside the bounds, scrolling |
||||
* will stop. |
||||
* <li>{@link EdgeType#INSIDE_EXTEND} for inside edges that continued to |
||||
* scroll when touch moves outside the bounds of the host view. |
||||
* </ul> |
||||
* |
||||
* @param type The type of edge to use. |
||||
* @return The select helper, which may used to chain setter calls. |
||||
*/ |
||||
public DragSelectTouchHelper setEdgeType(EdgeType type) { |
||||
switch (type) { |
||||
case INSIDE: |
||||
mScrollAboveTopRegion = false; |
||||
mScrollBelowBottomRegion = false; |
||||
break; |
||||
case INSIDE_EXTEND: |
||||
default: |
||||
mScrollAboveTopRegion = true; |
||||
mScrollBelowBottomRegion = true; |
||||
break; |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets sliding area's start and end, has been considered RTL situation |
||||
* |
||||
* @param startDp The start of the sliding area |
||||
* @param endDp The end of the sliding area |
||||
* @return The select helper, which may used to chain setter calls. |
||||
*/ |
||||
public DragSelectTouchHelper setSlideArea(int startDp, int endDp) { |
||||
if (!isRtl()) { |
||||
mSlideAreaLeft = dp2px(startDp); |
||||
mSlideAreaRight = dp2px(endDp); |
||||
} else { |
||||
int displayWidth = mDisplayMetrics.widthPixels; |
||||
mSlideAreaLeft = displayWidth - dp2px(endDp); |
||||
mSlideAreaRight = displayWidth - dp2px(startDp); |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the maximum velocity for scrolling |
||||
* |
||||
* @param velocity maximum velocity |
||||
* @return The select helper, which may used to chain setter calls. |
||||
*/ |
||||
public DragSelectTouchHelper setMaximumVelocity(int velocity) { |
||||
mMaximumVelocity = (int) (velocity * mDisplayMetrics.density + 0.5f); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets whether should auto enter slide mode after drag select finished. |
||||
* It's usefully for LinearLayout RecyclerView. |
||||
* |
||||
* @param autoEnterSlideState should auto enter slide mode |
||||
* @return The select helper, which may used to chain setter calls. |
||||
*/ |
||||
public DragSelectTouchHelper setAutoEnterSlideState(boolean autoEnterSlideState) { |
||||
mShouldAutoChangeState = autoEnterSlideState; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets whether can drag selection in slide select mode. |
||||
* It's usefully for LinearLayout RecyclerView. |
||||
* |
||||
* @param allowDragInSlideState allow drag selection in slide select mode |
||||
* @return The select helper, which may used to chain setter calls. |
||||
*/ |
||||
public DragSelectTouchHelper setAllowDragInSlideState(boolean allowDragInSlideState) { |
||||
mIsAllowDragInSlideState = allowDragInSlideState; |
||||
return this; |
||||
} |
||||
|
||||
private void init(int rvHeight) { |
||||
if (mHotspotOffset >= rvHeight * MAX_HOTSPOT_RATIO) { |
||||
mHotspotOffset = rvHeight * MAX_HOTSPOT_RATIO; |
||||
} |
||||
// The height of hotspot area is not set, using (RV height x ratio)
|
||||
if (mHotspotHeight <= 0) { |
||||
if (mHotspotHeightRatio <= 0 || mHotspotHeightRatio >= MAX_HOTSPOT_RATIO) { |
||||
mHotspotHeightRatio = DEFAULT_HOTSPOT_RATIO; |
||||
} |
||||
mHotspotHeight = rvHeight * mHotspotHeightRatio; |
||||
} else { |
||||
if (mHotspotHeight >= rvHeight * MAX_HOTSPOT_RATIO) { |
||||
mHotspotHeight = rvHeight * MAX_HOTSPOT_RATIO; |
||||
} |
||||
} |
||||
|
||||
mTopRegionFrom = mHotspotOffset; |
||||
mTopRegionTo = mTopRegionFrom + mHotspotHeight; |
||||
mBottomRegionTo = rvHeight - mHotspotOffset; |
||||
mBottomRegionFrom = mBottomRegionTo - mHotspotHeight; |
||||
|
||||
if (mTopRegionTo > mBottomRegionFrom) { |
||||
mTopRegionTo = mBottomRegionFrom = rvHeight >> 1; |
||||
} |
||||
|
||||
Logger.d("Hotspot: [" + mTopRegionFrom + ", " + mTopRegionTo + "], [" |
||||
+ mBottomRegionFrom + ", " + mBottomRegionTo + "]"); |
||||
} |
||||
|
||||
private void activeSelectInternal(int position) { |
||||
// We should initialize the hotspot here, because its data may be delayed load
|
||||
if (mRecyclerView != null) { |
||||
init(mRecyclerView.getHeight()); |
||||
} |
||||
if (position == RecyclerView.NO_POSITION) { |
||||
Logger.logSelectStateChange(mSelectState, SELECT_STATE_SLIDE); |
||||
mSelectState = SELECT_STATE_SLIDE; |
||||
} else { |
||||
if (!mHaveCalledSelectStart) { |
||||
mCallback.onSelectStart(position); |
||||
mHaveCalledSelectStart = true; |
||||
} |
||||
if (mSelectState == SELECT_STATE_SLIDE) { |
||||
if (mIsAllowDragInSlideState && selectFirstItem(position)) { |
||||
Logger.logSelectStateChange(mSelectState, SELECT_STATE_DRAG_FROM_SLIDE); |
||||
mSelectState = SELECT_STATE_DRAG_FROM_SLIDE; |
||||
} |
||||
} else if (mSelectState == SELECT_STATE_NORMAL) { |
||||
if (selectFirstItem(position)) { |
||||
Logger.logSelectStateChange(mSelectState, SELECT_STATE_DRAG_FROM_NORMAL); |
||||
mSelectState = SELECT_STATE_DRAG_FROM_NORMAL; |
||||
} |
||||
} else { |
||||
Logger.e("activeSelect in unexpected state: " + mSelectState); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private boolean selectFirstItem(int position) { |
||||
boolean selectFirstItemSucceed = mCallback.onSelectChange(position, true); |
||||
// The drag select feature is only available if the first item is available for selection
|
||||
if (selectFirstItemSucceed) { |
||||
mStart = position; |
||||
mEnd = position; |
||||
mLastRealStart = position; |
||||
mLastRealEnd = position; |
||||
} |
||||
return selectFirstItemSucceed; |
||||
} |
||||
|
||||
private void updateSelectedRange(RecyclerView rv, MotionEvent e) { |
||||
updateSelectedRange(rv, e.getX(), e.getY()); |
||||
} |
||||
|
||||
private void updateSelectedRange(RecyclerView rv, float x, float y) { |
||||
int position = getItemPosition(rv, x, y); |
||||
if (position != RecyclerView.NO_POSITION && mEnd != position) { |
||||
mEnd = position; |
||||
notifySelectRangeChange(); |
||||
} |
||||
} |
||||
|
||||
private void notifySelectRangeChange() { |
||||
if (mStart == RecyclerView.NO_POSITION || mEnd == RecyclerView.NO_POSITION) { |
||||
return; |
||||
} |
||||
|
||||
int newStart, newEnd; |
||||
newStart = Math.min(mStart, mEnd); |
||||
newEnd = Math.max(mStart, mEnd); |
||||
if (mLastRealStart == RecyclerView.NO_POSITION || mLastRealEnd == RecyclerView.NO_POSITION) { |
||||
if (newEnd - newStart == 1) { |
||||
notifySelectChange(newStart, newStart, true); |
||||
} else { |
||||
notifySelectChange(newStart, newEnd, true); |
||||
} |
||||
} else { |
||||
if (newStart > mLastRealStart) { |
||||
notifySelectChange(mLastRealStart, newStart - 1, false); |
||||
} else if (newStart < mLastRealStart) { |
||||
notifySelectChange(newStart, mLastRealStart - 1, true); |
||||
} |
||||
|
||||
if (newEnd > mLastRealEnd) { |
||||
notifySelectChange(mLastRealEnd + 1, newEnd, true); |
||||
} else if (newEnd < mLastRealEnd) { |
||||
notifySelectChange(newEnd + 1, mLastRealEnd, false); |
||||
} |
||||
} |
||||
|
||||
mLastRealStart = newStart; |
||||
mLastRealEnd = newEnd; |
||||
} |
||||
|
||||
private void notifySelectChange(int start, int end, boolean newState) { |
||||
for (int i = start; i <= end; i++) { |
||||
mCallback.onSelectChange(i, newState); |
||||
} |
||||
} |
||||
|
||||
private void selectFinished(int lastItem) { |
||||
if (lastItem != RecyclerView.NO_POSITION) { |
||||
mCallback.onSelectEnd(lastItem); |
||||
} |
||||
mStart = RecyclerView.NO_POSITION; |
||||
mEnd = RecyclerView.NO_POSITION; |
||||
mLastRealStart = RecyclerView.NO_POSITION; |
||||
mLastRealEnd = RecyclerView.NO_POSITION; |
||||
mHaveCalledSelectStart = false; |
||||
mIsInTopHotspot = false; |
||||
mIsInBottomHotspot = false; |
||||
stopAutoScroll(); |
||||
switch (mSelectState) { |
||||
case SELECT_STATE_DRAG_FROM_NORMAL: |
||||
if (mShouldAutoChangeState) { |
||||
Logger.logSelectStateChange(mSelectState, SELECT_STATE_SLIDE); |
||||
mSelectState = SELECT_STATE_SLIDE; |
||||
} else { |
||||
Logger.logSelectStateChange(mSelectState, SELECT_STATE_NORMAL); |
||||
mSelectState = SELECT_STATE_NORMAL; |
||||
} |
||||
break; |
||||
case SELECT_STATE_DRAG_FROM_SLIDE: |
||||
Logger.logSelectStateChange(mSelectState, SELECT_STATE_SLIDE); |
||||
mSelectState = SELECT_STATE_SLIDE; |
||||
break; |
||||
default: |
||||
// doesn't change the selection state
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Process motion event, according to the location to determine whether to scroll |
||||
*/ |
||||
private void processAutoScroll(MotionEvent e) { |
||||
float y = e.getY(); |
||||
if (y >= mTopRegionFrom && y <= mTopRegionTo && y < mDownY) { |
||||
mLastX = e.getX(); |
||||
mLastY = e.getY(); |
||||
float scrollDistanceFactor = (y - mTopRegionTo) / mHotspotHeight; |
||||
mScrollDistance = (int) (mMaximumVelocity * scrollDistanceFactor); |
||||
if (!mIsInTopHotspot) { |
||||
mIsInTopHotspot = true; |
||||
startAutoScroll(); |
||||
mDownY = mTopRegionTo; |
||||
} |
||||
} else if (mScrollAboveTopRegion && y < mTopRegionFrom && mIsInTopHotspot) { |
||||
mLastX = e.getX(); |
||||
mLastY = mTopRegionFrom; |
||||
// Use the maximum speed
|
||||
mScrollDistance = mMaximumVelocity * -1; |
||||
startAutoScroll(); |
||||
} else if (y >= mBottomRegionFrom && y <= mBottomRegionTo && y > mDownY) { |
||||
mLastX = e.getX(); |
||||
mLastY = e.getY(); |
||||
float scrollDistanceFactor = (y - mBottomRegionFrom) / mHotspotHeight; |
||||
mScrollDistance = (int) (mMaximumVelocity * scrollDistanceFactor); |
||||
if (!mIsInBottomHotspot) { |
||||
mIsInBottomHotspot = true; |
||||
startAutoScroll(); |
||||
mDownY = mBottomRegionFrom; |
||||
} |
||||
} else if (mScrollBelowBottomRegion && y > mBottomRegionTo && mIsInBottomHotspot) { |
||||
mLastX = e.getX(); |
||||
mLastY = mBottomRegionTo; |
||||
// Use the maximum speed
|
||||
mScrollDistance = mMaximumVelocity; |
||||
startAutoScroll(); |
||||
} else { |
||||
mIsInTopHotspot = false; |
||||
mIsInBottomHotspot = false; |
||||
mLastX = Float.MIN_VALUE; |
||||
mLastY = Float.MIN_VALUE; |
||||
stopAutoScroll(); |
||||
} |
||||
|
||||
} |
||||
|
||||
private void startAutoScroll() { |
||||
if (!mIsScrolling) { |
||||
mIsScrolling = true; |
||||
mRecyclerView.removeCallbacks(mScrollRunnable); |
||||
ViewCompat.postOnAnimation(mRecyclerView, mScrollRunnable); |
||||
} |
||||
} |
||||
|
||||
private void stopAutoScroll() { |
||||
if (mIsScrolling) { |
||||
mIsScrolling = false; |
||||
mRecyclerView.removeCallbacks(mScrollRunnable); |
||||
} |
||||
} |
||||
|
||||
private void scrollBy(int distance) { |
||||
int scrollDistance; |
||||
if (distance > 0) { |
||||
scrollDistance = Math.min(distance, mMaximumVelocity); |
||||
} else { |
||||
scrollDistance = Math.max(distance, -mMaximumVelocity); |
||||
} |
||||
mRecyclerView.scrollBy(0, scrollDistance); |
||||
if (mLastX != Float.MIN_VALUE && mLastY != Float.MIN_VALUE) { |
||||
updateSelectedRange(mRecyclerView, mLastX, mLastY); |
||||
} |
||||
} |
||||
|
||||
private int dp2px(float dpVal) { |
||||
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, |
||||
dpVal, mDisplayMetrics); |
||||
} |
||||
|
||||
private boolean isRtl() { |
||||
return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) |
||||
== View.LAYOUT_DIRECTION_RTL; |
||||
} |
||||
|
||||
private boolean isInSlideArea(MotionEvent e) { |
||||
float x = e.getX(); |
||||
return (x > mSlideAreaLeft && x < mSlideAreaRight); |
||||
} |
||||
|
||||
private int getItemPosition(RecyclerView rv, MotionEvent e) { |
||||
return getItemPosition(rv, e.getX(), e.getY()); |
||||
} |
||||
|
||||
private int getItemPosition(RecyclerView rv, float x, float y) { |
||||
final View v = rv.findChildViewUnder(x, y); |
||||
if (v == null) { |
||||
RecyclerView.LayoutManager layoutManager = rv.getLayoutManager(); |
||||
if (layoutManager instanceof GridLayoutManager) { |
||||
int lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); |
||||
int lastItemPosition = layoutManager.getItemCount() - 1; |
||||
if (lastItemPosition == lastVisibleItemPosition) { |
||||
return lastItemPosition; |
||||
} |
||||
} |
||||
return RecyclerView.NO_POSITION; |
||||
} |
||||
return rv.getChildAdapterPosition(v); |
||||
} |
||||
|
||||
/** |
||||
* Edge type that specifies an activation area starting at the view bounds and extending inward. |
||||
*/ |
||||
public enum EdgeType { |
||||
/** |
||||
* After activation begins, moving outside the view bounds will stop scrolling. |
||||
*/ |
||||
INSIDE, |
||||
|
||||
/** |
||||
* After activation begins, moving outside the view bounds will continue scrolling. |
||||
*/ |
||||
INSIDE_EXTEND |
||||
} |
||||
|
||||
/** |
||||
* This class is the contract between DragSelectTouchHelper and your application. It lets you |
||||
* update adapter when selection start/end and state changed. |
||||
*/ |
||||
public abstract static class Callback { |
||||
/** |
||||
* Called when changing item state. |
||||
* |
||||
* @param position this item want to change the state to new state. |
||||
* @param isSelected true if the position should be selected, false otherwise. |
||||
* @return Whether to set the new state successfully. |
||||
*/ |
||||
public abstract boolean onSelectChange(int position, boolean isSelected); |
||||
|
||||
/** |
||||
* Called when selection start. |
||||
* |
||||
* @param start the first selected item. |
||||
*/ |
||||
public void onSelectStart(int start) { |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Called when selection end. |
||||
* |
||||
* @param end the last selected item. |
||||
*/ |
||||
public void onSelectEnd(int end) { |
||||
|
||||
} |
||||
} |
||||
|
||||
/** |
||||
* An advance Callback which provide 4 useful selection modes {@link Mode}. |
||||
* <p> |
||||
* Note: Since the state of item may be repeatedly set, in order to improve efficiency, |
||||
* please process it in the Adapter |
||||
*/ |
||||
public abstract static class AdvanceCallback<T> extends Callback { |
||||
private Mode mMode; |
||||
private Set<T> mOriginalSelection; |
||||
private boolean mFirstWasSelected; |
||||
|
||||
/** |
||||
* Creates a SimpleCallback with default {@link Mode#SelectAndReverse}# mode. |
||||
* |
||||
* @see Mode |
||||
*/ |
||||
public AdvanceCallback() { |
||||
setMode(Mode.SelectAndReverse); |
||||
} |
||||
|
||||
/** |
||||
* Creates a SimpleCallback with select mode. |
||||
* |
||||
* @param mode the initial select mode |
||||
* @see Mode |
||||
*/ |
||||
public AdvanceCallback(Mode mode) { |
||||
setMode(mode); |
||||
} |
||||
|
||||
/** |
||||
* Sets the select mode. |
||||
* |
||||
* @param mode The type of select mode. |
||||
* @see Mode |
||||
*/ |
||||
public void setMode(Mode mode) { |
||||
mMode = mode; |
||||
} |
||||
|
||||
@Override |
||||
public void onSelectStart(int start) { |
||||
mOriginalSelection = new HashSet<>(); |
||||
Set<T> selected = currentSelectedId(); |
||||
if (selected != null) { |
||||
mOriginalSelection.addAll(selected); |
||||
} |
||||
mFirstWasSelected = mOriginalSelection.contains(getItemId(start)); |
||||
} |
||||
|
||||
@Override |
||||
public void onSelectEnd(int end) { |
||||
mOriginalSelection = null; |
||||
} |
||||
|
||||
@Override |
||||
public boolean onSelectChange(int position, boolean isSelected) { |
||||
boolean stateChanged; |
||||
switch (mMode) { |
||||
case SelectAndKeep: { |
||||
stateChanged = updateSelectState(position, true); |
||||
break; |
||||
} |
||||
case SelectAndReverse: { |
||||
stateChanged = updateSelectState(position, isSelected); |
||||
break; |
||||
} |
||||
case SelectAndUndo: { |
||||
if (isSelected) { |
||||
stateChanged = updateSelectState(position, true); |
||||
} else { |
||||
stateChanged = updateSelectState(position, mOriginalSelection.contains(getItemId(position))); |
||||
} |
||||
break; |
||||
} |
||||
case ToggleAndKeep: { |
||||
stateChanged = updateSelectState(position, !mFirstWasSelected); |
||||
break; |
||||
} |
||||
case ToggleAndReverse: { |
||||
if (isSelected) { |
||||
stateChanged = updateSelectState(position, !mFirstWasSelected); |
||||
} else { |
||||
stateChanged = updateSelectState(position, mFirstWasSelected); |
||||
} |
||||
break; |
||||
} |
||||
case ToggleAndUndo: { |
||||
if (isSelected) { |
||||
stateChanged = updateSelectState(position, !mFirstWasSelected); |
||||
} else { |
||||
stateChanged = updateSelectState(position, mOriginalSelection.contains(getItemId(position))); |
||||
} |
||||
break; |
||||
} |
||||
default: |
||||
// SelectAndReverse Mode
|
||||
stateChanged = updateSelectState(position, isSelected); |
||||
} |
||||
return stateChanged; |
||||
} |
||||
|
||||
/** |
||||
* Get the currently selected items when selecting first item. |
||||
* |
||||
* @return the currently selected item's id set. |
||||
*/ |
||||
public abstract Set<T> currentSelectedId(); |
||||
|
||||
/** |
||||
* Get the ID of the item. |
||||
* |
||||
* @param position item position to be judged. |
||||
* @return item's identity. |
||||
*/ |
||||
public abstract T getItemId(int position); |
||||
|
||||
/** |
||||
* Update the selection status of the position. |
||||
* |
||||
* @param position the position who's selection state changed. |
||||
* @param isSelected true if the position should be selected, false otherwise. |
||||
* @return Whether to set the state successfully. |
||||
*/ |
||||
public abstract boolean updateSelectState(int position, boolean isSelected); |
||||
|
||||
/** |
||||
* Different existing selection modes |
||||
*/ |
||||
public enum Mode { |
||||
/** |
||||
* Selects the first item and applies the same state to each item you go by |
||||
* and keep the state on move back |
||||
*/ |
||||
SelectAndKeep, |
||||
/** |
||||
* Selects the first item and applies the same state to each item you go by |
||||
* and applies inverted state on move back |
||||
*/ |
||||
SelectAndReverse, |
||||
/** |
||||
* Selects the first item and applies the same state to each item you go by |
||||
* and reverts to the original state on move back |
||||
*/ |
||||
SelectAndUndo, |
||||
/** |
||||
* Toggles the first item and applies the same state to each item you go by |
||||
* and keep the state on move back |
||||
*/ |
||||
ToggleAndKeep, |
||||
/** |
||||
* Toggles the first item and applies the same state to each item you go by |
||||
* and applies inverted state on move back |
||||
*/ |
||||
ToggleAndReverse, |
||||
/** |
||||
* Toggles the first item and applies the same state to each item you go by |
||||
* and reverts to the original state on move back |
||||
*/ |
||||
ToggleAndUndo, |
||||
} |
||||
} |
||||
|
||||
private static class Logger { |
||||
private static boolean DEBUG = BuildConfig.DEBUG; |
||||
|
||||
private static void d(String msg) { |
||||
if (DEBUG) { |
||||
Log.d(TAG, msg); |
||||
} |
||||
} |
||||
|
||||
private static void e(String msg) { |
||||
Log.e(TAG, msg); |
||||
} |
||||
|
||||
private static void i(String msg) { |
||||
Log.i(TAG, msg); |
||||
} |
||||
|
||||
private static void logSelectStateChange(int before, int after) { |
||||
i("Select state changed: " + stateName(before) + " --> " + stateName(after)); |
||||
} |
||||
|
||||
private static String stateName(int state) { |
||||
switch (state) { |
||||
case SELECT_STATE_NORMAL: |
||||
return "NormalState"; |
||||
case SELECT_STATE_SLIDE: |
||||
return "SlideState"; |
||||
case SELECT_STATE_DRAG_FROM_NORMAL: |
||||
return "DragFromNormal"; |
||||
case SELECT_STATE_DRAG_FROM_SLIDE: |
||||
return "DragFromSlide"; |
||||
default: |
||||
return "Unknown"; |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue