Android. Прокрутка 2 списка вместе
OK. То, что я пытаюсь достичь, - это макет, который делает тот же эффект, что и замороженные панели в Excel. То есть я хочу, чтобы строка заголовка прокручивалась горизонтально с помощью главного ListView и левой ListView List, которая прокручивается по вертикали с помощью основного ListView. Строка заголовка и список слева должны оставаться неподвижными при прокрутке в другом измерении.
Вот макет xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recordViewLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<LinearLayout android:layout_width="160dp"
android:layout_height="match_parent"
android:orientation="vertical">
<CheckBox
android:id="@+id/checkBoxTop"
android:text="Check All"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ListView android:id="@+id/engNameList"
android:layout_width="160dp"
android:layout_height="wrap_content"/>
</LinearLayout>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout android:id="@+id/scroll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/record_view_line" android:id="@+id/titleLine" />
<ListView
android:id="@android:id/list"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>
Затем я использую этот код в ListActivity
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
View v = recordsListView.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
((ListView)findViewById(R.id.engNameList)).setSelectionFromTop(firstVisibleItem, top);
}
Это должно привести к тому, что левый список ListView будет прокручиваться, когда пользователь прокручивает правую руку. К сожалению, это не так.
У меня было немного о Google, и кажется, что функция setSelectionFromTop() не будет работать в ListView, который вложен внутри более чем одного макета.
Если это так, может кто-нибудь предложить способ заставить их прокручиваться вместе или по-другому настроить макет или другую технику вообще.
Ответы
Ответ 1
OK. У меня есть ответ. Проблема в том, что .setSelectionFromTop() будет работать, только если listview находится в верхнем макете (т.е. Не вложен). После некоторой царапины на голове я понял, что смогу сделать свой макет RelativeLayout и получить тот же вид, но без необходимости размещения макетов для флажка и списка. Это новый макет:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recordViewLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<CheckBox android:id="@+id/checkBoxTop"
android:text="Check All"
android:layout_width="160dp"
android:layout_height="wrap_content"/>"
<ListView android:id="@+id/engNameList"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:layout_below="@+id/checkBoxTop"/>
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/checkBoxTop">
<LinearLayout android:id="@+id/scroll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/record_view_line" android:id="@+id/titleLine" />
<ListView
android:id="@android:id/list"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
</LinearLayout>
</HorizontalScrollView>
</RelativeLayout>
В основном это код, который идет с макетом.
В onCreate()
engListView=getListView();
engListView.setOnTouchListener(this);
recordListView=(ListView)findViewById(R.id.recordList);
recordListView.setOnScrollListener(this);
и методы слушателя:
public boolean onTouch(View arg0, MotionEvent event) {
recordListView.dispatchTouchEvent(event);
return false;
}
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
View v=view.getChildAt(0);
if(v != null)
engListView.setSelectionFromTop(firstVisibleItem, v.getTop());
}
Ответ 2
Rewrite
Мне не повезло с передачей действий прокрутки в один ListView другому. Поэтому я выбрал другой метод: передав MotionEvent
. Это позволяет каждому ListView
вычислять свои собственные плавные свитки, быстрые прокрутки или что-то еще.
Во-первых, нам понадобятся некоторые переменные класса:
ListView listView;
ListView listView2;
View clickSource;
View touchSource;
int offset = 0;
Каждый метод, который я добавляю к ListView
, будет почти идентичным для listView2
, единственное отличие состоит в том, что listView2
будет ссылаться на ListView
(а не на себя). Я не включил повторяющийся код listView2
.
Во-вторых, начнем с OnTouchListener:
listView = (ListView) findViewById(R.id.engNameList);
listView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if(touchSource == null)
touchSource = v;
if(v == touchSource) {
listView2.dispatchTouchEvent(event);
if(event.getAction() == MotionEvent.ACTION_UP) {
clickSource = v;
touchSource = null;
}
}
return false;
}
});
Чтобы предотвратить циклическую логику: ListView
вызывает listView2
вызывает вызовы ListView
... Я использовал переменную класса touchSource
, чтобы определить, когда нужно передать MotionEvent
. Я предположил, что вы не хотите, чтобы строка нажала кнопку ListView
, чтобы также щелкнуть по listView2
, поэтому я использовал другую переменную класса clickSource
, чтобы предотвратить это.
В-третьих, OnItemClickListener:
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if(parent == clickSource) {
// Do something with the ListView was clicked
}
}
});
В-четвертых,, проходящее каждое событие касания, не идеально, потому что появляются случайные расхождения. OnScrollListener идеально подходит для устранения этих проблем:
listView.setOnScrollListener(new OnScrollListener() {
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if(view == clickSource)
listView2.setSelectionFromTop(firstVisibleItem, view.getChildAt(0).getTop() + offset);
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
});
(необязательно) Наконец, вы упомянули, что у вас есть проблемы, так как ListView
и listView2
начинаются с разных высот в вашем макете... Я высоко рекомендую модифицировать ваш макет, чтобы сбалансировать ListViews, но я нашел способ решить эту проблему. Однако это немного сложно.
Вы не можете рассчитать разницу в высоте между двумя макетами до тех пор, пока не будет отображен весь макет, но на этот момент нет обратного вызова... поэтому я использую простой обработчик:
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// Set listView x, y coordinates in loc[0], loc[1]
int[] loc = new int[2];
listView.getLocationInWindow(loc);
// Save listView y and get listView2 coordinates
int firstY = loc[1];
listView2.getLocationInWindow(loc);
offset = firstY - loc[1];
//Log.v("Example", "offset: " + offset + " = " + firstY + " + " + loc[1]);
}
};
Я предполагаю, что полусекундная задержка достаточно длинная, чтобы отобразить макет и запустить таймер в onResume()
:
handler.sendEmptyMessageDelayed(0, 500);
Если вы используете смещение, я хочу быть ясно, что метод listView2
OnScroll вычитает смещение, а не добавляет его:
listView2.setSelectionFromTop(firstVisibleItem, view.getChildAt(0).getTop() - offset);
Надеюсь, что это поможет!
Ответ 3
Этот поток помог мне найти решение для аналогичной проблемы, с которой я боролся некоторое время. Он также основан на перехвате событий касания. В этом случае синхронизация работает для нескольких списков и полностью симметрична.
Основная проблема заключалась в том, чтобы запретить клики элемента списка распространяться в другие списки. Мне нужны клики и длинные клики, отправленные только списком, который первоначально получил событие касания, включая, в частности, обратную связь, когда вы просто касаетесь элемента (перехват события onClick бесполезен, поскольку он слишком поздно в вызывающей иерархии).
Ключом к этому является перехват события касания дважды. Во-первых, начальное событие касания передается другим спискам. Такая же функция обработчика onTouch захватывает их и передает их в GestureDetector. В обратных вызовах GestureDetector
потребляются события статического касания (onDown
и т.д.) (return true
), тогда как жесты движения не являются (return false
), так что они могут быть дополнительно отправлены самим представлением и запускают прокрутки.
Использование setPositionFromTop
внутри onScroll
не работало для меня, потому что это делает поведение прокрутки крайне вялым. Вместо этого OnScroll используется для выравнивания начальных позиций прокрутки, когда в Syncer добавляются новые ListView.
Единственная проблема, которая сохраняется до сих пор, - это поднятая выше s1ni5t3r. Если вы дважды бросаете listView, они все равно будут отключены.
public class ListScrollSyncer
implements AbsListView.OnScrollListener, OnTouchListener, OnGestureListener
{
private GestureDetector gestureDetector;
private Set<ListView> listSet = new HashSet<ListView>();
private ListView currentTouchSource;
private int currentOffset = 0;
private int currentPosition = 0;
public void addList(ListView list)
{
listSet.add(list);
list.setOnTouchListener(this);
list.setSelectionFromTop(currentPosition, currentOffset);
if (gestureDetector == null)
gestureDetector = new GestureDetector(list.getContext(), this);
}
public void removeList(ListView list)
{
listSet.remove(list);
}
public boolean onTouch(View view, MotionEvent event)
{
ListView list = (ListView) view;
if (currentTouchSource != null)
{
list.setOnScrollListener(null);
return gestureDetector.onTouchEvent(event);
}
else
{
list.setOnScrollListener(this);
currentTouchSource = list;
for (ListView list : listSet)
if (list != currentTouchSource)
list.dispatchTouchEvent(event);
currentTouchSource = null;
return false;
}
}
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
if (view.getChildCount() > 0)
{
currentPosition = view.getFirstVisiblePosition();
currentOffset = view.getChildAt(0).getTop();
}
}
public void onScrollStateChanged(AbsListView view, int scrollState) { }
// GestureDetector callbacks
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; }
public boolean onSingleTapUp(MotionEvent e) { return true; }
public boolean onDown(MotionEvent e) { return true; }
public void onLongPress(MotionEvent e) { }
public void onShowPress(MotionEvent e) { }
}
edit: проблема с двойным щелчком может быть решена с помощью переменной состояния.
private boolean scrolling;
public boolean onDown(MotionEvent e) { return !scrolling; }
public void onScrollStateChanged(AbsListView view, int scrollState)
{
scrolling = scrollState != SCROLL_STATE_IDLE;
}
Ответ 4
Я разрешаю его, изменяя ListView на RecyclerView, используя RecyclerView.addOnScrollListener для синхронизации события прокрутки. И нет расхождений, это прекрасно!
private void syncScrollEvent(RecyclerView leftList, RecyclerView rightList) {
leftList.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return rightList.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
}
});
rightList.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return leftList.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
}
});
leftList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (recyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
rightList.scrollBy(dx, dy);
}
}
});
rightList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (recyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
leftList.scrollBy(dx, dy);
}
}
});
}
Ответ 5
Согласно инструкциям Сэма, я разработал простое приложение, показывающее прокрутку двух списков вместе. На самом деле, моя главная цель - создать горизонтальный прокручиваемый расширяемый список
с фиксированными/замороженными столбцами и работая над ним по завершении, скоро поделится им
https://github.com/huseyinDaVinci/Android/tree/master/FrozenListViews
Ответ 6
Попробуйте решение ниже, оно работает в моем случае
leftlist.setOnScrollListener(new SyncedScrollListener(rightlist));
rightlist.setOnScrollListener(new SyncedScrollListener(leftlist));
SyncedScrollListener.java
package com.xorbix.util;
import android.view.View;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
public class SyncedScrollListener implements OnScrollListener{
int offset;
int oldVisibleItem = -1;
int currentHeight;
int prevHeight;
private View mSyncedView;
public SyncedScrollListener(View syncedView){
if(syncedView == null){
throw new IllegalArgumentException("syncedView is null");
}
mSyncedView = syncedView;
}
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
int[] location = new int[2];
if(visibleItemCount == 0){
return;
}
if(oldVisibleItem != firstVisibleItem){
if(oldVisibleItem < firstVisibleItem){
prevHeight = currentHeight;
currentHeight = view.getChildAt(0).getHeight();
offset += prevHeight;
}else{
currentHeight = view.getChildAt(0).getHeight();
View prevView;
if((prevView = view.getChildAt(firstVisibleItem - 1)) != null){
prevHeight = prevView.getHeight();
}else{
prevHeight = 0;
}
offset -= currentHeight;
}
oldVisibleItem = firstVisibleItem;
}
view.getLocationOnScreen(location);
int listContainerPosition = location[1];
view.getChildAt(0).getLocationOnScreen(location);
int currentLocation = location[1];
int blah = listContainerPosition - currentLocation + offset;
mSyncedView.scrollTo(0, blah);
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
}
}