【问题标题】:Android ListView - stop scrolling at 'whole' row positionAndroid ListView - 在“整个”行位置停止滚动
【发布时间】:2013-03-11 12:24:49
【问题描述】:

抱歉标题混乱,我无法非常简洁地表达问题......

我有一个带有 ListView 的 Android 应用程序,它使用圆形/“无限”适配器,这基本上意味着我可以根据需要向上或向下滚动它,当它到达顶部或底部时,项目将环绕,让用户觉得他好像在旋转一个无限长的(~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 为这两个像素“向上”跳转,以便选择 second 可见项。相反,当然,第一个可见项被选中,这意味着 ListView 几乎“向下”跳了一整行(减去这两个像素)。

简而言之,我希望它跳转到最可见的项目,而不是跳转到第一个可见项目。如果第一个可见项目不到一半可见,我希望它跳转到第二个可见项目。

这里有一个插图,希望能传达我的观点。最左边的 ListView (每对)显示了投掷停止后的状态(停止的地方),右边的 ListView 显示了它通过选择第一个可见项目进行“跳跃”后的样子。在左侧,我显示了当前(错误)的情况:项目 B 几乎不可见,但它仍然是第一个可见项目,因此 listView 跳转以选择该项目 - 这是不合逻辑的,因为它必须滚动几乎整个项目高度到那里。滚动到项目 C(如右图所示)会更合乎逻辑,因为它“更接近”。


(来源:nickthissen.nl

我怎样才能实现这种行为?我能想到的唯一方法是以某种方式测量第一个可见项目有多少可见。如果超过 50%,那么我跳到那个位置。如果小于 50%,我会跳到那个位置 + 1。但是我不知道如何测量它......

有什么想法吗?

【问题讨论】:

    标签: android listview scroll


    【解决方案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
    }
    

    【讨论】:

    • 谢谢,这似乎是我一直在寻找的。我一直在内部某个地方得到一个空引用(我已经尝试用 'new Point(0, 0)' 替换你的 'null' 但仍然发生)。我会看看我是否可以调试它(由于某种原因,我的应用程序中的调试无法正常工作.....)
    • 问题是 'child' 为空。似乎 getChildAt 没有返回任何东西。我首先认为这是由于我使用了 CircularArrayAdapter(如this answer)引起的。 getFirstVisiblePosition 返回的位置接近 Integer (1073741756) 的最大值的一半。我想也许我应该将该值“解包”回该项目的真实索引,即 116(1073741756 % itemcount = 116),但使用该“虚拟”索引它也返回空值......我做错了什么?跨度>
    • 这与当前问题完全无关。我还没有真正使用过您提到的适配器,无法进一步帮助您。你应该问另一个问题。
    • 问题第一句提到了他对圆形适配器的使用。
    • 其实矩形应该是这样的:Rect r = new Rect(0, 0, child.getWidth(), child.getHeight());,参考stackoverflow.com/a/17544173/657487答案
    【解决方案2】:

    这是受 Shade 回答启发的最终代码。

    一开始我忘了加"if(Math.abs(r.height())!=height)"。然后它在滚动到正确位置后只滚动两次,因为它总是大于 childView 的 height/2。 希望对您有所帮助。

    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) {
    
                }});
    

    【讨论】:

      【解决方案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;
                      }
                  }
      

      【讨论】:

      • 您的解决方案似乎运行良好,但并非总是如此,我的意思是,有时它不会计算平滑滚动,有时是的.. 但这是我找到的最佳解决方案!
      【解决方案4】:

      你可能解决了这个问题,但我认为这个解决方案应该可以工作

      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)
              }
          });
      }
      

      【讨论】:

        【解决方案5】:

        一旦你知道了第一个可见位置,你应该可以在下一个位置的视图上使用View.getLocationinWindow()View.getLocationOnScreen()来获得第一个的可见高度>。将其与View 的高度进行比较,并在合适的情况下滚动到下一个位置。

        您可能需要调整它以考虑填充,具体取决于您的行的样子。


        我没有尝试过上述方法,但它似乎应该可以工作。如果没有,这是另一个可能不太可靠的想法:

        getLastVisiblePosition()。如果你拿lastfirst 之间的区别,你可以看到屏幕上有多少个位置是可见的。将其与首次填充列表时可见的位置数(滚动位置 0)进行比较。

        如果相同数量的位置可见,只需滚动到第一个可见位置即可。如果还有一个可见,请滚动到“first + 1”位置。

        【讨论】:

        • 我刚刚尝试了您的第二个建议,但它似乎不起作用。它现在总是滚动到“first + 1”。我想你是假设在 ListView 的高度上总是有整数个项目(这样一开始就没有部分项目可见)?我想我可以不用勉强,接下来我会试试的。
        • 是的,这就是为什么我说它可能不太健壮。这实际上取决于列表视图的布局方式。顺便说一句,我赞成 Shade 的回答,因为我认为这可能对你更有效,假设你可以解决你的 null 问题。
        【解决方案6】:

        如果可以得到需要滚动到的行的位置,可以使用方法:

        smoothScrollToPosition

        比如:

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

        编辑
        试试这个,抱歉我没有时间测试,我出去了。

        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);
        

        【讨论】:

        • 关键是要获得要滚动到的行的位置...当我有它时,我可以使用 smoothScroll 或只是 setSelection 让它去那里。我尝试了smoothScroll,它在某些设备上表现得很奇怪(滚动后它开始“向上”移动,基本上每秒选择下一个项目)。所以我更喜欢 setPosition 实际上的跳转,至少这是有效的。正如我所说:我现在需要弄清楚要跳转到哪个项目。
        • 用可能的解决方案更新了我的答案。请返回结果,我还没有测试这个。
        【解决方案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);
            }
        }
        

        【讨论】:

        • 你能edit你的答案包含一个简短的描述吗?
        猜你喜欢
        • 1970-01-01
        • 2012-10-31
        • 1970-01-01
        • 1970-01-01
        • 2011-09-16
        • 2011-08-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多