HorizontalScrollView внутри SwipeRefreshLayout
Я реализовал новый SwipeRefreshLayout
компонент в моем приложении, и он хорошо работает с любыми вертикальными представлениями, такими как ListView
, GridView
а также ScrollView
,
Он ведет себя очень плохо с горизонтальными видами, как HorizontalScrollView
, При прокрутке вправо или влево, SwipeRefreshLayout
вид кеширует касание, мешает HorizontalScrollView
от получения и начинает прокручивать вертикально, чтобы выполнить обновление.
Я пытался решить эту проблему, как я ранее решил проблемы с вертикальной ScrollView
с ViewPager
внутри, используя requestDisallowInterceptTouchEvent
но это не сработало. Я также заметил, что этот метод переопределен в оригинале SwipeRefreshLayout
класс без возврата супер. Разработчик Google оставил комментарий "//Nope.
:)
Так как SwipeRefreshLayout
Компонент является относительно новым, я не смог найти решение, которое устраняет проблему горизонтальной прокрутки, в то же время позволяя свайпу обновлять вид для отслеживания и обработки вертикальной прокрутки, поэтому я решил поделиться своим решением с надеждой, что он сэкономит кому-то час или два.
4 ответа
Я решил это путем расширения SwipeRefreshLayout
и переопределив его onInterceptTouchEvent
, Внутри я вычисляю, больше ли пройденное пользователем X расстояние, чем сенсорная поверхность. Если это так, это означает, что пользователь проводит по горизонтали, поэтому я возвращаюсь false
который позволяет ребенку видеть (HorizontalScrollView
в этом случае), чтобы получить сенсорное событие.
public class CustomSwipeToRefresh extends SwipeRefreshLayout {
private int mTouchSlop;
private float mPrevX;
public CustomSwipeToRefresh(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPrevX = MotionEvent.obtain(event).getX();
break;
case MotionEvent.ACTION_MOVE:
final float eventX = event.getX();
float xDiff = Math.abs(eventX - mPrevX);
if (xDiff > mTouchSlop) {
return false;
}
}
return super.onInterceptTouchEvent(event);
}
}
Если вы не запомните тот факт, что вы уже отклонили событие ACTION_MOVE, вы, в конце концов, примете его позже, если пользователь вернется к вашему первоначальному mPrevX.
Просто добавьте логическое значение, чтобы запомнить его.
public class CustomSwipeToRefresh extends SwipeRefreshLayout {
private int mTouchSlop;
private float mPrevX;
// Indicate if we've already declined the move event
private boolean mDeclined;
public CustomSwipeToRefresh(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPrevX = MotionEvent.obtain(event).getX();
mDeclined = false; // New action
break;
case MotionEvent.ACTION_MOVE:
final float eventX = event.getX();
float xDiff = Math.abs(eventX - mPrevX);
if (mDeclined || xDiff > mTouchSlop) {
mDeclined = true; // Memorize
return false;
}
}
return super.onInterceptTouchEvent(event);
}
}
Решение, предложенное Lior Iluz с переопределением onInterceptTouchEvent(), имеет серьезную проблему. Если контейнер с прокруткой содержимого не полностью прокручивается, то может быть невозможно активировать пролистывание для обновления в том же жесте прокрутки вверх. Действительно, когда вы начинаете прокручивать внутренний контейнер и непреднамеренно перемещаете палец по горизонтали больше, чем mTouchSlop (по умолчанию 8dp), предлагаемый CustomSwipeToRefresh отклоняет этот жест. Таким образом, пользователь должен попробовать еще раз, чтобы начать обновление. Это может выглядеть странно для пользователя.
Я извлек исходный код оригинального SwipeRefreshLayout из библиотеки поддержки в свой проект и переписал onInterceptTouchEvent(). Новое имя класса TouchSafeSwipeRefreshLayout
private boolean mPendingActionDown;
private float mInitialDownY;
private float mInitialDownX;
private boolean mGestureDeclined;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = ev.getActionMasked();
int pointerIndex;
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (!isEnabled() || mReturningToStart || mRefreshing ) {
// Fail fast if we're not in a state where a swipe is possible
if (D) Log.e(LOG_TAG, "Fail because of not enabled OR refreshing OR returning to start. "+motionEventToShortText(ev));
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop());
mActivePointerId = ev.getPointerId(0);
if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) >= 0) {
if (mNestedScrollInProgress || canChildScrollUp()) {
if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. Set pending DOWN=true. "+motionEventToShortText(ev));
mPendingActionDown = true;
} else {
mInitialDownX = ev.getX(pointerIndex);
mInitialDownY = ev.getY(pointerIndex);
}
}
return false;
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
if (D) Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return false;
} else if (mGestureDeclined) {
if (D) Log.e(LOG_TAG, "Gesture was declined previously because of horizontal swipe");
return false;
} else if ((pointerIndex = ev.findPointerIndex(mActivePointerId)) < 0) {
return false;
} else if (mNestedScrollInProgress || canChildScrollUp()) {
if (D) Log.e(LOG_TAG, "Fail because of nested content is Scrolling. "+motionEventToShortText(ev));
return false;
} else if (mPendingActionDown) {
// This is the 1-st Move after content stops scrolling.
// Consider this Move as Down (a start of new gesture)
if (D) Log.e(LOG_TAG, "Consider this move as down - setup initial X/Y."+motionEventToShortText(ev));
mPendingActionDown = false;
mInitialDownX = ev.getX(pointerIndex);
mInitialDownY = ev.getY(pointerIndex);
return false;
} else if (Math.abs(ev.getX(pointerIndex) - mInitialDownX) > mTouchSlop) {
mGestureDeclined = true;
if (D) Log.e(LOG_TAG, "Decline gesture because of horizontal swipe");
return false;
}
final float y = ev.getY(pointerIndex);
startDragging(y);
if (!mIsBeingDragged) {
if (D) Log.d(LOG_TAG, "Waiting for dY to start dragging. "+motionEventToShortText(ev));
} else {
if (D) Log.d(LOG_TAG, "Dragging started! "+motionEventToShortText(ev));
}
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mGestureDeclined = false;
mPendingActionDown = false;
mActivePointerId = INVALID_POINTER;
break;
}
return mIsBeingDragged;
}
Смотрите мой пример проекта на Github.
Если вы используете Tim Roes EnhancedListView
Смотрите это вопросы. Я был очень полезен для меня, потому что они добавляют функцию, которая определяет, когда начинается и когда заканчивается удар.
Когда начинается смахивание, я отключаю SwipeRefreshLayout, а когда смахивание завершается, я активирую swipeRefreshLayout.
Вот что я сделал:
class HorizontalScrollViewWithDragListener
@JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : HorizontalScrollView(context, attrs) {
var draggingState: Boolean = false
set(value) {
if (field != value) {
field = value
listener?.invoke(value)
}
}
var listener: ((draggingState: Boolean) -> Unit)? = null
override fun onInterceptTouchEvent(ev: MotionEvent) =
super.onInterceptTouchEvent(ev)
.also { draggingState = it }
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent) =
super.onTouchEvent(ev)
.also {
if(ev.action == MotionEvent.ACTION_UP) {
draggingState = false
}
}
}
Затем я просто делаю это в установочном коде:
myScrollView.listener = { refreshView.isEnabled = !it }