4

For demonstration, I have a ListView displaying a list of numbers. I would like to achieve the effect that when the user scrolls the ListView and the scrolling ends, it will only stop at certain positions so that the first visible item is always shown completely. I've attached my attempted code below. It works when users drag to scroll the ListView. But when there's fling, the normal acceleration is interrupted, causing an unnatural stop. My question is, how can I take acceleration caused by fling into account while achieving the same effect?

package com.example.snaptest;

import android.content.Context;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;


public class MainActivity extends ActionBarActivity {

    private static class TestListViewAdapter extends BaseAdapter {

        private Context mContext;

        public TestListViewAdapter(Context context) {
            mContext = context;
        }

        @Override
        public int getCount() {
            return 100;
        }

        @Override
        public Object getItem(int position) {
            return Integer.toString(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            TextView textView = new TextView(mContext);
            textView.setText(Integer.toString(position));
            AbsListView.LayoutParams params = new AbsListView.LayoutParams(ViewGroup.LayoutParams
                    .MATCH_PARENT, 180);
            textView.setLayoutParams(params);
            return textView;
        }
    }

    private static class TestListView extends ListView {

        public TestListView(Context context) {
            super(context);
        }

        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
                View itemView = getChildAt(0);
                int top = Math.abs(itemView.getTop()); // top is a negative value
                int bottom = Math.abs(itemView.getBottom());
                if (top >= bottom){
                    smoothScrollToPositionFromTop
                            (getFirstVisiblePosition() + 1, 0);
                } else {
                    smoothScrollToPositionFromTop
                            (getFirstVisiblePosition(), 0);
                }
            }
            return super.onTouchEvent(ev);
        }

    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TestListView listView = new TestListView(this);
        listView.setAdapter(new TestListViewAdapter(this));
        setContentView(listView);
    }

}
dementrock
  • 917
  • 3
  • 9
  • 21

1 Answers1

1

I would try with an OnScrollListener rather than extending ListView.

Something like this:

listView.setOnScrollListener(new AbsListView.OnScrollListener() {
  @Override
  public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (scrollState == AbsListView.SCROLL_STATE_IDLE) {
      // snap the listview according to the top/bottom items' visibility
    }
  }

  @Override
  public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
  }
});
Sebastiano
  • 12,289
  • 6
  • 47
  • 80
  • Thanks for the suggestion! I agree that this is better than the solution I provided in the question. But the animation will still be not smooth because the snapping will happen after the ListView already decelerates to zero speed, instead of naturally stopping at the desired position while decelerating. (This is easily achievable in iOS, in comparison, by overriding the willEndDragging method). – dementrock Mar 15 '15 at 17:34
  • 1
    I see. I would try calculating the scrolling speed and, when this speed lowers to a fixed threshold, call the `smoothScrollToPosition()` method. As for the speed, this looks like it: http://stackoverflow.com/a/10755297/1367571. But I honestly don't know if this approach can work. – Sebastiano Mar 18 '15 at 17:44