├── .gitignore ├── AndroidHorizontalListView ├── .classpath ├── .project ├── AndroidManifest.xml ├── libs │ └── android-support-v4.jar ├── proguard-project.txt ├── project.properties ├── res │ └── values │ │ └── attrs.xml └── src │ └── com │ └── meetme │ └── android │ └── horizontallistview │ └── HorizontalListView.java ├── AndroidHorizontalListViewSample ├── .classpath ├── .project ├── AndroidManifest.xml ├── ic_launcher-web.png ├── libs │ └── android-support-v4.jar ├── proguard-project.txt ├── project.properties ├── res │ ├── drawable-hdpi │ │ └── ic_launcher.png │ ├── drawable-ldpi │ │ └── ic_launcher.png │ ├── drawable-mdpi │ │ └── ic_launcher.png │ ├── drawable-xhdpi │ │ └── ic_launcher.png │ ├── layout │ │ ├── activity_main.xml │ │ └── custom_data_view.xml │ ├── menu │ │ └── activity_main.xml │ ├── values-v11 │ │ └── styles.xml │ ├── values-v14 │ │ └── styles.xml │ └── values │ │ ├── strings.xml │ │ └── styles.xml └── src │ └── com │ └── meetme │ └── android │ └── horizontallistview │ └── sample │ ├── CustomArrayAdapter.java │ ├── CustomData.java │ └── MainActivity.java └── README.md /.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 | -------------------------------------------------------------------------------- /AndroidHorizontalListView/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AndroidHorizontalListView/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | AndroidHorizontalListView 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /AndroidHorizontalListView/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /AndroidHorizontalListView/libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeetMe/Android-HorizontalListView/40cdf31cbe953746b6eda8910c73540c48065667/AndroidHorizontalListView/libs/android-support-v4.jar -------------------------------------------------------------------------------- /AndroidHorizontalListView/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /AndroidHorizontalListView/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-17 15 | android.library=true 16 | -------------------------------------------------------------------------------- /AndroidHorizontalListView/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /AndroidHorizontalListView/src/com/meetme/android/horizontallistview/HorizontalListView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License Copyright (c) 2011 Paul Soucy (paul@dev-smart.com) 3 | * The MIT License Copyright (c) 2013 MeetMe, Inc. 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | * associated documentation files (the "Software"), to deal in the Software without restriction, 7 | * including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all copies or 12 | * substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | */ 20 | 21 | // @formatter:off 22 | /* 23 | * This is based on HorizontalListView.java from: https://github.com/dinocore1/DevsmartLib-Android 24 | * It has been substantially rewritten and added to from the original version. 25 | */ 26 | // @formatter:on 27 | package com.meetme.android.horizontallistview; 28 | 29 | import android.annotation.SuppressLint; 30 | import android.annotation.TargetApi; 31 | import android.content.Context; 32 | import android.content.res.TypedArray; 33 | import android.database.DataSetObserver; 34 | import android.graphics.Canvas; 35 | import android.graphics.Rect; 36 | import android.graphics.drawable.Drawable; 37 | import android.os.Build; 38 | import android.os.Bundle; 39 | import android.os.Parcelable; 40 | import android.support.v4.view.ViewCompat; 41 | import android.support.v4.widget.EdgeEffectCompat; 42 | import android.util.AttributeSet; 43 | import android.view.GestureDetector; 44 | import android.view.HapticFeedbackConstants; 45 | import android.view.MotionEvent; 46 | import android.view.View; 47 | import android.view.ViewGroup; 48 | import android.widget.AdapterView; 49 | import android.widget.ListAdapter; 50 | import android.widget.ListView; 51 | import android.widget.ScrollView; 52 | import android.widget.Scroller; 53 | 54 | import java.util.ArrayList; 55 | import java.util.LinkedList; 56 | import java.util.List; 57 | import java.util.Queue; 58 | 59 | // @formatter:off 60 | /** 61 | * A view that shows items in a horizontally scrolling list. The items 62 | * come from the {@link ListAdapter} associated with this view.
63 | *
64 | * Limitations: 65 | * 71 | *
72 | * Custom XML Parameters Supported:
73 | *
74 | * 81 | */ 82 | // @formatter:on 83 | public class HorizontalListView extends AdapterView { 84 | /** Defines where to insert items into the ViewGroup, as defined in {@code ViewGroup #addViewInLayout(View, int, LayoutParams, boolean)} */ 85 | private static final int INSERT_AT_END_OF_LIST = -1; 86 | private static final int INSERT_AT_START_OF_LIST = 0; 87 | 88 | /** The velocity to use for overscroll absorption */ 89 | private static final float FLING_DEFAULT_ABSORB_VELOCITY = 30f; 90 | 91 | /** The friction amount to use for the fling tracker */ 92 | private static final float FLING_FRICTION = 0.009f; 93 | 94 | /** Used for tracking the state data necessary to restore the HorizontalListView to its previous state after a rotation occurs */ 95 | private static final String BUNDLE_ID_CURRENT_X = "BUNDLE_ID_CURRENT_X"; 96 | 97 | /** The bundle id of the parents state. Used to restore the parent's state after a rotation occurs */ 98 | private static final String BUNDLE_ID_PARENT_STATE = "BUNDLE_ID_PARENT_STATE"; 99 | 100 | /** Tracks ongoing flings */ 101 | protected Scroller mFlingTracker = new Scroller(getContext()); 102 | 103 | /** Gesture listener to receive callbacks when gestures are detected */ 104 | private final GestureListener mGestureListener = new GestureListener(); 105 | 106 | /** Used for detecting gestures within this view so they can be handled */ 107 | private GestureDetector mGestureDetector; 108 | 109 | /** This tracks the starting layout position of the leftmost view */ 110 | private int mDisplayOffset; 111 | 112 | /** Holds a reference to the adapter bound to this view */ 113 | protected ListAdapter mAdapter; 114 | 115 | /** Holds a cache of recycled views to be reused as needed */ 116 | private List> mRemovedViewsCache = new ArrayList>(); 117 | 118 | /** Flag used to mark when the adapters data has changed, so the view can be relaid out */ 119 | private boolean mDataChanged = false; 120 | 121 | /** Temporary rectangle to be used for measurements */ 122 | private Rect mRect = new Rect(); 123 | 124 | /** Tracks the currently touched view, used to delegate touches to the view being touched */ 125 | private View mViewBeingTouched = null; 126 | 127 | /** The width of the divider that will be used between list items */ 128 | private int mDividerWidth = 0; 129 | 130 | /** The drawable that will be used as the list divider */ 131 | private Drawable mDivider = null; 132 | 133 | /** The x position of the currently rendered view */ 134 | protected int mCurrentX; 135 | 136 | /** The x position of the next to be rendered view */ 137 | protected int mNextX; 138 | 139 | /** Used to hold the scroll position to restore to post rotate */ 140 | private Integer mRestoreX = null; 141 | 142 | /** Tracks the maximum possible X position, stays at max value until last item is laid out and it can be determined */ 143 | private int mMaxX = Integer.MAX_VALUE; 144 | 145 | /** The adapter index of the leftmost view currently visible */ 146 | private int mLeftViewAdapterIndex; 147 | 148 | /** The adapter index of the rightmost view currently visible */ 149 | private int mRightViewAdapterIndex; 150 | 151 | /** This tracks the currently selected accessibility item */ 152 | private int mCurrentlySelectedAdapterIndex; 153 | 154 | /** 155 | * Callback interface to notify listener that the user has scrolled this view to the point that it is low on data. 156 | */ 157 | private RunningOutOfDataListener mRunningOutOfDataListener = null; 158 | 159 | /** 160 | * This tracks the user value set of how many items from the end will be considered running out of data. 161 | */ 162 | private int mRunningOutOfDataThreshold = 0; 163 | 164 | /** 165 | * Tracks if we have told the listener that we are running low on data. We only want to tell them once. 166 | */ 167 | private boolean mHasNotifiedRunningLowOnData = false; 168 | 169 | /** 170 | * Callback interface to be invoked when the scroll state has changed. 171 | */ 172 | private OnScrollStateChangedListener mOnScrollStateChangedListener = null; 173 | 174 | /** 175 | * Represents the current scroll state of this view. Needed so we can detect when the state changes so scroll listener can be notified. 176 | */ 177 | private OnScrollStateChangedListener.ScrollState mCurrentScrollState = OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE; 178 | 179 | /** 180 | * Tracks the state of the left edge glow. 181 | */ 182 | private EdgeEffectCompat mEdgeGlowLeft; 183 | 184 | /** 185 | * Tracks the state of the right edge glow. 186 | */ 187 | private EdgeEffectCompat mEdgeGlowRight; 188 | 189 | /** The height measure spec for this view, used to help size children views */ 190 | private int mHeightMeasureSpec; 191 | 192 | /** Used to track if a view touch should be blocked because it stopped a fling */ 193 | private boolean mBlockTouchAction = false; 194 | 195 | /** Used to track if the parent vertically scrollable view has been told to DisallowInterceptTouchEvent */ 196 | private boolean mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent = false; 197 | 198 | /** 199 | * The listener that receives notifications when this view is clicked. 200 | */ 201 | private OnClickListener mOnClickListener; 202 | 203 | public HorizontalListView(Context context, AttributeSet attrs) { 204 | super(context, attrs); 205 | mEdgeGlowLeft = new EdgeEffectCompat(context); 206 | mEdgeGlowRight = new EdgeEffectCompat(context); 207 | mGestureDetector = new GestureDetector(context, mGestureListener); 208 | bindGestureDetector(); 209 | initView(); 210 | retrieveXmlConfiguration(context, attrs); 211 | setWillNotDraw(false); 212 | 213 | // If the OS version is high enough then set the friction on the fling tracker */ 214 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 215 | HoneycombPlus.setFriction(mFlingTracker, FLING_FRICTION); 216 | } 217 | } 218 | 219 | /** Registers the gesture detector to receive gesture notifications for this view */ 220 | private void bindGestureDetector() { 221 | // Generic touch listener that can be applied to any view that needs to process gestures 222 | final View.OnTouchListener gestureListenerHandler = new View.OnTouchListener() { 223 | @Override 224 | public boolean onTouch(final View v, final MotionEvent event) { 225 | // Delegate the touch event to our gesture detector 226 | return mGestureDetector.onTouchEvent(event); 227 | } 228 | }; 229 | 230 | setOnTouchListener(gestureListenerHandler); 231 | } 232 | 233 | /** 234 | * When this HorizontalListView is embedded within a vertical scrolling view it is important to disable the parent view from interacting with 235 | * any touch events while the user is scrolling within this HorizontalListView. This will start at this view and go up the view tree looking 236 | * for a vertical scrolling view. If one is found it will enable or disable parent touch interception. 237 | * 238 | * @param disallowIntercept If true the parent will be prevented from intercepting child touch events 239 | */ 240 | private void requestParentListViewToNotInterceptTouchEvents(Boolean disallowIntercept) { 241 | // Prevent calling this more than once needlessly 242 | if (mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent != disallowIntercept) { 243 | View view = this; 244 | 245 | while (view.getParent() instanceof View) { 246 | // If the parent is a ListView or ScrollView then disallow intercepting of touch events 247 | if (view.getParent() instanceof ListView || view.getParent() instanceof ScrollView) { 248 | view.getParent().requestDisallowInterceptTouchEvent(disallowIntercept); 249 | mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent = disallowIntercept; 250 | return; 251 | } 252 | 253 | view = (View) view.getParent(); 254 | } 255 | } 256 | } 257 | 258 | /** 259 | * Parse the XML configuration for this widget 260 | * 261 | * @param context Context used for extracting attributes 262 | * @param attrs The Attribute Set containing the ColumnView attributes 263 | */ 264 | private void retrieveXmlConfiguration(Context context, AttributeSet attrs) { 265 | if (attrs != null) { 266 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HorizontalListView); 267 | 268 | // Get the provided drawable from the XML 269 | final Drawable d = a.getDrawable(R.styleable.HorizontalListView_android_divider); 270 | if (d != null) { 271 | // If a drawable is provided to use as the divider then use its intrinsic width for the divider width 272 | setDivider(d); 273 | } 274 | 275 | // If a width is explicitly specified then use that width 276 | final int dividerWidth = a.getDimensionPixelSize(R.styleable.HorizontalListView_dividerWidth, 0); 277 | if (dividerWidth != 0) { 278 | setDividerWidth(dividerWidth); 279 | } 280 | 281 | a.recycle(); 282 | } 283 | } 284 | 285 | @Override 286 | public Parcelable onSaveInstanceState() { 287 | Bundle bundle = new Bundle(); 288 | 289 | // Add the parent state to the bundle 290 | bundle.putParcelable(BUNDLE_ID_PARENT_STATE, super.onSaveInstanceState()); 291 | 292 | // Add our state to the bundle 293 | bundle.putInt(BUNDLE_ID_CURRENT_X, mCurrentX); 294 | 295 | return bundle; 296 | } 297 | 298 | @Override 299 | public void onRestoreInstanceState(Parcelable state) { 300 | if (state instanceof Bundle) { 301 | Bundle bundle = (Bundle) state; 302 | 303 | // Restore our state from the bundle 304 | mRestoreX = Integer.valueOf((bundle.getInt(BUNDLE_ID_CURRENT_X))); 305 | 306 | // Restore out parent's state from the bundle 307 | super.onRestoreInstanceState(bundle.getParcelable(BUNDLE_ID_PARENT_STATE)); 308 | } 309 | } 310 | 311 | /** 312 | * Sets the drawable that will be drawn between each item in the list. If the drawable does 313 | * not have an intrinsic width, you should also call {@link #setDividerWidth(int)} 314 | * 315 | * @param divider The drawable to use. 316 | */ 317 | public void setDivider(Drawable divider) { 318 | mDivider = divider; 319 | 320 | if (divider != null) { 321 | setDividerWidth(divider.getIntrinsicWidth()); 322 | } else { 323 | setDividerWidth(0); 324 | } 325 | } 326 | 327 | /** 328 | * Sets the width of the divider that will be drawn between each item in the list. Calling 329 | * this will override the intrinsic width as set by {@link #setDivider(Drawable)} 330 | * 331 | * @param width The width of the divider in pixels. 332 | */ 333 | public void setDividerWidth(int width) { 334 | mDividerWidth = width; 335 | 336 | // Force the view to rerender itself 337 | requestLayout(); 338 | invalidate(); 339 | } 340 | 341 | private void initView() { 342 | mLeftViewAdapterIndex = -1; 343 | mRightViewAdapterIndex = -1; 344 | mDisplayOffset = 0; 345 | mCurrentX = 0; 346 | mNextX = 0; 347 | mMaxX = Integer.MAX_VALUE; 348 | setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE); 349 | } 350 | 351 | /** Will re-initialize the HorizontalListView to remove all child views rendered and reset to initial configuration. */ 352 | private void reset() { 353 | initView(); 354 | removeAllViewsInLayout(); 355 | requestLayout(); 356 | } 357 | 358 | /** DataSetObserver used to capture adapter data change events */ 359 | private DataSetObserver mAdapterDataObserver = new DataSetObserver() { 360 | @Override 361 | public void onChanged() { 362 | mDataChanged = true; 363 | 364 | // Clear so we can notify again as we run out of data 365 | mHasNotifiedRunningLowOnData = false; 366 | 367 | unpressTouchedChild(); 368 | 369 | // Invalidate and request layout to force this view to completely redraw itself 370 | invalidate(); 371 | requestLayout(); 372 | } 373 | 374 | @Override 375 | public void onInvalidated() { 376 | // Clear so we can notify again as we run out of data 377 | mHasNotifiedRunningLowOnData = false; 378 | 379 | unpressTouchedChild(); 380 | reset(); 381 | 382 | // Invalidate and request layout to force this view to completely redraw itself 383 | invalidate(); 384 | requestLayout(); 385 | } 386 | }; 387 | 388 | @Override 389 | public void setSelection(int position) { 390 | mCurrentlySelectedAdapterIndex = position; 391 | } 392 | 393 | @Override 394 | public View getSelectedView() { 395 | return getChild(mCurrentlySelectedAdapterIndex); 396 | } 397 | 398 | @Override 399 | public void setAdapter(ListAdapter adapter) { 400 | if (mAdapter != null) { 401 | mAdapter.unregisterDataSetObserver(mAdapterDataObserver); 402 | } 403 | 404 | if (adapter != null) { 405 | // Clear so we can notify again as we run out of data 406 | mHasNotifiedRunningLowOnData = false; 407 | 408 | mAdapter = adapter; 409 | mAdapter.registerDataSetObserver(mAdapterDataObserver); 410 | } 411 | 412 | initializeRecycledViewCache(mAdapter.getViewTypeCount()); 413 | reset(); 414 | } 415 | 416 | @Override 417 | public ListAdapter getAdapter() { 418 | return mAdapter; 419 | } 420 | 421 | /** 422 | * Will create and initialize a cache for the given number of different types of views. 423 | * 424 | * @param viewTypeCount - The total number of different views supported 425 | */ 426 | private void initializeRecycledViewCache(int viewTypeCount) { 427 | // The cache is created such that the response from mAdapter.getItemViewType is the array index to the correct cache for that item. 428 | mRemovedViewsCache.clear(); 429 | for (int i = 0; i < viewTypeCount; i++) { 430 | mRemovedViewsCache.add(new LinkedList()); 431 | } 432 | } 433 | 434 | /** 435 | * Returns a recycled view from the cache that can be reused, or null if one is not available. 436 | * 437 | * @param adapterIndex 438 | * @return 439 | */ 440 | private View getRecycledView(int adapterIndex) { 441 | int itemViewType = mAdapter.getItemViewType(adapterIndex); 442 | 443 | if (isItemViewTypeValid(itemViewType)) { 444 | return mRemovedViewsCache.get(itemViewType).poll(); 445 | } 446 | 447 | return null; 448 | } 449 | 450 | /** 451 | * Adds the provided view to a recycled views cache. 452 | * 453 | * @param adapterIndex 454 | * @param view 455 | */ 456 | private void recycleView(int adapterIndex, View view) { 457 | // There is one Queue of views for each different type of view. 458 | // Just add the view to the pile of other views of the same type. 459 | // The order they are added and removed does not matter. 460 | int itemViewType = mAdapter.getItemViewType(adapterIndex); 461 | if (isItemViewTypeValid(itemViewType)) { 462 | mRemovedViewsCache.get(itemViewType).offer(view); 463 | } 464 | } 465 | 466 | private boolean isItemViewTypeValid(int itemViewType) { 467 | return itemViewType < mRemovedViewsCache.size(); 468 | } 469 | 470 | /** Adds a child to this viewgroup and measures it so it renders the correct size */ 471 | private void addAndMeasureChild(final View child, int viewPos) { 472 | LayoutParams params = getLayoutParams(child); 473 | addViewInLayout(child, viewPos, params, true); 474 | measureChild(child); 475 | } 476 | 477 | /** 478 | * Measure the provided child. 479 | * 480 | * @param child The child. 481 | */ 482 | private void measureChild(View child) { 483 | ViewGroup.LayoutParams childLayoutParams = getLayoutParams(child); 484 | int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec, getPaddingTop() + getPaddingBottom(), childLayoutParams.height); 485 | 486 | int childWidthSpec; 487 | if (childLayoutParams.width > 0) { 488 | childWidthSpec = MeasureSpec.makeMeasureSpec(childLayoutParams.width, MeasureSpec.EXACTLY); 489 | } else { 490 | childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 491 | } 492 | 493 | child.measure(childWidthSpec, childHeightSpec); 494 | } 495 | 496 | /** Gets a child's layout parameters, defaults if not available. */ 497 | private ViewGroup.LayoutParams getLayoutParams(View child) { 498 | ViewGroup.LayoutParams layoutParams = child.getLayoutParams(); 499 | if (layoutParams == null) { 500 | // Since this is a horizontal list view default to matching the parents height, and wrapping the width 501 | layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); 502 | } 503 | 504 | return layoutParams; 505 | } 506 | 507 | @SuppressLint("WrongCall") 508 | @Override 509 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 510 | super.onLayout(changed, left, top, right, bottom); 511 | 512 | if (mAdapter == null) { 513 | return; 514 | } 515 | 516 | // Force the OS to redraw this view 517 | invalidate(); 518 | 519 | // If the data changed then reset everything and render from scratch at the same offset as last time 520 | if (mDataChanged) { 521 | int oldCurrentX = mCurrentX; 522 | initView(); 523 | removeAllViewsInLayout(); 524 | mNextX = oldCurrentX; 525 | mDataChanged = false; 526 | } 527 | 528 | // If restoring from a rotation 529 | if (mRestoreX != null) { 530 | mNextX = mRestoreX; 531 | mRestoreX = null; 532 | } 533 | 534 | // If in a fling 535 | if (mFlingTracker.computeScrollOffset()) { 536 | // Compute the next position 537 | mNextX = mFlingTracker.getCurrX(); 538 | } 539 | 540 | // Prevent scrolling past 0 so you can't scroll past the end of the list to the left 541 | if (mNextX < 0) { 542 | mNextX = 0; 543 | 544 | // Show an edge effect absorbing the current velocity 545 | if (mEdgeGlowLeft.isFinished()) { 546 | mEdgeGlowLeft.onAbsorb((int) determineFlingAbsorbVelocity()); 547 | } 548 | 549 | mFlingTracker.forceFinished(true); 550 | setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE); 551 | } else if (mNextX > mMaxX) { 552 | // Clip the maximum scroll position at mMaxX so you can't scroll past the end of the list to the right 553 | mNextX = mMaxX; 554 | 555 | // Show an edge effect absorbing the current velocity 556 | if (mEdgeGlowRight.isFinished()) { 557 | mEdgeGlowRight.onAbsorb((int) determineFlingAbsorbVelocity()); 558 | } 559 | 560 | mFlingTracker.forceFinished(true); 561 | setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE); 562 | } 563 | 564 | // Calculate our delta from the last time the view was drawn 565 | int dx = mCurrentX - mNextX; 566 | removeNonVisibleChildren(dx); 567 | fillList(dx); 568 | positionChildren(dx); 569 | 570 | // Since the view has now been drawn, update our current position 571 | mCurrentX = mNextX; 572 | 573 | // If we have scrolled enough to lay out all views, then determine the maximum scroll position now 574 | if (determineMaxX()) { 575 | // Redo the layout pass since we now know the maximum scroll position 576 | onLayout(changed, left, top, right, bottom); 577 | return; 578 | } 579 | 580 | // If the fling has finished 581 | if (mFlingTracker.isFinished()) { 582 | // If the fling just ended 583 | if (mCurrentScrollState == OnScrollStateChangedListener.ScrollState.SCROLL_STATE_FLING) { 584 | setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE); 585 | } 586 | } else { 587 | // Still in a fling so schedule the next frame 588 | ViewCompat.postOnAnimation(this, mDelayedLayout); 589 | } 590 | } 591 | 592 | @Override 593 | protected float getLeftFadingEdgeStrength() { 594 | int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength(); 595 | 596 | // If completely at the edge then disable the fading edge 597 | if (mCurrentX == 0) { 598 | return 0; 599 | } else if (mCurrentX < horizontalFadingEdgeLength) { 600 | // We are very close to the edge, so enable the fading edge proportional to the distance from the edge, and the width of the edge effect 601 | return (float) mCurrentX / horizontalFadingEdgeLength; 602 | } else { 603 | // The current x position is more then the width of the fading edge so enable it fully. 604 | return 1; 605 | } 606 | } 607 | 608 | @Override 609 | protected float getRightFadingEdgeStrength() { 610 | int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength(); 611 | 612 | // If completely at the edge then disable the fading edge 613 | if (mCurrentX == mMaxX) { 614 | return 0; 615 | } else if ((mMaxX - mCurrentX) < horizontalFadingEdgeLength) { 616 | // We are very close to the edge, so enable the fading edge proportional to the distance from the ednge, and the width of the edge effect 617 | return (float) (mMaxX - mCurrentX) / horizontalFadingEdgeLength; 618 | } else { 619 | // The distance from the maximum x position is more then the width of the fading edge so enable it fully. 620 | return 1; 621 | } 622 | } 623 | 624 | /** Determines the current fling absorb velocity */ 625 | private float determineFlingAbsorbVelocity() { 626 | // If the OS version is high enough get the real velocity */ 627 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 628 | return IceCreamSandwichPlus.getCurrVelocity(mFlingTracker); 629 | } else { 630 | // Unable to get the velocity so just return a default. 631 | // In actuality this is never used since EdgeEffectCompat does not draw anything unless the device is ICS+. 632 | // Less then ICS EdgeEffectCompat essentially performs a NOP. 633 | return FLING_DEFAULT_ABSORB_VELOCITY; 634 | } 635 | } 636 | 637 | /** Use to schedule a request layout via a runnable */ 638 | private Runnable mDelayedLayout = new Runnable() { 639 | @Override 640 | public void run() { 641 | requestLayout(); 642 | } 643 | }; 644 | 645 | @Override 646 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 647 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 648 | 649 | // Cache off the measure spec 650 | mHeightMeasureSpec = heightMeasureSpec; 651 | }; 652 | 653 | /** 654 | * Determine the Max X position. This is the farthest that the user can scroll the screen. Until the last adapter item has been 655 | * laid out it is impossible to calculate; once that has occurred this will perform the calculation, and if necessary force a 656 | * redraw and relayout of this view. 657 | * 658 | * @return true if the maxx position was just determined 659 | */ 660 | private boolean determineMaxX() { 661 | // If the last view has been laid out, then we can determine the maximum x position 662 | if (isLastItemInAdapter(mRightViewAdapterIndex)) { 663 | View rightView = getRightmostChild(); 664 | 665 | if (rightView != null) { 666 | int oldMaxX = mMaxX; 667 | 668 | // Determine the maximum x position 669 | mMaxX = mCurrentX + (rightView.getRight() - getPaddingLeft()) - getRenderWidth(); 670 | 671 | // Handle the case where the views do not fill at least 1 screen 672 | if (mMaxX < 0) { 673 | mMaxX = 0; 674 | } 675 | 676 | if (mMaxX != oldMaxX) { 677 | return true; 678 | } 679 | } 680 | } 681 | 682 | return false; 683 | } 684 | 685 | /** Adds children views to the left and right of the current views until the screen is full */ 686 | private void fillList(final int dx) { 687 | // Get the rightmost child and determine its right edge 688 | int edge = 0; 689 | View child = getRightmostChild(); 690 | if (child != null) { 691 | edge = child.getRight(); 692 | } 693 | 694 | // Add new children views to the right, until past the edge of the screen 695 | fillListRight(edge, dx); 696 | 697 | // Get the leftmost child and determine its left edge 698 | edge = 0; 699 | child = getLeftmostChild(); 700 | if (child != null) { 701 | edge = child.getLeft(); 702 | } 703 | 704 | // Add new children views to the left, until past the edge of the screen 705 | fillListLeft(edge, dx); 706 | } 707 | 708 | private void removeNonVisibleChildren(final int dx) { 709 | View child = getLeftmostChild(); 710 | 711 | // Loop removing the leftmost child, until that child is on the screen 712 | while (child != null && child.getRight() + dx <= 0) { 713 | // The child is being completely removed so remove its width from the display offset and its divider if it has one. 714 | // To remove add the size of the child and its divider (if it has one) to the offset. 715 | // You need to add since its being removed from the left side, i.e. shifting the offset to the right. 716 | mDisplayOffset += isLastItemInAdapter(mLeftViewAdapterIndex) ? child.getMeasuredWidth() : mDividerWidth + child.getMeasuredWidth(); 717 | 718 | // Add the removed view to the cache 719 | recycleView(mLeftViewAdapterIndex, child); 720 | 721 | // Actually remove the view 722 | removeViewInLayout(child); 723 | 724 | // Keep track of the adapter index of the left most child 725 | mLeftViewAdapterIndex++; 726 | 727 | // Get the new leftmost child 728 | child = getLeftmostChild(); 729 | } 730 | 731 | child = getRightmostChild(); 732 | 733 | // Loop removing the rightmost child, until that child is on the screen 734 | while (child != null && child.getLeft() + dx >= getWidth()) { 735 | recycleView(mRightViewAdapterIndex, child); 736 | removeViewInLayout(child); 737 | mRightViewAdapterIndex--; 738 | child = getRightmostChild(); 739 | } 740 | } 741 | 742 | private void fillListRight(int rightEdge, final int dx) { 743 | // Loop adding views to the right until the screen is filled 744 | while (rightEdge + dx + mDividerWidth < getWidth() && mRightViewAdapterIndex + 1 < mAdapter.getCount()) { 745 | mRightViewAdapterIndex++; 746 | 747 | // If mLeftViewAdapterIndex < 0 then this is the first time a view is being added, and left == right 748 | if (mLeftViewAdapterIndex < 0) { 749 | mLeftViewAdapterIndex = mRightViewAdapterIndex; 750 | } 751 | 752 | // Get the view from the adapter, utilizing a cached view if one is available 753 | View child = mAdapter.getView(mRightViewAdapterIndex, getRecycledView(mRightViewAdapterIndex), this); 754 | addAndMeasureChild(child, INSERT_AT_END_OF_LIST); 755 | 756 | // If first view, then no divider to the left of it, otherwise add the space for the divider width 757 | rightEdge += (mRightViewAdapterIndex == 0 ? 0 : mDividerWidth) + child.getMeasuredWidth(); 758 | 759 | // Check if we are running low on data so we can tell listeners to go get more 760 | determineIfLowOnData(); 761 | } 762 | } 763 | 764 | private void fillListLeft(int leftEdge, final int dx) { 765 | // Loop adding views to the left until the screen is filled 766 | while (leftEdge + dx - mDividerWidth > 0 && mLeftViewAdapterIndex >= 1) { 767 | mLeftViewAdapterIndex--; 768 | View child = mAdapter.getView(mLeftViewAdapterIndex, getRecycledView(mLeftViewAdapterIndex), this); 769 | addAndMeasureChild(child, INSERT_AT_START_OF_LIST); 770 | 771 | // If first view, then no divider to the left of it 772 | leftEdge -= mLeftViewAdapterIndex == 0 ? child.getMeasuredWidth() : mDividerWidth + child.getMeasuredWidth(); 773 | 774 | // If on a clean edge then just remove the child, otherwise remove the divider as well 775 | mDisplayOffset -= leftEdge + dx == 0 ? child.getMeasuredWidth() : mDividerWidth + child.getMeasuredWidth(); 776 | } 777 | } 778 | 779 | /** Loops through each child and positions them onto the screen */ 780 | private void positionChildren(final int dx) { 781 | int childCount = getChildCount(); 782 | 783 | if (childCount > 0) { 784 | mDisplayOffset += dx; 785 | int leftOffset = mDisplayOffset; 786 | 787 | // Loop each child view 788 | for (int i = 0; i < childCount; i++) { 789 | View child = getChildAt(i); 790 | int left = leftOffset + getPaddingLeft(); 791 | int top = getPaddingTop(); 792 | int right = left + child.getMeasuredWidth(); 793 | int bottom = top + child.getMeasuredHeight(); 794 | 795 | // Layout the child 796 | child.layout(left, top, right, bottom); 797 | 798 | // Increment our offset by added child's size and divider width 799 | leftOffset += child.getMeasuredWidth() + mDividerWidth; 800 | } 801 | } 802 | } 803 | 804 | /** Gets the current child that is leftmost on the screen. */ 805 | private View getLeftmostChild() { 806 | return getChildAt(0); 807 | } 808 | 809 | /** Gets the current child that is rightmost on the screen. */ 810 | private View getRightmostChild() { 811 | return getChildAt(getChildCount() - 1); 812 | } 813 | 814 | /** 815 | * Finds a child view that is contained within this view, given the adapter index. 816 | * @return View The child view, or or null if not found. 817 | */ 818 | private View getChild(int adapterIndex) { 819 | if (adapterIndex >= mLeftViewAdapterIndex && adapterIndex <= mRightViewAdapterIndex) { 820 | return getChildAt(adapterIndex - mLeftViewAdapterIndex); 821 | } 822 | 823 | return null; 824 | } 825 | 826 | /** 827 | * Returns the index of the child that contains the coordinates given. 828 | * This is useful to determine which child has been touched. 829 | * This can be used for a call to {@link #getChildAt(int)} 830 | * 831 | * @param x X-coordinate 832 | * @param y Y-coordinate 833 | * @return The index of the child that contains the coordinates. If no child is found then returns -1 834 | */ 835 | private int getChildIndex(final int x, final int y) { 836 | int childCount = getChildCount(); 837 | 838 | for (int index = 0; index < childCount; index++) { 839 | getChildAt(index).getHitRect(mRect); 840 | if (mRect.contains(x, y)) { 841 | return index; 842 | } 843 | } 844 | 845 | return -1; 846 | } 847 | 848 | /** Simple convenience method for determining if this index is the last index in the adapter */ 849 | private boolean isLastItemInAdapter(int index) { 850 | return index == mAdapter.getCount() - 1; 851 | } 852 | 853 | /** Gets the height in px this view will be rendered. (padding removed) */ 854 | private int getRenderHeight() { 855 | return getHeight() - getPaddingTop() - getPaddingBottom(); 856 | } 857 | 858 | /** Gets the width in px this view will be rendered. (padding removed) */ 859 | private int getRenderWidth() { 860 | return getWidth() - getPaddingLeft() - getPaddingRight(); 861 | } 862 | 863 | /** Scroll to the provided offset */ 864 | public void scrollTo(int x) { 865 | mFlingTracker.startScroll(mNextX, 0, x - mNextX, 0); 866 | setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_FLING); 867 | requestLayout(); 868 | } 869 | 870 | @Override 871 | public int getFirstVisiblePosition() { 872 | return mLeftViewAdapterIndex; 873 | } 874 | 875 | @Override 876 | public int getLastVisiblePosition() { 877 | return mRightViewAdapterIndex; 878 | } 879 | 880 | /** Draws the overscroll edge glow effect on the left and right sides of the horizontal list */ 881 | private void drawEdgeGlow(Canvas canvas) { 882 | if (mEdgeGlowLeft != null && !mEdgeGlowLeft.isFinished() && isEdgeGlowEnabled()) { 883 | // The Edge glow is meant to come from the top of the screen, so rotate it to draw on the left side. 884 | final int restoreCount = canvas.save(); 885 | final int height = getHeight(); 886 | 887 | canvas.rotate(-90, 0, 0); 888 | canvas.translate(-height + getPaddingBottom(), 0); 889 | 890 | mEdgeGlowLeft.setSize(getRenderHeight(), getRenderWidth()); 891 | if (mEdgeGlowLeft.draw(canvas)) { 892 | invalidate(); 893 | } 894 | 895 | canvas.restoreToCount(restoreCount); 896 | } else if (mEdgeGlowRight != null && !mEdgeGlowRight.isFinished() && isEdgeGlowEnabled()) { 897 | // The Edge glow is meant to come from the top of the screen, so rotate it to draw on the right side. 898 | final int restoreCount = canvas.save(); 899 | final int width = getWidth(); 900 | 901 | canvas.rotate(90, 0, 0); 902 | canvas.translate(getPaddingTop(), -width); 903 | mEdgeGlowRight.setSize(getRenderHeight(), getRenderWidth()); 904 | if (mEdgeGlowRight.draw(canvas)) { 905 | invalidate(); 906 | } 907 | 908 | canvas.restoreToCount(restoreCount); 909 | } 910 | } 911 | 912 | /** Draws the dividers that go in between the horizontal list view items */ 913 | private void drawDividers(Canvas canvas) { 914 | final int count = getChildCount(); 915 | 916 | // Only modify the left and right in the loop, we set the top and bottom here since they are always the same 917 | final Rect bounds = mRect; 918 | mRect.top = getPaddingTop(); 919 | mRect.bottom = mRect.top + getRenderHeight(); 920 | 921 | // Draw the list dividers 922 | for (int i = 0; i < count; i++) { 923 | // Don't draw a divider to the right of the last item in the adapter 924 | if (!(i == count - 1 && isLastItemInAdapter(mRightViewAdapterIndex))) { 925 | View child = getChildAt(i); 926 | 927 | bounds.left = child.getRight(); 928 | bounds.right = child.getRight() + mDividerWidth; 929 | 930 | // Clip at the left edge of the screen 931 | if (bounds.left < getPaddingLeft()) { 932 | bounds.left = getPaddingLeft(); 933 | } 934 | 935 | // Clip at the right edge of the screen 936 | if (bounds.right > getWidth() - getPaddingRight()) { 937 | bounds.right = getWidth() - getPaddingRight(); 938 | } 939 | 940 | // Draw a divider to the right of the child 941 | drawDivider(canvas, bounds); 942 | 943 | // If the first view, determine if a divider should be shown to the left of it. 944 | // A divider should be shown if the left side of this view does not fill to the left edge of the screen. 945 | if (i == 0 && child.getLeft() > getPaddingLeft()) { 946 | bounds.left = getPaddingLeft(); 947 | bounds.right = child.getLeft(); 948 | drawDivider(canvas, bounds); 949 | } 950 | } 951 | } 952 | } 953 | 954 | /** 955 | * Draws a divider in the given bounds. 956 | * 957 | * @param canvas The canvas to draw to. 958 | * @param bounds The bounds of the divider. 959 | */ 960 | private void drawDivider(Canvas canvas, Rect bounds) { 961 | if (mDivider != null) { 962 | mDivider.setBounds(bounds); 963 | mDivider.draw(canvas); 964 | } 965 | } 966 | 967 | @Override 968 | protected void onDraw(Canvas canvas) { 969 | super.onDraw(canvas); 970 | drawDividers(canvas); 971 | } 972 | 973 | @Override 974 | protected void dispatchDraw(Canvas canvas) { 975 | super.dispatchDraw(canvas); 976 | drawEdgeGlow(canvas); 977 | } 978 | 979 | @Override 980 | protected void dispatchSetPressed(boolean pressed) { 981 | // Don't dispatch setPressed to our children. We call setPressed on ourselves to 982 | // get the selector in the right state, but we don't want to press each child. 983 | } 984 | 985 | protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 986 | mFlingTracker.fling(mNextX, 0, (int) -velocityX, 0, 0, mMaxX, 0, 0); 987 | setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_FLING); 988 | requestLayout(); 989 | return true; 990 | } 991 | 992 | protected boolean onDown(MotionEvent e) { 993 | // If the user just caught a fling, then disable all touch actions until they release their finger 994 | mBlockTouchAction = !mFlingTracker.isFinished(); 995 | 996 | // Allow a finger down event to catch a fling 997 | mFlingTracker.forceFinished(true); 998 | setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE); 999 | 1000 | unpressTouchedChild(); 1001 | 1002 | if (!mBlockTouchAction) { 1003 | // Find the child that was pressed 1004 | final int index = getChildIndex((int) e.getX(), (int) e.getY()); 1005 | if (index >= 0) { 1006 | // Save off view being touched so it can later be released 1007 | mViewBeingTouched = getChildAt(index); 1008 | 1009 | if (mViewBeingTouched != null) { 1010 | // Set the view as pressed 1011 | mViewBeingTouched.setPressed(true); 1012 | refreshDrawableState(); 1013 | } 1014 | } 1015 | } 1016 | 1017 | return true; 1018 | } 1019 | 1020 | /** If a view is currently pressed then unpress it */ 1021 | private void unpressTouchedChild() { 1022 | if (mViewBeingTouched != null) { 1023 | // Set the view as not pressed 1024 | mViewBeingTouched.setPressed(false); 1025 | refreshDrawableState(); 1026 | 1027 | // Null out the view so we don't leak it 1028 | mViewBeingTouched = null; 1029 | } 1030 | } 1031 | 1032 | private class GestureListener extends GestureDetector.SimpleOnGestureListener { 1033 | @Override 1034 | public boolean onDown(MotionEvent e) { 1035 | return HorizontalListView.this.onDown(e); 1036 | } 1037 | 1038 | @Override 1039 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 1040 | return HorizontalListView.this.onFling(e1, e2, velocityX, velocityY); 1041 | } 1042 | 1043 | @Override 1044 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 1045 | // Lock the user into interacting just with this view 1046 | requestParentListViewToNotInterceptTouchEvents(true); 1047 | 1048 | setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_TOUCH_SCROLL); 1049 | unpressTouchedChild(); 1050 | mNextX += (int) distanceX; 1051 | updateOverscrollAnimation(Math.round(distanceX)); 1052 | requestLayout(); 1053 | 1054 | return true; 1055 | } 1056 | 1057 | @Override 1058 | public boolean onSingleTapConfirmed(MotionEvent e) { 1059 | unpressTouchedChild(); 1060 | OnItemClickListener onItemClickListener = getOnItemClickListener(); 1061 | 1062 | final int index = getChildIndex((int) e.getX(), (int) e.getY()); 1063 | 1064 | // If the tap is inside one of the child views, and we are not blocking touches 1065 | if (index >= 0 && !mBlockTouchAction) { 1066 | View child = getChildAt(index); 1067 | int adapterIndex = mLeftViewAdapterIndex + index; 1068 | 1069 | if (onItemClickListener != null) { 1070 | onItemClickListener.onItemClick(HorizontalListView.this, child, adapterIndex, mAdapter.getItemId(adapterIndex)); 1071 | return true; 1072 | } 1073 | } 1074 | 1075 | if (mOnClickListener != null && !mBlockTouchAction) { 1076 | mOnClickListener.onClick(HorizontalListView.this); 1077 | } 1078 | 1079 | return false; 1080 | } 1081 | 1082 | @Override 1083 | public void onLongPress(MotionEvent e) { 1084 | unpressTouchedChild(); 1085 | 1086 | final int index = getChildIndex((int) e.getX(), (int) e.getY()); 1087 | if (index >= 0 && !mBlockTouchAction) { 1088 | View child = getChildAt(index); 1089 | OnItemLongClickListener onItemLongClickListener = getOnItemLongClickListener(); 1090 | if (onItemLongClickListener != null) { 1091 | int adapterIndex = mLeftViewAdapterIndex + index; 1092 | boolean handled = onItemLongClickListener.onItemLongClick(HorizontalListView.this, child, adapterIndex, mAdapter 1093 | .getItemId(adapterIndex)); 1094 | 1095 | if (handled) { 1096 | // BZZZTT!!1! 1097 | performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 1098 | } 1099 | } 1100 | } 1101 | } 1102 | }; 1103 | 1104 | @Override 1105 | public boolean onTouchEvent(MotionEvent event) { 1106 | // Detect when the user lifts their finger off the screen after a touch 1107 | if (event.getAction() == MotionEvent.ACTION_UP) { 1108 | // If not flinging then we are idle now. The user just finished a finger scroll. 1109 | if (mFlingTracker == null || mFlingTracker.isFinished()) { 1110 | setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE); 1111 | } 1112 | 1113 | // Allow the user to interact with parent views 1114 | requestParentListViewToNotInterceptTouchEvents(false); 1115 | 1116 | releaseEdgeGlow(); 1117 | } else if (event.getAction() == MotionEvent.ACTION_CANCEL) { 1118 | unpressTouchedChild(); 1119 | releaseEdgeGlow(); 1120 | 1121 | // Allow the user to interact with parent views 1122 | requestParentListViewToNotInterceptTouchEvents(false); 1123 | } 1124 | 1125 | return super.onTouchEvent(event); 1126 | } 1127 | 1128 | /** Release the EdgeGlow so it animates */ 1129 | private void releaseEdgeGlow() { 1130 | if (mEdgeGlowLeft != null) { 1131 | mEdgeGlowLeft.onRelease(); 1132 | } 1133 | 1134 | if (mEdgeGlowRight != null) { 1135 | mEdgeGlowRight.onRelease(); 1136 | } 1137 | } 1138 | 1139 | /** 1140 | * Sets a listener to be called when the HorizontalListView has been scrolled to a point where it is 1141 | * running low on data. An example use case is wanting to auto download more data when the user 1142 | * has scrolled to the point where only 10 items are left to be rendered off the right of the 1143 | * screen. To get called back at that point just register with this function with a 1144 | * numberOfItemsLeftConsideredLow value of 10.
1145 | *
1146 | * This will only be called once to notify that the HorizontalListView is running low on data. 1147 | * Calling notifyDataSetChanged on the adapter will allow this to be called again once low on data. 1148 | * 1149 | * @param listener The listener to be notified when the number of array adapters items left to 1150 | * be shown is running low. 1151 | * 1152 | * @param numberOfItemsLeftConsideredLow The number of array adapter items that have not yet 1153 | * been displayed that is considered too low. 1154 | */ 1155 | public void setRunningOutOfDataListener(RunningOutOfDataListener listener, int numberOfItemsLeftConsideredLow) { 1156 | mRunningOutOfDataListener = listener; 1157 | mRunningOutOfDataThreshold = numberOfItemsLeftConsideredLow; 1158 | } 1159 | 1160 | /** 1161 | * This listener is used to allow notification when the HorizontalListView is running low on data to display. 1162 | */ 1163 | public static interface RunningOutOfDataListener { 1164 | /** Called when the HorizontalListView is running out of data and has reached at least the provided threshold. */ 1165 | void onRunningOutOfData(); 1166 | } 1167 | 1168 | /** 1169 | * Determines if we are low on data and if so will call to notify the listener, if there is one, 1170 | * that we are running low on data. 1171 | */ 1172 | private void determineIfLowOnData() { 1173 | // Check if the threshold has been reached and a listener is registered 1174 | if (mRunningOutOfDataListener != null && mAdapter != null && 1175 | mAdapter.getCount() - (mRightViewAdapterIndex + 1) < mRunningOutOfDataThreshold) { 1176 | 1177 | // Prevent notification more than once 1178 | if (!mHasNotifiedRunningLowOnData) { 1179 | mHasNotifiedRunningLowOnData = true; 1180 | mRunningOutOfDataListener.onRunningOutOfData(); 1181 | } 1182 | } 1183 | } 1184 | 1185 | /** 1186 | * Register a callback to be invoked when the HorizontalListView has been clicked. 1187 | * 1188 | * @param listener The callback that will be invoked. 1189 | */ 1190 | @Override 1191 | public void setOnClickListener(OnClickListener listener) { 1192 | mOnClickListener = listener; 1193 | } 1194 | 1195 | /** 1196 | * Interface definition for a callback to be invoked when the view scroll state has changed. 1197 | */ 1198 | public interface OnScrollStateChangedListener { 1199 | public enum ScrollState { 1200 | /** 1201 | * The view is not scrolling. Note navigating the list using the trackball counts as being 1202 | * in the idle state since these transitions are not animated. 1203 | */ 1204 | SCROLL_STATE_IDLE, 1205 | 1206 | /** 1207 | * The user is scrolling using touch, and their finger is still on the screen 1208 | */ 1209 | SCROLL_STATE_TOUCH_SCROLL, 1210 | 1211 | /** 1212 | * The user had previously been scrolling using touch and had performed a fling. The 1213 | * animation is now coasting to a stop 1214 | */ 1215 | SCROLL_STATE_FLING 1216 | } 1217 | 1218 | /** 1219 | * Callback method to be invoked when the scroll state changes. 1220 | * 1221 | * @param scrollState The current scroll state. 1222 | */ 1223 | public void onScrollStateChanged(ScrollState scrollState); 1224 | } 1225 | 1226 | /** 1227 | * Sets a listener to be invoked when the scroll state has changed. 1228 | * 1229 | * @param listener The listener to be invoked. 1230 | */ 1231 | public void setOnScrollStateChangedListener(OnScrollStateChangedListener listener) { 1232 | mOnScrollStateChangedListener = listener; 1233 | } 1234 | 1235 | /** 1236 | * Call to set the new scroll state. 1237 | * If it has changed and a listener is registered then it will be notified. 1238 | */ 1239 | private void setCurrentScrollState(OnScrollStateChangedListener.ScrollState newScrollState) { 1240 | // If the state actually changed then notify listener if there is one 1241 | if (mCurrentScrollState != newScrollState && mOnScrollStateChangedListener != null) { 1242 | mOnScrollStateChangedListener.onScrollStateChanged(newScrollState); 1243 | } 1244 | 1245 | mCurrentScrollState = newScrollState; 1246 | } 1247 | 1248 | /** 1249 | * Updates the over scroll animation based on the scrolled offset. 1250 | * 1251 | * @param scrolledOffset The scroll offset 1252 | */ 1253 | private void updateOverscrollAnimation(final int scrolledOffset) { 1254 | if (mEdgeGlowLeft == null || mEdgeGlowRight == null) return; 1255 | 1256 | // Calculate where the next scroll position would be 1257 | int nextScrollPosition = mCurrentX + scrolledOffset; 1258 | 1259 | // If not currently in a fling (Don't want to allow fling offset updates to cause over scroll animation) 1260 | if (mFlingTracker == null || mFlingTracker.isFinished()) { 1261 | // If currently scrolled off the left side of the list and the adapter is not empty 1262 | if (nextScrollPosition < 0) { 1263 | 1264 | // Calculate the amount we have scrolled since last frame 1265 | int overscroll = Math.abs(scrolledOffset); 1266 | 1267 | // Tell the edge glow to redraw itself at the new offset 1268 | mEdgeGlowLeft.onPull((float) overscroll / getRenderWidth()); 1269 | 1270 | // Cancel animating right glow 1271 | if (!mEdgeGlowRight.isFinished()) { 1272 | mEdgeGlowRight.onRelease(); 1273 | } 1274 | } else if (nextScrollPosition > mMaxX) { 1275 | // Scrolled off the right of the list 1276 | 1277 | // Calculate the amount we have scrolled since last frame 1278 | int overscroll = Math.abs(scrolledOffset); 1279 | 1280 | // Tell the edge glow to redraw itself at the new offset 1281 | mEdgeGlowRight.onPull((float) overscroll / getRenderWidth()); 1282 | 1283 | // Cancel animating left glow 1284 | if (!mEdgeGlowLeft.isFinished()) { 1285 | mEdgeGlowLeft.onRelease(); 1286 | } 1287 | } 1288 | } 1289 | } 1290 | 1291 | /** 1292 | * Checks if the edge glow should be used enabled. 1293 | * The glow is not enabled unless there are more views than can fit on the screen at one time. 1294 | */ 1295 | private boolean isEdgeGlowEnabled() { 1296 | if (mAdapter == null || mAdapter.isEmpty()) return false; 1297 | 1298 | // If the maxx is more then zero then the user can scroll, so the edge effects should be shown 1299 | return mMaxX > 0; 1300 | } 1301 | 1302 | @TargetApi(11) 1303 | /** Wrapper class to protect access to API version 11 and above features */ 1304 | private static final class HoneycombPlus { 1305 | static { 1306 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { 1307 | throw new RuntimeException("Should not get to HoneycombPlus class unless sdk is >= 11!"); 1308 | } 1309 | } 1310 | 1311 | /** Sets the friction for the provided scroller */ 1312 | public static void setFriction(Scroller scroller, float friction) { 1313 | if (scroller != null) { 1314 | scroller.setFriction(friction); 1315 | } 1316 | } 1317 | } 1318 | 1319 | @TargetApi(14) 1320 | /** Wrapper class to protect access to API version 14 and above features */ 1321 | private static final class IceCreamSandwichPlus { 1322 | static { 1323 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 1324 | throw new RuntimeException("Should not get to IceCreamSandwichPlus class unless sdk is >= 14!"); 1325 | } 1326 | } 1327 | 1328 | /** Gets the velocity for the provided scroller */ 1329 | public static float getCurrVelocity(Scroller scroller) { 1330 | return scroller.getCurrVelocity(); 1331 | } 1332 | } 1333 | } 1334 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | AndroidHorizontalListViewSample 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeetMe/Android-HorizontalListView/40cdf31cbe953746b6eda8910c73540c48065667/AndroidHorizontalListViewSample/ic_launcher-web.png -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeetMe/Android-HorizontalListView/40cdf31cbe953746b6eda8910c73540c48065667/AndroidHorizontalListViewSample/libs/android-support-v4.jar -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-17 15 | android.library.reference.1=../AndroidHorizontalListView 16 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeetMe/Android-HorizontalListView/40cdf31cbe953746b6eda8910c73540c48065667/AndroidHorizontalListViewSample/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/res/drawable-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeetMe/Android-HorizontalListView/40cdf31cbe953746b6eda8910c73540c48065667/AndroidHorizontalListViewSample/res/drawable-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeetMe/Android-HorizontalListView/40cdf31cbe953746b6eda8910c73540c48065667/AndroidHorizontalListViewSample/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeetMe/Android-HorizontalListView/40cdf31cbe953746b6eda8910c73540c48065667/AndroidHorizontalListViewSample/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 19 | 20 | 24 | 25 | 32 | 33 | 37 | 38 | 45 | 46 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/res/layout/custom_data_view.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/res/menu/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/res/values-v11/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/res/values-v14/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AndroidHorizontalListViewSample 5 | Hello world! 6 | Settings 7 | Simple String List 8 | Custom Adapter List 9 | Custom Adapter List with dividers 10 | 11 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/src/com/meetme/android/horizontallistview/sample/CustomArrayAdapter.java: -------------------------------------------------------------------------------- 1 | package com.meetme.android.horizontallistview.sample; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ArrayAdapter; 8 | import android.widget.TextView; 9 | 10 | /** An array adapter that knows how to render views when given CustomData classes */ 11 | public class CustomArrayAdapter extends ArrayAdapter { 12 | private LayoutInflater mInflater; 13 | 14 | public CustomArrayAdapter(Context context, CustomData[] values) { 15 | super(context, R.layout.custom_data_view, values); 16 | mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 17 | } 18 | 19 | @Override 20 | public View getView(int position, View convertView, ViewGroup parent) { 21 | Holder holder; 22 | 23 | if (convertView == null) { 24 | // Inflate the view since it does not exist 25 | convertView = mInflater.inflate(R.layout.custom_data_view, parent, false); 26 | 27 | // Create and save off the holder in the tag so we get quick access to inner fields 28 | // This must be done for performance reasons 29 | holder = new Holder(); 30 | holder.textView = (TextView) convertView.findViewById(R.id.textView); 31 | convertView.setTag(holder); 32 | } else { 33 | holder = (Holder) convertView.getTag(); 34 | } 35 | 36 | // Populate the text 37 | holder.textView.setText(getItem(position).getText()); 38 | 39 | // Set the color 40 | convertView.setBackgroundColor(getItem(position).getBackgroundColor()); 41 | 42 | return convertView; 43 | } 44 | 45 | /** View holder for the views we need access to */ 46 | private static class Holder { 47 | public TextView textView; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/src/com/meetme/android/horizontallistview/sample/CustomData.java: -------------------------------------------------------------------------------- 1 | package com.meetme.android.horizontallistview.sample; 2 | 3 | /** This is just a simple class for holding data that is used to render our custom view */ 4 | public class CustomData { 5 | private int mBackgroundColor; 6 | private String mText; 7 | 8 | public CustomData(int backgroundColor, String text) { 9 | mBackgroundColor = backgroundColor; 10 | mText = text; 11 | } 12 | 13 | /** 14 | * @return the backgroundColor 15 | */ 16 | public int getBackgroundColor() { 17 | return mBackgroundColor; 18 | } 19 | 20 | /** 21 | * @return the text 22 | */ 23 | public String getText() { 24 | return mText; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /AndroidHorizontalListViewSample/src/com/meetme/android/horizontallistview/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.meetme.android.horizontallistview.sample; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Color; 5 | import android.os.Bundle; 6 | import android.view.Menu; 7 | import android.widget.ArrayAdapter; 8 | 9 | import com.meetme.android.horizontallistview.HorizontalListView; 10 | 11 | public class MainActivity extends Activity { 12 | 13 | private HorizontalListView mHlvSimpleList; 14 | private HorizontalListView mHlvCustomList; 15 | private HorizontalListView mHlvCustomListWithDividerAndFadingEdge; 16 | 17 | private String[] mSimpleListValues = new String[] { "Android", "iPhone", "WindowsMobile", 18 | "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X", 19 | "Linux", "OS/2" }; 20 | 21 | private CustomData[] mCustomData = new CustomData[] { 22 | new CustomData(Color.RED, "Red"), 23 | new CustomData(Color.DKGRAY, "Dark Gray"), 24 | new CustomData(Color.GREEN, "Green"), 25 | new CustomData(Color.LTGRAY, "Light Gray"), 26 | new CustomData(Color.WHITE, "White"), 27 | new CustomData(Color.RED, "Red"), 28 | new CustomData(Color.BLACK, "Black"), 29 | new CustomData(Color.CYAN, "Cyan"), 30 | new CustomData(Color.DKGRAY, "Dark Gray"), 31 | new CustomData(Color.GREEN, "Green"), 32 | new CustomData(Color.RED, "Red"), 33 | new CustomData(Color.LTGRAY, "Light Gray"), 34 | new CustomData(Color.WHITE, "White"), 35 | new CustomData(Color.BLACK, "Black"), 36 | new CustomData(Color.CYAN, "Cyan"), 37 | new CustomData(Color.DKGRAY, "Dark Gray"), 38 | new CustomData(Color.GREEN, "Green"), 39 | new CustomData(Color.LTGRAY, "Light Gray"), 40 | new CustomData(Color.RED, "Red"), 41 | new CustomData(Color.WHITE, "White"), 42 | new CustomData(Color.DKGRAY, "Dark Gray"), 43 | new CustomData(Color.GREEN, "Green"), 44 | new CustomData(Color.LTGRAY, "Light Gray"), 45 | new CustomData(Color.WHITE, "White"), 46 | new CustomData(Color.RED, "Red"), 47 | new CustomData(Color.BLACK, "Black"), 48 | new CustomData(Color.CYAN, "Cyan"), 49 | new CustomData(Color.DKGRAY, "Dark Gray"), 50 | new CustomData(Color.GREEN, "Green"), 51 | new CustomData(Color.LTGRAY, "Light Gray"), 52 | new CustomData(Color.RED, "Red"), 53 | new CustomData(Color.WHITE, "White"), 54 | new CustomData(Color.BLACK, "Black"), 55 | new CustomData(Color.CYAN, "Cyan"), 56 | new CustomData(Color.DKGRAY, "Dark Gray"), 57 | new CustomData(Color.GREEN, "Green"), 58 | new CustomData(Color.LTGRAY, "Light Gray") 59 | }; 60 | 61 | @Override 62 | protected void onCreate(Bundle savedInstanceState) { 63 | super.onCreate(savedInstanceState); 64 | setContentView(R.layout.activity_main); 65 | 66 | // Get references to UI widgets 67 | mHlvSimpleList = (HorizontalListView) findViewById(R.id.hlvSimpleList); 68 | mHlvCustomList = (HorizontalListView) findViewById(R.id.hlvCustomList); 69 | mHlvCustomListWithDividerAndFadingEdge = (HorizontalListView) findViewById(R.id.hlvCustomListWithDividerAndFadingEdge); 70 | 71 | setupSimpleList(); 72 | setupCustomLists(); 73 | } 74 | 75 | private void setupSimpleList() { 76 | // Make an array adapter using the built in android layout to render a list of strings 77 | ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_2, android.R.id.text1, mSimpleListValues); 78 | 79 | // Assign adapter to the HorizontalListView 80 | mHlvSimpleList.setAdapter(adapter); 81 | } 82 | 83 | private void setupCustomLists() { 84 | // Make an array adapter using the built in android layout to render a list of strings 85 | CustomArrayAdapter adapter = new CustomArrayAdapter(this, mCustomData); 86 | 87 | // Assign adapter to HorizontalListView 88 | mHlvCustomList.setAdapter(adapter); 89 | mHlvCustomListWithDividerAndFadingEdge.setAdapter(adapter); 90 | } 91 | 92 | @Override 93 | public boolean onCreateOptionsMenu(Menu menu) { 94 | // Inflate the menu; this adds items to the action bar if it is present. 95 | getMenuInflater().inflate(R.menu.activity_main, menu); 96 | return true; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HorizontalListView 2 | 3 | HorizontalListView is an Android ListView widget which scrolls in a horizontal manner (in contrast with the SDK-provided ListView which scrolls vertically). 4 | 5 | ## Usage 6 | To use in an XML layout: 7 | 0. Include The Library into your project 8 | 0. Make sure you are running ADT version 17 or greater 9 | 0. Add this XML namespace to your layout `xmlns:widget="http://schemas.android.com/apk/res-auto"` 10 | 0. Create the HorizontalListView as `com.meetme.android.horizontallistview.HorizontalListView` 11 | 12 | **Example**: 13 | 14 | 22 | 23 | Notice you set the `dividerWidth` via the XML namespace you just defined as it is a custom attribute. All other attributes can only be set normally via the `android` namespace. 24 | 25 | ## Known Issues 26 | - Currently this widget only supports uniform width items. When the item width is not uniform it leads to the UI rendering in inconsistent corrupted states. 27 | 28 | ## Known limitations 29 | - Does not support trackball/d-pad navigation 30 | - Does not support scroll bars 31 | - Does not support header or footer views 32 | - Does not support disabled items 33 | 34 | ## Contributors 35 | 36 | - [Bill Donahue](https://github.com/bdonahue) 37 | 38 | ## Licenses 39 | 40 | This library licensed under the MIT license. This library makes use of code originally developed and licensed by [Paul Soucy](paul@dev-smart.com). 41 | 42 | The MIT License Copyright (c) 2011 Paul Soucy (paul@dev-smart.com) 43 | The MIT License Copyright (c) 2013 MeetMe, Inc. 44 | 45 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 46 | associated documentation files (the "Software"), to deal in the Software without restriction, 47 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 48 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 49 | furnished to do so, subject to the following conditions: 50 | 51 | The above copyright notice and this permission notice shall be included in all copies or 52 | substantial portions of the Software. 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 55 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 56 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 57 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 58 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 59 | --------------------------------------------------------------------------------