Как синхронизировать прокрутку двух обзоров в Android?
Я пытаюсь создать EPG в Android с помощью Recyclerviews. Требуется фиксированный верхний ряд, который прокручивается по горизонтали, чтобы показать программы, соответствующие времени, и фиксированный крайний левый столбец, который прокручивается по вертикали, чтобы показать различные каналы.
Исходя из этого ответа, я пришел с ниже
<?xml version="1.0" encoding="utf-8"?>
<!--Outer container layout-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".MainActivity">
<!--To display Channels list-->
<LinearLayout
android:layout_width="150dp"
android:layout_height="match_parent"
android:orientation="vertical">
<!--Position (0,0)-->
<TextView
android:id="@+id/tv_change_date"
android:layout_width="match_parent"
android:layout_height="50dp" />
<android.support.v7.widget.RecyclerView
android:id="@+id/rcv_channel_name"
android:scrollbars="none"
android:layout_width="150dp"
android:layout_height="match_parent"/>
</LinearLayout>
<!--To display Time and Programs list-->
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<!--Time horizontal list-->
<android.support.v7.widget.RecyclerView
android:id="@+id/rcv_vertical_header"
android:scrollbars="none"
android:layout_width="match_parent"
android:layout_height="50dp"/>
<!--Vertical list whose each element is horizontal list to show programs-->
<android.support.v7.widget.RecyclerView
android:id="@+id/rcv_vertical"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>
Теперь мне нужно синхронизировать вертикальную прокрутку rcv_vertical и rcv_channel_name. Я реализовал это как в этом проекте GitHub.
public class SelfRemovingOnScrollListener extends RecyclerView.OnScrollListener {
@Override
public final void onScrollStateChanged(@NonNull final RecyclerView recyclerView, final int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
recyclerView.removeOnScrollListener(this);
}
}
}
В MainActivity
private final RecyclerView.OnScrollListener channelScrollListener = new SelfRemovingOnScrollListener() {
@Override
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
programsRecyclerView.scrollBy(dx, dy);
}
};
private final RecyclerView.OnScrollListener programScrollListener = new SelfRemovingOnScrollListener() {
@Override
public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
channelsRecyclerView.scrollBy(dx, dy);
}
};
@Override
protected void onStart() {
super.onStart();
//Sync channel name RCV and Programs RCV scrolling
channelsRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
private int mLastY;
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
Log.d("debug", "LEFT: onInterceptTouchEvent");
final Boolean ret = rv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
if (!ret) {
onTouchEvent(rv, e);
}
return Boolean.FALSE;
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
Log.d("debug", "LEFT: onTouchEvent");
final int action;
if ((action = e.getAction()) == MotionEvent.ACTION_DOWN && programsRecyclerView
.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
mLastY = rv.getScrollY();
Log.d("scroll","channelsRecyclerView Y: "+mLastY);
rv.addOnScrollListener(channelScrollListener);
}
else {
if (action == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) {
rv.removeOnScrollListener(channelScrollListener);
}
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
Log.d("debug", "LEFT: onRequestDisallowInterceptTouchEvent");
}
});
programsRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
private int mLastY;
@Override
public boolean onInterceptTouchEvent(@NonNull final RecyclerView rv, @NonNull final
MotionEvent e) {
Log.d("debug", "RIGHT: onInterceptTouchEvent");
final Boolean ret = rv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
if (!ret) {
onTouchEvent(rv, e);
}
return Boolean.FALSE;
}
@Override
public void onTouchEvent(@NonNull final RecyclerView rv, @NonNull final MotionEvent e) {
Log.d("debug", "RIGHT: onTouchEvent");
final int action;
if ((action = e.getAction()) == MotionEvent.ACTION_DOWN && channelsRecyclerView
.getScrollState
() == RecyclerView.SCROLL_STATE_IDLE) {
mLastY = rv.getScrollY();
rv.addOnScrollListener(programScrollListener);
Log.d("scroll","programsRecyclerView Y: "+mLastY);
}
else {
if (action == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) {
rv.removeOnScrollListener(programScrollListener);
}
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(final boolean disallowIntercept) {
Log.d("debug", "RIGHT: onRequestDisallowInterceptTouchEvent");
}
});
[![}][3]][3]
Это работает нормально, пока я не сделаю горизонтальную прокрутку в HorizontalScrollView. После этого левая утилита просмотра "rcv_channel_name" прокручивается быстрее, чем правая rcv_vertical.
Любая помощь или предложение, чтобы исправить это высоко ценится.
2 ответа
Редакция! Мне наконец удалось заставить это работать! Вот мое краткое решение (см. Также старый ответ для ввода на основе фокуса с помощью d-pad):
Прежде всего вам нужно 3 переработчика (во фрагменте или в активности):
public RecyclerView vertical_recycler, current_focus_recycler, time_recycler;
Затем добавьте
public int scroll_offset;
Эта переменная будет содержать смещение прокрутки в пикселях для дальнейшего использования. Затем правильно заполните ваш вертикальный RecyclerView, убедитесь, что ваш RecyclerView имеет горизонтальные RecyclerViews как дочерние. Также убедитесь, что ваш time_recycler будет длиннее любого из horizontal_recyclers (возвращая Integer.MAX_VALUE в getItemCount в своем адаптере, вы убедитесь, что вы хорошо.. вроде...):
@Override
public int getItemCount() {
return Integer.MAX_VALUE;
}
Затем добавьте прослушиватель прокрутки в Activity или Fragment.
time_recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
horizontal_scroll(dx);
if (dx == 0) {
scroll_offset = 0;
} else {
scroll_offset += dx;
}
}
});
public void horizontal_scroll(int dx) {
for (int i = 0, n = vertical_recycler.getChildCount(); i < n; ++i) {
if (vertical_recycler.getChildAt(i).findViewById(R.id.horizontal_recycler) != current_focus_recycler) {
vertical_recycler.getChildAt(i).findViewById(R.id.horizontal_recycler).scrollBy(dx, 0);
}
}
}
В вертикальном адаптере RecyclerView добавляются другие необходимые функции:
private AdapterSheduleHorizontal adapter_horizontal;
@Override
public void onViewAttachedToWindow(AdapterSheduleVertical.VerticalViewHolder holder){
super.onViewAttachedToWindow(holder);
((LinearLayoutManager) holder.horizontal_recycler.getLayoutManager())
.scrollToPositionWithOffset(0,
-fragmentShedule.scroll_offset);
holder.horizontal_recycler.addOnScrollListener(onScrollListener);
}
@Override
public void onViewDetachedFromWindow(VerticalViewHolder holder) {
super.onViewDetachedFromWindow(holder);
holder.horizontal_recycler.removeOnScrollListener(onScrollListener);
}
RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(recyclerView == fragmentShedule.current_focus_recycler) {
fragmentShedule.time_recycler.scrollBy(dx, 0);
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int state) {
super.onScrollStateChanged(recyclerView, state);
switch (state) {
case RecyclerView.SCROLL_STATE_IDLE:
fragmentShedule.current_focus_recycler = null;
break;
}
}
};
@Override
public void onBindViewHolder(VerticalViewHolder sheduleViewHolder, final int position) {
...
LinearLayoutManager lm = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
sheduleViewHolder.horizontal_recycler.setLayoutManager(lm);
adapter_horizontal = new AdapterSheduleHorizontal(fragmentShedule, context, dayItemList);
sheduleViewHolder.horizontal_recycler.setAdapter(adapter_horizontal);
...
}
Следующий в горизонтальном адаптере RecyclerView
...
RecyclerView parent_recyclerview;
@Override
public void onBindViewHolder(final HorizontalViewHolder sheduleViewHolder, final int i) {
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
lp.width = 500;//some width to
sheduleViewHolder.shedule_item.setLayoutParams(lp);
sheduleViewHolder.itemView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
fragmentShedule.current_focus_recycler = parent_recyclerview;
break;
}
return false;
}
});
sheduleViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context, "click"+i, Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
parent_recyclerview = recyclerView;
}
Я верю, что это все. Пожалуйста, не стесняйтесь комментировать и скажите мне, если это не работает для вас, я дважды проверю свой код (я проверил его для вертикальной и горизонтальной прокрутки всех детей).
СТАРЫЙ ОТВЕТ
Хорошо, я покажу свой подход, который работает для моего приложения, похожего на EPG.
Предположим, у вас есть RecyclerView с дочерними элементами RecyclerView. Родитель прокручивает вертикально, ребенок - горизонтально. Вверху этого макета у вас есть другой RecyclerView с временной шкалой (также с горизонтальной прокруткой).
Вам нужно прокрутить временную шкалу, чтобы прокрутить всех ребят-переработчиков, кроме той, которая получает фокус / касается / прокручивает (если вы не собираетесь использовать возможности сенсорного экрана, вы можете использовать только фокус дочерних элементов горизонтального обзора),
С учетом сказанного вам нужно зарегистрировать, какой дочерний RecyclerView в данный момент прокручивается.
public RecyclerView vertical_recycler, current_focus_recycler, time_recycler;
Также я зарегистрировал метод, который прокручивает все горизонтальные RecyclerViews одновременно:
public void horizontal_scroll(int dx) {
for (int i = 0, n = vertical_recycler.getChildCount(); i < n; ++i) {
if (vertical_recycler.getChildAt(i).findViewById(R.id.horizontal_recycler) != current_focus_recycler) {
vertical_recycler.getChildAt(i).findViewById(R.id.horizontal_recycler).scrollBy(dx, 0);
}
}
}
После установки адаптера для моего time_recycler я добавил слушателя:
time_recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
horizontal_scroll(dx);
}
});
Все идет нормально. Затем в адаптер, который соответствует горизонтальному RecyclerView-child, я добавил:
RecyclerView parent_recyclerview;
RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
((ActivityMain) context).time_recycler.scrollBy(dx, 0);
}
};
//...
@Override
public void onBindViewHolder(final HorizontalViewHolder viewHolder, final int i) {
viewHolder.shedule_item.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (v.hasFocus()) {
((ActivityMain) context).current_focus_recycler = parent_recyclerview;
parent_recyclerview.addOnScrollListener(onScrollListener);
} else {
parent_recyclerview.removeOnScrollListener(onScrollListener);
}
}
});
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
parent_recyclerview = recyclerView;
}
//...
class HorizontalViewHolder extends RecyclerView.ViewHolder {
private LinearLayout shedule_item;
public HorizontalViewHolder(View view) {
super(view);
shedule_item = (LinearLayout) view.findViewById(R.id.shedule_item);
}
}
А расположение горизонтального элемента RecyclerView:
<LinearLayout
android:id="@+id/shedule_item"
.../>
...
</LinearLayout>
Это работает в моем случае, если я использую клавиатуру и перемещаюсь по таблице epg с помощью клавиш со стрелками. С некоторыми корректировками (с некоторой болью в золе) вы сможете перехватывать события касания от родительского горизонтального переработчика до его дочерних элементов. Надеюсь, что это поможет вам.
В продолжение ответа, приведенного выше Романом Т., это исправляет несоответствующие виды рециркулятора в случае бросков (в основном, в случае бросков в противоположных направлениях).
sheduleViewHolder.itemView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if (fragmentShedule.current_focus_recycler != null) {
//stop any scroll due to fling on recyclerview currently in focus
fragmentShedule.current_focus_recycler.stopScroll();
}
fragmentShedule.current_focus_recycler = parent_recyclerview;
break;
}
return false;
}
});
Добавлен метод stopScroll, чтобы отменить любую текущую прокрутку из-за перехода на последний использованный вид рециркулятора
PS: отправляя это как ответ из-за низкой репутации.