├── .gitignore ├── LICENSE ├── README.md └── src └── com └── larphoid └── overscrolllistview └── OverscrollListview.java /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Overscroll-ListView 2 | Copyright (C) 2013 Larphoid Apps. 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | android-Overscroll-ListView 2 | =========================== 3 | 4 | An 'Overscrollable' ListView with 'Bounce' effect for android ! 5 | 6 | Usage: 7 | ====== 8 | The bounce effect can be turned on / off with listview.setBounce(true / false). 9 | 10 | The animation delay can be set via the delay variable, which is in milliseconds. Default = 10. The higher the value, the slower the animation. 11 | 12 | The bounce length can be set with listview.setElasticity(float), see source file for help. 13 | 14 | The break speed can be set with listview.setBreakspeed(float), see source file for help. 15 | 16 | In xml, instead of creating a ListView, create a com.larphoid.overscrollinglistview.OverscrollListview 17 | 18 | Ofcourse you can put the OverscrollListview.java in your current package and change com.larphoid.overscrollinglistview to your current package name. 19 | 20 | When assigning the listview to a variable somewhere in your app, ofcourse you have to make it a OverscrollListview. 21 | 22 | IMPORTANT: whenever you populate your listview, when you'r finished populating, don't forget to call listview.initializeValues(). 23 | 24 | CHANGES: 25 | after populating the listview it is not necesary anymore to set the first visible item to something other than the header or footer view (for example like this: listview.setSelectionFromTop(listview.nHeaders, listview.divHeight)), this is now done in initializeValues(). 26 | 27 | Thats it, enjoy ! 28 | 29 | Example: 30 | ======== 31 | The following is an example of how to include Overscrollable in your project. 32 | ```Java 33 | package com.example.tutorial; 34 | 35 | 36 | import android.os.Bundle; 37 | import android.app.Activity; 38 | import android.widget.ArrayAdapter; 39 | import com.larphoid.overscrolllistview.OverscrollListview; 40 | 41 | public class MainActivity extends Activity { 42 | 43 | @Override 44 | protected void onCreate(Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | setContentView(R.layout.activity_main); 47 | 48 | OverscrollListview listView = (OverscrollListview)findViewById(R.id.list); 49 | 50 | // Defined Array values to show in ListView 51 | String[] values = new String[] { 52 | "List item 1", 53 | "List item 2", 54 | "List item 3", 55 | "List item 4", 56 | "List item 5", 57 | "List item 6", 58 | "List item 7", 59 | "List item 8" 60 | }; 61 | ArrayAdapter adapter = new ArrayAdapter(this, 62 | android.R.layout.simple_list_item_1, android.R.id.text1, values); 63 | listView.setAdapter(adapter); 64 | } 65 | } 66 | 67 | ``` 68 | 69 | ```XML 70 | 71 | 75 | 76 | 80 | 81 | 82 | 83 | ``` 84 | 85 | Be sure to include the 'com.larphoid.overscrolllistview' package in your Android 86 | Project src file and you're all set. 87 | 88 | 89 | LICENSE 90 | ======= 91 | 92 | This projected is licensed under the terms of the LGPL license. 93 | -------------------------------------------------------------------------------- /src/com/larphoid/overscrolllistview/OverscrollListview.java: -------------------------------------------------------------------------------- 1 | package com.larphoid.overscrolllistview; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.util.AttributeSet; 6 | import android.view.Display; 7 | import android.view.GestureDetector; 8 | import android.view.MotionEvent; 9 | import android.view.View; 10 | import android.view.WindowManager; 11 | import android.view.GestureDetector.OnGestureListener; 12 | import android.widget.AbsListView; 13 | import android.widget.Adapter; 14 | import android.widget.AdapterView; 15 | import android.widget.ListView; 16 | import android.widget.AbsListView.OnScrollListener; 17 | 18 | /** @author Larphoid Apps. */ 19 | public class OverscrollListView extends ListView implements OnScrollListener, View.OnTouchListener, android.widget.AdapterView.OnItemSelectedListener { 20 | 21 | protected static float BREAKSPEED = 4f, ELASTICITY = 0.67f; 22 | 23 | public int nHeaders = 1, nFooters = 1, divHeight = 0, delay = 10; 24 | private int firstVis, visibleCnt, lastVis, totalItems, scrollstate; 25 | private boolean bounce = true, rebound = false, recalcV = false, trackballEvent = false; 26 | private long flingTimestamp; 27 | private float velocity; 28 | private View measure; 29 | private GestureDetector gesture; 30 | private Handler mHandler = new Handler(); 31 | 32 | public OverscrollListView(Context context) { 33 | super(context); 34 | initialize(context); 35 | } 36 | 37 | public OverscrollListView(Context context, AttributeSet attrs) { 38 | super(context, attrs); 39 | initialize(context); 40 | } 41 | 42 | public OverscrollListView(Context context, AttributeSet attrs, int defStyle) { 43 | super(context, attrs, defStyle); 44 | initialize(context); 45 | } 46 | 47 | @Override 48 | public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 49 | firstVis = firstVisibleItem; 50 | visibleCnt = visibleItemCount; 51 | totalItems = totalItemCount; 52 | lastVis = firstVisibleItem + visibleItemCount; 53 | } 54 | 55 | @Override 56 | public void onScrollStateChanged(AbsListView view, int scrollState) { 57 | scrollstate = scrollState; 58 | if ( scrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL ) { 59 | rebound = true; 60 | mHandler.postDelayed(checkListviewTopAndBottom, delay); 61 | } 62 | } 63 | 64 | @Override 65 | public void onItemSelected(AdapterView av, View v, int position, long id) { 66 | rebound = true; 67 | mHandler.postDelayed(checkListviewTopAndBottom, delay); 68 | } 69 | 70 | @Override 71 | public void onNothingSelected(AdapterView av) { 72 | rebound = true; 73 | mHandler.postDelayed(checkListviewTopAndBottom, delay); 74 | } 75 | 76 | @Override 77 | public boolean onTrackballEvent(MotionEvent event) { 78 | trackballEvent = true; 79 | rebound = true; 80 | mHandler.postDelayed(checkListviewTopAndBottom, delay); 81 | return super.onTrackballEvent(event); 82 | } 83 | 84 | @Override 85 | public boolean onTouch(View v, MotionEvent event) { 86 | gesture.onTouchEvent(event); 87 | return false; 88 | } 89 | 90 | private class gestureListener implements OnGestureListener { 91 | @Override 92 | public boolean onDown(MotionEvent e) { 93 | rebound = false; 94 | recalcV = false; 95 | velocity = 0f; 96 | return false; 97 | } 98 | 99 | @Override 100 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 101 | rebound = true; 102 | recalcV = true; 103 | velocity = velocityY / 25f; 104 | flingTimestamp = System.currentTimeMillis(); 105 | return false; 106 | } 107 | 108 | @Override 109 | public void onLongPress(MotionEvent e) { 110 | } 111 | 112 | @Override 113 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 114 | return false; 115 | } 116 | 117 | @Override 118 | public void onShowPress(MotionEvent e) { 119 | } 120 | 121 | @Override 122 | public boolean onSingleTapUp(MotionEvent e) { 123 | rebound = true; 124 | recalcV = false; 125 | velocity = 0f; 126 | return false; 127 | } 128 | }; 129 | 130 | private void initialize(Context context) { 131 | final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); 132 | final View v = new View(context); 133 | v.setMinimumHeight(Math.max(display.getWidth(), display.getHeight())); 134 | addHeaderView(v, null, false); 135 | addFooterView(v, null, false); 136 | 137 | gesture = new GestureDetector(new gestureListener()); 138 | gesture.setIsLongpressEnabled(false); 139 | flingTimestamp = System.currentTimeMillis(); 140 | setHeaderDividersEnabled(false); 141 | setFooterDividersEnabled(false); 142 | setOnTouchListener(this); 143 | setOnScrollListener(this); 144 | setOnItemSelectedListener(this); 145 | } 146 | 147 | /** This should be called after you finish populating the listview ! This includes any calls to {@link Adapter#notifyDataSetChanged()} and obviously every time you re-populate the listview. */ 148 | public void initializeValues() { 149 | nHeaders = getHeaderViewsCount(); 150 | nFooters = getFooterViewsCount(); 151 | divHeight = getDividerHeight(); 152 | firstVis = 0; 153 | visibleCnt = 0; 154 | lastVis = 0; 155 | totalItems = 0; 156 | scrollstate = 0; 157 | rebound = true; 158 | setSelectionFromTop(nHeaders, divHeight); 159 | smoothScrollBy(0, 0); 160 | mHandler.postDelayed(checkListviewTopAndBottom, delay); 161 | } 162 | 163 | /** 164 | * Turns the bouncing animation on or off. 165 | * 166 | * @param bouncing 167 | * {@code true } for bouncing effect (this is also the default), {@code false} to turn it off. 168 | */ 169 | public void setBounce(boolean bouncing) { 170 | bounce = bouncing; 171 | } 172 | 173 | /** 174 | * Sets how fast the animation will be. Higher value means faster animation. Must be >= 1.05. Together with Elasticity <= 0.75 it will not bounce forever. 175 | * 176 | * @param breakspead 177 | * Default is 4.0 178 | */ 179 | public void setBreakspeed(final float breakspeed) { 180 | if ( Math.abs(breakspeed) >= 1.05f ) { 181 | BREAKSPEED = Math.abs(breakspeed); 182 | } 183 | } 184 | 185 | /** 186 | * Sets how much it will keep bouncing. Lower value means less bouncing. Must be <= 0.75. Together with Breakspeed >= 1.05 it will not bounce forever. 187 | * 188 | * @param elasticity 189 | * Default is 0.67 190 | */ 191 | public void setElasticity(final float elasticity) { 192 | if ( Math.abs(elasticity) <= 0.75f ) { 193 | ELASTICITY = Math.abs(elasticity); 194 | } 195 | } 196 | 197 | public Runnable checkListviewTopAndBottom = new Runnable() { 198 | @Override 199 | public void run() { 200 | 201 | mHandler.removeCallbacks(checkListviewTopAndBottom); 202 | 203 | if ( trackballEvent && firstVis < nHeaders && lastVis >= totalItems ) { 204 | trackballEvent = false; 205 | rebound = false; 206 | return; 207 | } 208 | 209 | if ( rebound ) { 210 | 211 | if ( firstVis < nHeaders ) { 212 | 213 | // hack to avoid strange behaviour when there aren't enough items to fill the entire listview 214 | if ( lastVis >= totalItems ) { 215 | smoothScrollBy(0, 0); 216 | rebound = false; 217 | recalcV = false; 218 | velocity = 0f; 219 | } 220 | 221 | if ( recalcV ) { 222 | recalcV = false; 223 | velocity /= (1f + ((System.currentTimeMillis() - flingTimestamp) / 1000f)); 224 | } 225 | if ( firstVis == nHeaders ) { 226 | recalcV = false; 227 | } 228 | if ( visibleCnt > nHeaders ) { 229 | measure = getChildAt(nHeaders); 230 | if ( measure.getTop() + velocity < divHeight ) { 231 | velocity *= -ELASTICITY; 232 | if ( !bounce || Math.abs(velocity) < BREAKSPEED ) { 233 | rebound = false; 234 | recalcV = false; 235 | velocity = 0f; 236 | } else { 237 | setSelectionFromTop(nHeaders, divHeight + 1); 238 | } 239 | } 240 | } else { 241 | if ( velocity > 0f ) velocity = -velocity; 242 | } 243 | if ( rebound ) { 244 | smoothScrollBy((int) -velocity, 0); 245 | if ( velocity > BREAKSPEED ) { 246 | velocity *= ELASTICITY; 247 | if ( velocity < BREAKSPEED ) { 248 | rebound = false; 249 | recalcV = false; 250 | velocity = 0f; 251 | } 252 | } else velocity -= BREAKSPEED; 253 | } 254 | 255 | } else if ( lastVis >= totalItems ) { 256 | 257 | if ( recalcV ) { 258 | recalcV = false; 259 | velocity /= (1f + ((System.currentTimeMillis() - flingTimestamp) / 1000f)); 260 | } 261 | if ( lastVis == totalItems - nHeaders - nFooters ) { 262 | rebound = false; 263 | recalcV = false; 264 | velocity = 0f; 265 | } else { 266 | if ( visibleCnt > (nHeaders + nFooters) ) { 267 | measure = getChildAt(visibleCnt - nHeaders - nFooters); 268 | if ( measure.getBottom() + velocity > getHeight() - divHeight ) { 269 | velocity *= -ELASTICITY; 270 | if ( !bounce || Math.abs(velocity) < BREAKSPEED ) { 271 | rebound = false; 272 | recalcV = false; 273 | velocity = 0f; 274 | } else { 275 | setSelectionFromTop(lastVis - nHeaders - nFooters, getHeight() - divHeight - measure.getHeight() - 1); 276 | } 277 | } 278 | } else { 279 | if ( velocity < 0f ) velocity = -velocity; 280 | } 281 | } 282 | if ( rebound ) { 283 | smoothScrollBy((int) -velocity, 0); 284 | if ( velocity < -BREAKSPEED ) { 285 | velocity *= ELASTICITY; 286 | if ( velocity > -BREAKSPEED / ELASTICITY ) { 287 | rebound = false; 288 | recalcV = false; 289 | velocity = 0f; 290 | } 291 | } else velocity += BREAKSPEED; 292 | } 293 | 294 | } else if ( scrollstate == OnScrollListener.SCROLL_STATE_IDLE ) { 295 | 296 | rebound = false; 297 | recalcV = false; 298 | velocity = 0f; 299 | } 300 | mHandler.postDelayed(checkListviewTopAndBottom, delay); 301 | return; 302 | } 303 | 304 | if ( scrollstate != OnScrollListener.SCROLL_STATE_IDLE ) return; 305 | 306 | if ( totalItems == (nHeaders + nFooters) || firstVis < nHeaders ) { 307 | setSelectionFromTop(nHeaders, divHeight); 308 | smoothScrollBy(0, 0); 309 | } else if ( lastVis == totalItems ) { 310 | int offset = getHeight() - divHeight; 311 | measure = getChildAt(visibleCnt - nHeaders - nFooters); 312 | if ( measure != null ) offset -= measure.getHeight(); 313 | setSelectionFromTop(lastVis - nHeaders - nFooters, offset); 314 | smoothScrollBy(0, 0); 315 | } 316 | } 317 | }; 318 | } 319 | --------------------------------------------------------------------------------