英文:
Trigger click and drag actions from item in RecyclerView
问题
我有一个垂直滚动的RecyclerView,我想允许其中的项目被点击和重新排序。
RecyclerView中的每个项目在左侧有一个“StatusIndicator”(继承自AppCompatImageButton)。我已经向StatusIndicator添加了一个GestureDetector,这似乎有效,但垂直拖动不会发送到StatusIndicator。
我看到的行为是,如果我尝试在StatusIndicator上进行垂直滚动(意图触发拖动),则拖动既不会触发,也不会滚动RecyclerView。如果我继续滚动手势,但水平滚动,突然之间我就可以拖动该项目。
我假设ACTION_DOWN事件到达StatusIndicator的GestureDetector以触发拖动,但是RecyclerView捕获了随后(主要是垂直的)ACTION_MOVE,认为它需要处理它们以进行垂直滚动。一旦手势涉及的水平移动超过垂直移动,RecyclerView::onInterceptTouchEvent不再捕获事件(在调试中确认),然后ACTION_MOVE事件如预期地发送到StatusIndicator。
这里的正确方法是什么?
英文:
I have a vertically scrolling RecyclerView and I want to allow items on it to be clicked and reordered.
Each item in the RecyclerView has a "StatusIndicator" on the left hand side (inheriting from an AppCompatImageButton). I've added a GestureDetector to the StatusIndicator and this seems to work except that the vertical drag doesn't get sent to the StatusIndicator.
The behaviour I see is that if I attempt vertical scrolling on the StatusIndicator (intending to trigger a drag) neither the drag is triggered nor the RecyclerView is scrolled. If I continue the scrolling gesture but scroll horizontally suddenly I can drag the item.
I'm assuming the ACTION_DOWN event reaches the StatusIndicator's GestureDetector to trigger the drag but that the RecyclerView captures the following (predominantly vertical) ACTION_MOVE assuming it needs to handle them for it's vertical scroll. Once the gesture involves more horizontal than vertical movement RecyclerView::onInterceptTouchEvent no longer captures the event (confirmed in debug) and the ACTION_MOVEs are then sent to the StatusIndicator as intended.
What is the correct approach here?
The relevant bits of StatusIndicator.java:
class StatusIndicator extends AppCompatImageButton {
class GestureTouch extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent event) {
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (mStatusIndicatorListener != null)
mStatusIndicatorListener.incrementStatus();
createDrawableState();
return true;
}
@Override
public boolean onScroll (MotionEvent e1,
MotionEvent e2,
float distanceX,
float distanceY) {
mStatusIndicatorListener.startDrag();
return true;
}
}
private StatusIndicatorListener mStatusIndicatorListener;
private GestureDetector mTouchDetector;
public StatusIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchDetector = new GestureDetector(context,new GestureTouch());
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mTouchDetector.onTouchEvent(event);
return true;
}
答案1
得分: 0
以下是翻译好的部分:
我相信发生的情况是,任何ACTION_MOVE事件都会沿着堆栈向下传递,并被RecyclerView在其onInterceptTouchEvent方法中捕获。在这个过程中,RecyclerView.LayoutManager.canScrollVertically()被用来确定RecyclerView是否应该捕获事件。
最终,我继承了LinearLayoutManager,并重写了canScrollVertically()方法,以便我可以通过调用新方法来手动阻止它。然后,在RecyclerView.onInterceptTouchEvent中,我检查传入的ACTION_DOWN事件是否位于StatusIndicator之上,如果是的话,我会强制RecyclerView.LayoutManager.canScrollVertically()在下一个ACTION_DOWN事件之前返回false。
这是一个相当巧妙的解决方案,但似乎很可靠。如果有人提出一个“正确”的解决方案,我很乐意看看!
新的布局管理器:
public class ListLayoutManager extends LinearLayoutManager {
boolean mScrollBlocked;
public ListLayoutManager(Context context) {
super(context);
mScrollBlocked = false;
}
@Override
public boolean canScrollVertically() {
return (!mScrollBlocked && super.canScrollVertically());
}
public void blockScrolling() {
mScrollBlocked = true;
}
public void unblockScrolling() {
mScrollBlocked = false;
}
}
扩展了onInterceptTouchEvent的RecyclerView:
public class ListRecyclerView extends RecyclerView {
ListLayoutManager mListLayoutManager;
public ListRecyclerView(Context context) {
super(context);
}
public ListRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ListRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 我们重写了触摸处理,以便RecyclerView不处理出现在StatusIndicator上的任何内容。
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
mListLayoutManager.unblockScrolling();
View child = findChildViewUnder(e.getX(), e.getY());
if (child != null) {
StatusIndicator statusInd = child.findViewById(R.id.rowStatus);
if ((statusInd != null) &&
(e.getX() <= statusInd.getWidth())) {
mListLayoutManager.blockScrolling();
}
}
}
return super.onInterceptTouchEvent(e);
}
public void setListLayoutManager(ListLayoutManager lm) {
mListLayoutManager = lm;
setLayoutManager(mListLayoutManager);
}
}
英文:
I believe what was happenning was that any ACTION_MOVE event was heading down the stack and being captured by the RecyclerView within its onInterceptTouchEvent method. Within this RecyclerView.LayoutManager.canScrollVertically() is used to determine that the RecyclerView should capture the event.
In the end I inherited from LinearLayoutManager and overrode canScrollVertically() so I could manually block it with a call to a new method. Then, within RecyclerView.onInterceptTouchEvent, I checked whether the incoming ACTION_DOWN event was over the StatusIndicator and, if so, forced RecyclerView.LayoutManater.canScrollVertically() to return false until the next ACTION_DOWN event.
This is a pretty hacky solution but appears robust. If anyone comes along with a "proper" solution I'd be keen to see it!
The new layout manager:
public class ListLayoutManager extends LinearLayoutManager {
boolean mScrollBlocked;
public ListLayoutManager(Context context) {
super(context);
mScrollBlocked = false;
}
@Override
public boolean canScrollVertically() {
return (!mScrollBlocked && super.canScrollVertically());
}
public void blockScrolling() {
mScrollBlocked = true;
}
public void unblockScrolling() {
mScrollBlocked = false;
}
}
The extended RecyclerView with overridden onInterceptTouchEvent:
ListLayoutManager mListLayoutManager;
public ListRecyclerView(Context context) {
super(context);
}
public ListRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ListRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// We override touch handling so the RecyclerView doesn't handle
// anything which occurs over the StatusIndicator.
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
mListLayoutManager.unblockScrolling();
View child = findChildViewUnder(e.getX(), e.getY());
if (child != null) {
StatusIndicator statusInd = child.findViewById(R.id.rowStatus);
if ((statusInd != null) &&
(e.getX() <= statusInd.getWidth())) {
mListLayoutManager.blockScrolling();
}
}
}
return super.onInterceptTouchEvent(e);
}
public void setListLayoutManager(ListLayoutManager lm) {
mListLayoutManager = lm;
setLayoutManager(mListLayoutManager);
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论