├── .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 | *
66 | * - Does not support keyboard navigation
67 | * - Does not support scroll bars
-
68 | *
- Does not support header or footer views
-
69 | *
- Does not support disabled items
-
70 | *
71 | *
72 | * Custom XML Parameters Supported:
73 | *
74 | *
75 | * - divider - The divider to use between items. This can be a color or a drawable. If a drawable is used
76 | * dividerWidth will automatically be set to the intrinsic width of the provided drawable, this can be overriden by providing a dividerWidth.
77 | * - dividerWidth - The width of the divider to be drawn.
78 | * - android:requiresFadingEdge - If horizontal fading edges are enabled this view will render them
79 | * - android:fadingEdgeLength - The length of the horizontal fading edges
80 | *
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------