Android ListView - остановить прокрутку в позиции "целое"

Извините за запутанное название, я не могу выразить проблему очень кратко...

У меня есть приложение Android со списком ListView, в котором используется круговой/ "бесконечный" адаптер, что в основном означает, что я могу прокручивать его вверх или вниз столько, сколько захочу, и элементы будут обтекать, когда он достигнет вершины или снизу, заставляя его казаться пользователю, как будто он вращает бесконечно длинный список (~ 100) повторяющихся предметов.

Точкой этой настройки является выбор пользователем случайного элемента, просто путем вращения/отображения списка и ожидания, чтобы увидеть, где он останавливается. Я уменьшил трение Listview, поэтому он немного ускоряется и дольше, и это, похоже, работает очень хорошо. Наконец, я разместил частично прозрачное изображение поверх ListView, чтобы заблокировать верхний и нижний элементы (с переходом от прозрачного к черному), заставляя его выглядеть так, как будто пользователь "выбирает" элемент посередине, как если бы они были на вращающемся "колесе", которым они управляли, бросая.

Существует одна очевидная проблема: после того, как вы выбрали ListView, он не останавливается на определенном элементе, но он может перестать вращаться между двумя элементами (где первый видимый элемент пока только частично показан). Я хочу избежать этого, потому что в этом случае не очевидно, какой элемент был "случайно выбран".

Короче говоря: после того, как ListView завершил прокрутку после перехода, я хочу, чтобы он остановился на "цельной" строке, а не на частично видимой строке.

В настоящее время я реализовал это поведение, проверив, когда прокрутка остановилась, а затем выберите первый видимый элемент как таковой:

    lv = this.getListView();

    lv.setFriction(0.005f);
    lv.setOnScrollListener(new OnScrollListener() {
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}

        public void onScrollStateChanged(AbsListView view, int scrollState) {
            if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) 
            {
                if (isAutoScrolling) return;

                isAutoScrolling = true;
                int pos = lv.getFirstVisiblePosition();
                lv.setSelection(pos);
                isAutoScrolling = false;
            }
        }
    });

Это работает достаточно хорошо, кроме одной очевидной проблемы... Первый видимый элемент может быть видимым только для пикселя или двух. В этом случае я хочу, чтобы ListView прыгал "вверх" для этих двух пикселей так, чтобы был выбран второй видимый элемент. Вместо этого, конечно, выбирается первый видимый элемент, который означает, что ListView прыгает "вниз" почти целую строку (минус эти два пикселя).

Короче говоря, вместо перехода к первому видимому элементу я хочу, чтобы он переместился к предмету, который больше всего виден. Если первый видимый элемент меньше половины видимого, я хочу, чтобы он переместился ко второму видимому элементу.

Вот иллюстрация, которая, надеюсь, передает мою мысль. Левый список ListView (каждой пары) показывает состояние после прекращения перехода (где он останавливается), а правый ListView показывает, как он выглядит после того, как он совершил "прыжок", выбрав первый видимый элемент. Слева я показываю текущую (неправильную) ситуацию: элемент B едва заметен, но он все еще является первым видимым элементом, поэтому listView перескакивает, чтобы выбрать этот элемент, что не является логичным, поскольку оно должно прокручивать почти всю высоту элемента попасть туда. Было бы гораздо логичнее прокручивать элемент C (который изображен справа), потому что это "ближе".

Изображение http://nickthissen.nl/Images/lv.jpg

Как я могу достичь такого поведения? Единственный способ, о котором я могу думать, - это как-то измерить, какая часть видимого элемента видима. Если это более 50%, я перехожу к этой позиции. Если он меньше 50%, я перехожу к этой позиции + 1. Однако я не знаю, как измерить это...

Есть идеи?

Ответ 1

Вы можете получить видимые размеры дочернего элемента с помощью метода getChildVisibleRect. Когда у вас это есть, и вы получите общую высоту ребенка, вы можете прокрутить до соответствующего ребенка.

В приведенном ниже примере я проверяю, видна ли хотя бы половина дочернего элемента:

View child = lv.getChildAt (0);    // first visible child
Rect r = new Rect (0, 0, child.getWidth(), child.getHeight());     // set this initially, as required by the docs
double height = child.getHeight () * 1.0;

lv.getChildVisibleRect (child, r, null);

if (Math.abs (r.height ()) < height / 2.0) {
    // show next child
}

else {
    // show this child
}

Ответ 2

Следуйте этим 3 шагам, тогда вы можете получить именно то, что хотите!!!!

1.Инициализация двух переменных для прокрутки вверх и вниз:

int scrollingUp=0,scrollingDown=0;

2. Затем увеличьте значение переменной на основе прокрутки:

@Override
public void onScroll(AbsListView view, int firstVisibleItem,
        int visibleItemCount, int totalItemCount) {

        if(mLastFirstVisibleItem<firstVisibleItem)
                {
                    scrollingDown=1;
                }
                if(mLastFirstVisibleItem>firstVisibleItem)
                {
                    scrollingUp=1;
                }
                mLastFirstVisibleItem=firstVisibleItem;
    } 

3. Затем выполняются изменения в onScrollStateChanged():

@Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            switch (scrollState) {
            case SCROLL_STATE_IDLE:

                if(scrollingUp==1)
                {
                    mainListView.post(new Runnable() {
                        public void run() {
                            View child = mainListView.getChildAt (0);    // first visible child
                            Rect r = new Rect (0, 0, child.getWidth(), child.getHeight());     // set this initially, as required by the docs
                            double height = child.getHeight () * 1.0;
                            mainListView.getChildVisibleRect (child, r, null);
                            int dpDistance=Math.abs (r.height());
                            double minusDistance=dpDistance-height;
                            if (Math.abs (r.height()) < height/2)
                            {
                                mainListView.smoothScrollBy(dpDistance, 1500);
                            }       
                            else
                            {
                                mainListView.smoothScrollBy((int)minusDistance, 1500);
                            }
                            scrollingUp=0;

                        }
                    });
                }
                if(scrollingDown==1)
                {
                    mainListView.post(new Runnable() {
                        public void run() {

                            View child = mainListView.getChildAt (0);    // first visible child
                            Rect r = new Rect (0, 0, child.getWidth(), child.getHeight());     // set this initially, as required by the docs
                            double height = child.getHeight () * 1.0;
                            mainListView.getChildVisibleRect (child, r, null);
                            int dpDistance=Math.abs (r.height());
                            double minusDistance=dpDistance-height;
                            if (Math.abs (r.height()) < height/2)
                            {
                                mainListView.smoothScrollBy(dpDistance, 1500);
                            }       
                            else
                            {
                                mainListView.smoothScrollBy((int)minusDistance, 1500);
                            }
                            scrollingDown=0;
                        } 
                    });
                }
            break;
            case SCROLL_STATE_TOUCH_SCROLL:

                break;
                }
            }

Ответ 3

Вот мой последний код, вдохновленный ответом Тени.

Я забыл сначала добавить "if(Math.abs(r.height())!=height)". Затем он просто прокручивается дважды после того, как он прокручивает до правильного положения, потому что он всегда больше высоты /2 childView. Надеюсь, что это поможет.

listView.setOnScrollListener(new AbsListView.OnScrollListener(){

            @Override
            public void onScrollStateChanged(AbsListView view,int scrollState) {
                if (scrollState == SCROLL_STATE_IDLE){
                    View child = listView.getChildAt (0);    // first visible child
                    Rect r = new Rect (0, 0, child.getWidth(), child.getHeight());     // set this initially, as required by the docs
                    double height = child.getHeight () * 1.0;
                    listView.getChildVisibleRect (child, r, null);
                    if(Math.abs(r.height())!=height){//only smooth scroll when not scroll to correct position
                        if (Math.abs (r.height ()) < height / 2.0) {
                            listView.smoothScrollToPosition(listView.getLastVisiblePosition());
                        }
                        else if(Math.abs (r.height ()) > height / 2.0){
                            listView.smoothScrollToPosition(listView.getFirstVisiblePosition());
                        }
                        else{
                            listView.smoothScrollToPosition(listView.getFirstVisiblePosition());
                        }

                    }
                }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount) {

            }});

Ответ 4

Если вы можете получить позицию строки, которую нужно прокрутить, вы можете использовать метод:

smoothScrollToPosition

Так что-то вроде:

int pos = lv.getFirstVisiblePosition();
lv.smoothScrollToPosition(pos);

Edit
Попробуйте это, извините, у меня нет времени на тестирование, я ухожу.

ImageView iv = //Code to find the image view
Rect rect = new Rect(iv.getLeft(), iv.getTop(), iv.getRight(), iv.getBottom());
lv.requestChildRectangleOnScreen(lv, rect, false);

Ответ 5

Как только вы узнаете первую видимую позицию, вы сможете использовать View.getLocationinWindow() или View.getLocationOnScreen() в представлении позиции next, чтобы получить видимую высоту первой. Сравните это с высотой View и прокрутите страницу до следующей позиции.

Вам может потребоваться настроить его для учета заполнения, в зависимости от того, как выглядят ваши ряды.


Я не пробовал выше, но похоже, что он должен работать. Если это не так, здесь другая, возможно, менее надежная идея:

getLastVisiblePosition(). Если вы берете разницу между last и first, вы можете видеть, сколько позиций видно на экране. Сравните это с тем, сколько позиций было видно, когда список был сначала заполнен (позиция прокрутки 0).

Если отображается одинаковое количество позиций, просто прокрутите до первой видимой позиции, как вы делаете. Если есть еще один видимый, прокрутите до положения "первая + 1".

Ответ 6

Вероятно, вы решили эту проблему, но я думаю, что это решение должно работать

if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
    View firstChild = lv.getChildAt(0);
    int pos = lv.getFirstVisiblePosition();

    //if first visible item is higher than the half of its height
    if (-firstChild.getTop() > firstChild.getHeight()/2) {
        pos++;
    }

    lv.setSelection(pos);
}

getTop() для просмотра первого элемента всегда возвращает неположительное значение, поэтому я не использую Math.abs(firstChild.getTop()), а просто -firstChild.getTop(). Даже если это значение будет > 0, это условие все еще работает.

Если вы хотите сделать эту гладкость, вы можете попробовать использовать lv.smoothScrollToPosition(pos) и заключить все выше фрагменты кода в

if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
    post(new Runnable() {
        @Override
        public void run() {
            //put above code here
            //and change lv.setSelection(pos) to lv.smoothScrollToPosition(pos)
        }
    });
}

Ответ 7

Мое решение:

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)

{

    if (swipeLayout.isRefreshing()) {
        swipeLayout.setRefreshing(false);
    } else {
        int pos = firstVisibleItem;
        if (pos == 0 && lv_post_list.getAdapter().getCount()>0) {

            int topOfNext = lv_post_list.getChildAt(pos + 1).getTop();
            int heightOfFirst = lv_post_list.getChildAt(pos).getHeight();
            if (topOfNext > heightOfFirst) {
                swipeLayout.setEnabled(true);
            } else {
                swipeLayout.setEnabled(false);
            }
        }
        else
            swipeLayout.setEnabled(false);
    }
}