(v);
526 | mCachedItemViews.addLast(ref);
527 | }
528 | }
529 |
530 | /**
531 | * Transforms MAC address in 00:11:22:33:44:55 format to byte array representation
532 | * @param MAC
533 | * @return
534 | */
535 | public static byte[] MACtobyteConverter(String MAC) {
536 | // first remove all ":" from MAC address
537 | MAC = MAC.replaceAll(":", "");
538 | Log.d("ComponentLibrary.ToolBox", "MACtobyteConverter input ="+MAC);
539 |
540 | // now convert to byte array
541 | int len = MAC.length();
542 | byte[] data = new byte[len / 2];
543 | for (int i = 0; i < len; i += 2) {
544 | data[i / 2] = (byte) ((Character.digit(MAC.charAt(i), 16) << 4)
545 | + Character.digit(MAC.charAt(i+1), 16));
546 | }
547 | return data;
548 | }
549 |
550 | /**
551 | * Transforms MAC address in 00:11:22:33:44:55 format to byte array representation
552 | * @param MAC
553 | * @return
554 | */
555 | public static byte[] IMEItobyteConverter(String IMEI) {
556 | // now convert to byte array
557 | long imeiInLong;
558 | try {
559 | imeiInLong = Long.parseLong(IMEI);
560 | }
561 | catch (NumberFormatException e) {
562 | Log.w(TAG, "Can't convert IMEI to byte, Illegal number format");
563 | return null;
564 | }
565 | byte[] data = ByteBuffer.allocate(8).putLong(imeiInLong).array();
566 | return data;
567 | }
568 |
569 | public static String formatISODate(String isoDate){
570 | SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",Locale.US);
571 | Date dtIn;
572 | try {
573 | dtIn = inFormat.parse(isoDate); //where dateString is a date in ISO-8601 format
574 | SimpleDateFormat outFormat = new SimpleDateFormat("dd.MM.yyyy",Locale.US);
575 | return outFormat.format(dtIn);
576 | } catch (ParseException e) {
577 | Log.e(TAG, "Parse date error",e);
578 | } catch (NullPointerException e) {
579 | Log.e(TAG, "Parse NullPointerException error",e);
580 | }
581 | return isoDate;
582 | }
583 | }
584 |
585 |
--------------------------------------------------------------------------------
/library/src/main/java/com/martinappl/components/general/Validate.java:
--------------------------------------------------------------------------------
1 | package com.martinappl.components.general;
2 |
3 | /*
4 | * Licensed to the Apache Software Foundation (ASF) under one or more
5 | * contributor license agreements. See the NOTICE file distributed with
6 | * this work for additional information regarding copyright ownership.
7 | * The ASF licenses this file to You under the Apache License, Version 2.0
8 | * (the "License"); you may not use this file except in compliance with
9 | * the License. You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | */
19 |
20 | import java.util.Collection;
21 | import java.util.Iterator;
22 | import java.util.Map;
23 |
24 | /**
25 | * This class assists in validating arguments.
26 | *
27 | * The class is based along the lines of JUnit. If an argument value is
28 | * deemed invalid, an IllegalArgumentException is thrown. For example:
29 | *
30 | *
31 | * Validate.isTrue( i > 0, "The value must be greater than zero: ", i);
32 | * Validate.notNull( surname, "The surname must not be null");
33 | *
34 | *
35 | * @author Apache Software Foundation
36 | * @author Ola Berg
37 | * @author Gary Gregory
38 | * @author Norm Deane
39 | * @since 2.0
40 | * @version $Id: Validate.java 1057051 2011-01-09 23:15:51Z sebb $
41 | */
42 | public class Validate {
43 | // Validate has no dependencies on other classes in Commons Lang at present
44 |
45 | /**
46 | * Constructor. This class should not normally be instantiated.
47 | */
48 | public Validate() {
49 | super();
50 | }
51 |
52 | // isTrue
53 | //---------------------------------------------------------------------------------
54 | /**
55 | * Validate that the argument condition is true; otherwise
56 | * throwing an exception with the specified message. This method is useful when
57 | * validating according to an arbitrary boolean expression, such as validating an
58 | * object or using your own custom validation expression.
59 | *
60 | * Validate.isTrue( myObject.isOk(), "The object is not OK: ", myObject);
61 | *
62 | * For performance reasons, the object value is passed as a separate parameter and
63 | * appended to the exception message only in the case of an error.
64 | *
65 | * @param expression the boolean expression to check
66 | * @param message the exception message if invalid
67 | * @param value the value to append to the message when invalid
68 | * @throws IllegalArgumentException if expression is false
69 | */
70 | public static void isTrue(boolean expression, String message, Object value) {
71 | if (expression == false) {
72 | throw new IllegalArgumentException(message + value);
73 | }
74 | }
75 |
76 | /**
77 | * Validate that the argument condition is true; otherwise
78 | * throwing an exception with the specified message. This method is useful when
79 | * validating according to an arbitrary boolean expression, such as validating a
80 | * primitive number or using your own custom validation expression.
81 | *
82 | * Validate.isTrue(i > 0.0, "The value must be greater than zero: ", i);
83 | *
84 | * For performance reasons, the long value is passed as a separate parameter and
85 | * appended to the exception message only in the case of an error.
86 | *
87 | * @param expression the boolean expression to check
88 | * @param message the exception message if invalid
89 | * @param value the value to append to the message when invalid
90 | * @throws IllegalArgumentException if expression is false
91 | */
92 | public static void isTrue(boolean expression, String message, long value) {
93 | if (expression == false) {
94 | throw new IllegalArgumentException(message + value);
95 | }
96 | }
97 |
98 | /**
99 | * Validate that the argument condition is true; otherwise
100 | * throwing an exception with the specified message. This method is useful when
101 | * validating according to an arbitrary boolean expression, such as validating a
102 | * primitive number or using your own custom validation expression.
103 | *
104 | * Validate.isTrue(d > 0.0, "The value must be greater than zero: ", d);
105 | *
106 | * For performance reasons, the double value is passed as a separate parameter and
107 | * appended to the exception message only in the case of an error.
108 | *
109 | * @param expression the boolean expression to check
110 | * @param message the exception message if invalid
111 | * @param value the value to append to the message when invalid
112 | * @throws IllegalArgumentException if expression is false
113 | */
114 | public static void isTrue(boolean expression, String message, double value) {
115 | if (expression == false) {
116 | throw new IllegalArgumentException(message + value);
117 | }
118 | }
119 |
120 | /**
121 | * Validate that the argument condition is true; otherwise
122 | * throwing an exception with the specified message. This method is useful when
123 | * validating according to an arbitrary boolean expression, such as validating a
124 | * primitive number or using your own custom validation expression.
125 | *
126 | *
127 | * Validate.isTrue( (i > 0), "The value must be greater than zero");
128 | * Validate.isTrue( myObject.isOk(), "The object is not OK");
129 | *
130 | *
131 | * @param expression the boolean expression to check
132 | * @param message the exception message if invalid
133 | * @throws IllegalArgumentException if expression is false
134 | */
135 | public static void isTrue(boolean expression, String message) {
136 | if (expression == false) {
137 | throw new IllegalArgumentException(message);
138 | }
139 | }
140 |
141 | /**
142 | * Validate that the argument condition is true; otherwise
143 | * throwing an exception. This method is useful when validating according
144 | * to an arbitrary boolean expression, such as validating a
145 | * primitive number or using your own custom validation expression.
146 | *
147 | *
148 | * Validate.isTrue(i > 0);
149 | * Validate.isTrue(myObject.isOk());
150 | *
151 | * The message of the exception is "The validated expression is
152 | * false".
153 | *
154 | * @param expression the boolean expression to check
155 | * @throws IllegalArgumentException if expression is false
156 | */
157 | public static void isTrue(boolean expression) {
158 | if (expression == false) {
159 | throw new IllegalArgumentException("The validated expression is false");
160 | }
161 | }
162 |
163 | // notNull
164 | //---------------------------------------------------------------------------------
165 |
166 | /**
167 | * Validate that the specified argument is not null;
168 | * otherwise throwing an exception.
169 | *
170 | *
Validate.notNull(myObject);
171 | *
172 | * The message of the exception is "The validated object is
173 | * null".
174 | *
175 | * @param object the object to check
176 | * @throws IllegalArgumentException if the object is null
177 | */
178 | public static void notNull(Object object) {
179 | notNull(object, "The validated object is null");
180 | }
181 |
182 | /**
183 | * Validate that the specified argument is not null;
184 | * otherwise throwing an exception with the specified message.
185 | *
186 | *
Validate.notNull(myObject, "The object must not be null");
187 | *
188 | * @param object the object to check
189 | * @param message the exception message if invalid
190 | */
191 | public static void notNull(Object object, String message) {
192 | if (object == null) {
193 | throw new IllegalArgumentException(message);
194 | }
195 | }
196 |
197 | // notEmpty array
198 | //---------------------------------------------------------------------------------
199 |
200 | /**
201 | * Validate that the specified argument array is neither null
202 | * nor a length of zero (no elements); otherwise throwing an exception
203 | * with the specified message.
204 | *
205 | *
Validate.notEmpty(myArray, "The array must not be empty");
206 | *
207 | * @param array the array to check
208 | * @param message the exception message if invalid
209 | * @throws IllegalArgumentException if the array is empty
210 | */
211 | public static void notEmpty(Object[] array, String message) {
212 | if (array == null || array.length == 0) {
213 | throw new IllegalArgumentException(message);
214 | }
215 | }
216 |
217 | /**
218 | * Validate that the specified argument array is neither null
219 | * nor a length of zero (no elements); otherwise throwing an exception.
220 | *
221 | *
Validate.notEmpty(myArray);
222 | *
223 | * The message in the exception is "The validated array is
224 | * empty".
225 | *
226 | * @param array the array to check
227 | * @throws IllegalArgumentException if the array is empty
228 | */
229 | public static void notEmpty(Object[] array) {
230 | notEmpty(array, "The validated array is empty");
231 | }
232 |
233 | // notEmpty collection
234 | //---------------------------------------------------------------------------------
235 |
236 | /**
237 | *
Validate that the specified argument collection is neither null
238 | * nor a size of zero (no elements); otherwise throwing an exception
239 | * with the specified message.
240 | *
241 | *
Validate.notEmpty(myCollection, "The collection must not be empty");
242 | *
243 | * @param collection the collection to check
244 | * @param message the exception message if invalid
245 | * @throws IllegalArgumentException if the collection is empty
246 | */
247 | public static void notEmpty(Collection collection, String message) {
248 | if (collection == null || collection.size() == 0) {
249 | throw new IllegalArgumentException(message);
250 | }
251 | }
252 |
253 | /**
254 | * Validate that the specified argument collection is neither null
255 | * nor a size of zero (no elements); otherwise throwing an exception.
256 | *
257 | *
Validate.notEmpty(myCollection);
258 | *
259 | * The message in the exception is "The validated collection is
260 | * empty".
261 | *
262 | * @param collection the collection to check
263 | * @throws IllegalArgumentException if the collection is empty
264 | */
265 | public static void notEmpty(Collection collection) {
266 | notEmpty(collection, "The validated collection is empty");
267 | }
268 |
269 | // notEmpty map
270 | //---------------------------------------------------------------------------------
271 |
272 | /**
273 | * Validate that the specified argument map is neither null
274 | * nor a size of zero (no elements); otherwise throwing an exception
275 | * with the specified message.
276 | *
277 | *
Validate.notEmpty(myMap, "The map must not be empty");
278 | *
279 | * @param map the map to check
280 | * @param message the exception message if invalid
281 | * @throws IllegalArgumentException if the map is empty
282 | */
283 | public static void notEmpty(Map map, String message) {
284 | if (map == null || map.size() == 0) {
285 | throw new IllegalArgumentException(message);
286 | }
287 | }
288 |
289 | /**
290 | * Validate that the specified argument map is neither null
291 | * nor a size of zero (no elements); otherwise throwing an exception.
292 | *
293 | *
Validate.notEmpty(myMap);
294 | *
295 | * The message in the exception is "The validated map is
296 | * empty".
297 | *
298 | * @param map the map to check
299 | * @throws IllegalArgumentException if the map is empty
300 | * @see #notEmpty(Map, String)
301 | */
302 | public static void notEmpty(Map map) {
303 | notEmpty(map, "The validated map is empty");
304 | }
305 |
306 | // notEmpty string
307 | //---------------------------------------------------------------------------------
308 |
309 | /**
310 | * Validate that the specified argument string is
311 | * neither null nor a length of zero (no characters);
312 | * otherwise throwing an exception with the specified message.
313 | *
314 | *
Validate.notEmpty(myString, "The string must not be empty");
315 | *
316 | * @param string the string to check
317 | * @param message the exception message if invalid
318 | * @throws IllegalArgumentException if the string is empty
319 | */
320 | public static void notEmpty(String string, String message) {
321 | if (string == null || string.length() == 0) {
322 | throw new IllegalArgumentException(message);
323 | }
324 | }
325 |
326 | /**
327 | * Validate that the specified argument string is
328 | * neither null nor a length of zero (no characters);
329 | * otherwise throwing an exception with the specified message.
330 | *
331 | *
Validate.notEmpty(myString);
332 | *
333 | * The message in the exception is "The validated
334 | * string is empty".
335 | *
336 | * @param string the string to check
337 | * @throws IllegalArgumentException if the string is empty
338 | */
339 | public static void notEmpty(String string) {
340 | notEmpty(string, "The validated string is empty");
341 | }
342 |
343 | // notNullElements array
344 | //---------------------------------------------------------------------------------
345 |
346 | /**
347 | * Validate that the specified argument array is neither
348 | * null nor contains any elements that are null;
349 | * otherwise throwing an exception with the specified message.
350 | *
351 | *
Validate.noNullElements(myArray, "The array contain null at position %d");
352 | *
353 | * If the array is null, then the message in the exception
354 | * is "The validated object is null".
355 | *
356 | * @param array the array to check
357 | * @param message the exception message if the collection has null elements
358 | * @throws IllegalArgumentException if the array is null or
359 | * an element in the array is null
360 | */
361 | public static void noNullElements(Object[] array, String message) {
362 | Validate.notNull(array);
363 | for (int i = 0; i < array.length; i++) {
364 | if (array[i] == null) {
365 | throw new IllegalArgumentException(message);
366 | }
367 | }
368 | }
369 |
370 | /**
371 | * Validate that the specified argument array is neither
372 | * null nor contains any elements that are null;
373 | * otherwise throwing an exception.
374 | *
375 | *
Validate.noNullElements(myArray);
376 | *
377 | * If the array is null, then the message in the exception
378 | * is "The validated object is null".
379 | *
380 | * If the array has a null element, then the message in the
381 | * exception is "The validated array contains null element at index:
382 | * " followed by the index.
383 | *
384 | * @param array the array to check
385 | * @throws IllegalArgumentException if the array is null or
386 | * an element in the array is null
387 | */
388 | public static void noNullElements(Object[] array) {
389 | Validate.notNull(array);
390 | for (int i = 0; i < array.length; i++) {
391 | if (array[i] == null) {
392 | throw new IllegalArgumentException("The validated array contains null element at index: " + i);
393 | }
394 | }
395 | }
396 |
397 | // notNullElements collection
398 | //---------------------------------------------------------------------------------
399 |
400 | /**
401 | * Validate that the specified argument collection is neither
402 | * null nor contains any elements that are null;
403 | * otherwise throwing an exception with the specified message.
404 | *
405 | *
Validate.noNullElements(myCollection, "The collection contains null elements");
406 | *
407 | * If the collection is null, then the message in the exception
408 | * is "The validated object is null".
409 | *
410 | *
411 | * @param collection the collection to check
412 | * @param message the exception message if the collection has
413 | * @throws IllegalArgumentException if the collection is null or
414 | * an element in the collection is null
415 | */
416 | public static void noNullElements(Collection collection, String message) {
417 | Validate.notNull(collection);
418 | for (Iterator it = collection.iterator(); it.hasNext();) {
419 | if (it.next() == null) {
420 | throw new IllegalArgumentException(message);
421 | }
422 | }
423 | }
424 |
425 | /**
426 | * Validate that the specified argument collection is neither
427 | * null nor contains any elements that are null;
428 | * otherwise throwing an exception.
429 | *
430 | *
Validate.noNullElements(myCollection);
431 | *
432 | * If the collection is null, then the message in the exception
433 | * is "The validated object is null".
434 | *
435 | * If the collection has a null element, then the message in the
436 | * exception is "The validated collection contains null element at index:
437 | * " followed by the index.
438 | *
439 | * @param collection the collection to check
440 | * @throws IllegalArgumentException if the collection is null or
441 | * an element in the collection is null
442 | */
443 | public static void noNullElements(Collection collection) {
444 | Validate.notNull(collection);
445 | int i = 0;
446 | for (Iterator it = collection.iterator(); it.hasNext(); i++) {
447 | if (it.next() == null) {
448 | throw new IllegalArgumentException("The validated collection contains null element at index: " + i);
449 | }
450 | }
451 | }
452 |
453 | /**
454 | * Validate an argument, throwing IllegalArgumentException
455 | * if the argument collection is null or has elements that
456 | * are not of type clazz or a subclass.
457 | *
458 | *
459 | * Validate.allElementsOfType(collection, String.class, "Collection has invalid elements");
460 | *
461 | *
462 | * @param collection the collection to check, not null
463 | * @param clazz the Class which the collection's elements are expected to be, not null
464 | * @param message the exception message if the Collection has elements not of type clazz
465 | * @since 2.1
466 | */
467 | public static void allElementsOfType(Collection collection, Class clazz, String message) {
468 | Validate.notNull(collection);
469 | Validate.notNull(clazz);
470 | for (Iterator it = collection.iterator(); it.hasNext(); ) {
471 | if (clazz.isInstance(it.next()) == false) {
472 | throw new IllegalArgumentException(message);
473 | }
474 | }
475 | }
476 |
477 | /**
478 | *
479 | * Validate an argument, throwing IllegalArgumentException if the argument collection is
480 | * null or has elements that are not of type clazz or a subclass.
481 | *
482 | *
483 | *
484 | * Validate.allElementsOfType(collection, String.class);
485 | *
486 | *
487 | *
488 | * The message in the exception is 'The validated collection contains an element not of type clazz at index: '.
489 | *
490 | *
491 | * @param collection the collection to check, not null
492 | * @param clazz the Class which the collection's elements are expected to be, not null
493 | * @since 2.1
494 | */
495 | public static void allElementsOfType(Collection collection, Class clazz) {
496 | Validate.notNull(collection);
497 | Validate.notNull(clazz);
498 | int i = 0;
499 | for (Iterator it = collection.iterator(); it.hasNext(); i++) {
500 | if (clazz.isInstance(it.next()) == false) {
501 | throw new IllegalArgumentException("The validated collection contains an element not of type "
502 | + clazz.getName() + " at index: " + i);
503 | }
504 | }
505 | }
506 |
507 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/martinappl/components/ui/containers/HorizontalList.java:
--------------------------------------------------------------------------------
1 | package com.martinappl.components.ui.containers;
2 |
3 |
4 | import android.content.Context;
5 | import android.database.DataSetObserver;
6 | import android.graphics.Point;
7 | import android.graphics.Rect;
8 | import android.util.AttributeSet;
9 | import android.view.MotionEvent;
10 | import android.view.VelocityTracker;
11 | import android.view.View;
12 | import android.view.ViewConfiguration;
13 | import android.view.ViewGroup;
14 | import android.widget.Adapter;
15 | import android.widget.Scroller;
16 |
17 | import com.martinappl.components.general.ToolBox;
18 | import com.martinappl.components.ui.containers.interfaces.IViewObserver;
19 |
20 | public class HorizontalList extends ViewGroup {
21 | protected final int NO_VALUE = -11;
22 |
23 | /** User is not touching the list */
24 | protected static final int TOUCH_STATE_RESTING = 0;
25 |
26 | /** User is scrolling the list */
27 | protected static final int TOUCH_STATE_SCROLLING = 1;
28 |
29 | /** Fling gesture in progress */
30 | protected static final int TOUCH_STATE_FLING = 2;
31 |
32 | /** Children added with this layout mode will be added after the last child */
33 | protected static final int LAYOUT_MODE_AFTER = 0;
34 |
35 | /** Children added with this layout mode will be added before the first child */
36 | protected static final int LAYOUT_MODE_TO_BEFORE = 1;
37 |
38 | protected int mFirstItemPosition;
39 | protected int mLastItemPosition;
40 | protected boolean isScrollingDisabled = false;
41 |
42 | protected Adapter mAdapter;
43 | protected final ToolBox.ViewCache mCache = new ToolBox.ViewCache();
44 | private final Scroller mScroller = new Scroller(getContext());
45 | protected int mTouchSlop;
46 | private int mMinimumVelocity;
47 | private int mMaximumVelocity;
48 |
49 | private int mTouchState = TOUCH_STATE_RESTING;
50 | private float mLastMotionX;
51 | private final Point mDown = new Point();
52 | private VelocityTracker mVelocityTracker;
53 | private boolean mHandleSelectionOnActionUp = false;
54 |
55 | protected int mRightEdge = NO_VALUE;
56 | private int mDefaultItemWidth = 200;
57 |
58 | protected IViewObserver mViewObserver;
59 |
60 | //listeners
61 | private OnItemClickListener mItemClickListener;
62 |
63 | private final DataSetObserver mDataObserver = new DataSetObserver() {
64 |
65 | @Override
66 | public void onChanged() {
67 | reset();
68 | invalidate();
69 | }
70 |
71 | @Override
72 | public void onInvalidated() {
73 | removeAllViews();
74 | invalidate();
75 | }
76 |
77 | };
78 |
79 | /**
80 | * Remove all data, reset to initial state and attempt to refill
81 | * Position of first item on screen in Adapter data set is maintained
82 | */
83 | private void reset() {
84 | int scroll = getScrollX();
85 |
86 | int left = 0;
87 | if(getChildCount() != 0){
88 | left = getChildAt(0).getLeft() - ((MarginLayoutParams)getChildAt(0).getLayoutParams()).leftMargin;
89 | }
90 |
91 | removeAllViewsInLayout();
92 | mLastItemPosition = mFirstItemPosition;
93 | mRightEdge = NO_VALUE;
94 | scrollTo(left, 0);
95 |
96 | final int leftScreenEdge = getScrollX();
97 | int rightScreenEdge = leftScreenEdge + getWidth();
98 |
99 | refillLeftToRight(leftScreenEdge, rightScreenEdge);
100 | refillRightToLeft(leftScreenEdge);
101 |
102 | scrollTo(scroll, 0);
103 | }
104 |
105 | public HorizontalList(Context context) {
106 | this(context, null);
107 | }
108 |
109 | public HorizontalList(Context context, AttributeSet attrs) {
110 | this(context, attrs,0);
111 | }
112 |
113 | public HorizontalList(Context context, AttributeSet attrs, int defStyle) {
114 | super(context, attrs, defStyle);
115 |
116 | final ViewConfiguration configuration = ViewConfiguration.get(context);
117 | mTouchSlop = configuration.getScaledTouchSlop();
118 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
119 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
120 | }
121 |
122 | public interface OnItemClickListener{
123 | void onItemClick(View v);
124 | }
125 |
126 | @Override
127 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
128 | refill();
129 | }
130 |
131 | /**
132 | * Checks and refills empty area on the left
133 | * @return firstItemPosition
134 | */
135 | protected void refillRightToLeft(final int leftScreenEdge){
136 | if(getChildCount() == 0) return;
137 |
138 | View child = getChildAt(0);
139 | int childLeft = child.getLeft();
140 | int lastLeft = childLeft - ((MarginLayoutParams)child.getLayoutParams()).leftMargin;
141 |
142 | while(lastLeft > leftScreenEdge && mFirstItemPosition > 0){
143 | mFirstItemPosition--;
144 |
145 | child = mAdapter.getView(mFirstItemPosition, mCache.getCachedView(), this);
146 | sanitizeLayoutParams(child);
147 |
148 | addAndMeasureChild(child, LAYOUT_MODE_TO_BEFORE);
149 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
150 | lastLeft = layoutChildToBefore(child, lastLeft, lp);
151 | childLeft = child.getLeft() - ((MarginLayoutParams)child.getLayoutParams()).leftMargin;
152 |
153 | }
154 | return;
155 | }
156 |
157 | /**
158 | * Checks and refills empty area on the right
159 | */
160 | protected void refillLeftToRight(final int leftScreenEdge, final int rightScreenEdge){
161 |
162 | View child;
163 | int lastRight;
164 | if(getChildCount() != 0){
165 | child = getChildAt(getChildCount() - 1);
166 | lastRight = child.getRight() + ((MarginLayoutParams)child.getLayoutParams()).rightMargin;
167 | }
168 | else{
169 | lastRight = leftScreenEdge;
170 | if(mLastItemPosition == mFirstItemPosition) mLastItemPosition--;
171 | }
172 |
173 | while(lastRight < rightScreenEdge && mLastItemPosition < mAdapter.getCount()-1){
174 | mLastItemPosition++;
175 |
176 | child = mAdapter.getView(mLastItemPosition, mCache.getCachedView(), this);
177 | sanitizeLayoutParams(child);
178 |
179 | addAndMeasureChild(child, LAYOUT_MODE_AFTER);
180 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
181 | lastRight = layoutChild(child, lastRight, lp);
182 |
183 | if(mLastItemPosition >= mAdapter.getCount()-1) {
184 | mRightEdge = lastRight;
185 | }
186 | }
187 | }
188 |
189 |
190 | /**
191 | * Remove non visible views from left edge of screen
192 | */
193 | protected void removeNonVisibleViewsLeftToRight(final int leftScreenEdge){
194 | if(getChildCount() == 0) return;
195 |
196 | // check if we should remove any views in the left
197 | View firstChild = getChildAt(0);
198 |
199 | while (firstChild != null && firstChild.getRight() + ((MarginLayoutParams)firstChild.getLayoutParams()).rightMargin < leftScreenEdge) {
200 |
201 | // remove view
202 | removeViewsInLayout(0, 1);
203 |
204 | if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(firstChild, mFirstItemPosition);
205 | mCache.cacheView(firstChild);
206 |
207 | mFirstItemPosition++;
208 | if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0;
209 |
210 | // Continue to check the next child only if we have more than
211 | // one child left
212 | if (getChildCount() > 1) {
213 | firstChild = getChildAt(0);
214 | } else {
215 | firstChild = null;
216 | }
217 | }
218 |
219 | }
220 |
221 | /**
222 | * Remove non visible views from right edge of screen
223 | */
224 | protected void removeNonVisibleViewsRightToLeft(final int rightScreenEdge){
225 | if(getChildCount() == 0) return;
226 |
227 | // check if we should remove any views in the right
228 | View lastChild = getChildAt(getChildCount() - 1);
229 | while (lastChild != null && lastChild.getLeft() - ((MarginLayoutParams)lastChild.getLayoutParams()).leftMargin > rightScreenEdge) {
230 | // remove the right view
231 | removeViewsInLayout(getChildCount() - 1, 1);
232 |
233 | if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(lastChild, mLastItemPosition);
234 | mCache.cacheView(lastChild);
235 |
236 | mLastItemPosition--;
237 | if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1;
238 |
239 | // Continue to check the next child only if we have more than
240 | // one child left
241 | if (getChildCount() > 1) {
242 | lastChild = getChildAt(getChildCount() - 1);
243 | } else {
244 | lastChild = null;
245 | }
246 | }
247 |
248 | }
249 |
250 | protected void refill(){
251 | if(mAdapter == null) return;
252 |
253 | final int leftScreenEdge = getScrollX();
254 | int rightScreenEdge = leftScreenEdge + getWidth();
255 |
256 | removeNonVisibleViewsLeftToRight(leftScreenEdge);
257 | removeNonVisibleViewsRightToLeft(rightScreenEdge);
258 |
259 | refillLeftToRight(leftScreenEdge, rightScreenEdge);
260 | refillRightToLeft(leftScreenEdge);
261 | }
262 |
263 |
264 |
265 | protected void sanitizeLayoutParams(View child){
266 | MarginLayoutParams lp;
267 | if(child.getLayoutParams() instanceof MarginLayoutParams) lp = (MarginLayoutParams) child.getLayoutParams();
268 | else if(child.getLayoutParams() != null) lp = new MarginLayoutParams(child.getLayoutParams());
269 | else lp = new MarginLayoutParams(mDefaultItemWidth,getHeight());
270 |
271 | if(lp.height == LayoutParams.MATCH_PARENT) lp.height = getHeight();
272 | if(lp.width == LayoutParams.MATCH_PARENT) lp.width = getWidth();
273 |
274 | if(lp.height == LayoutParams.WRAP_CONTENT){
275 | measureUnspecified(child);
276 | lp.height = child.getMeasuredHeight();
277 | }
278 | if(lp.width == LayoutParams.WRAP_CONTENT){
279 | measureUnspecified(child);
280 | lp.width = child.getMeasuredWidth();
281 | }
282 | child.setLayoutParams(lp);
283 | }
284 |
285 | private void measureUnspecified(View child){
286 | final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED);
287 | final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED);
288 | measureChild(child, pwms, phms);
289 | }
290 |
291 | /**
292 | * Adds a view as a child view and takes care of measuring it
293 | *
294 | * @param child The view to add
295 | * @param layoutMode Either LAYOUT_MODE_LEFT or LAYOUT_MODE_RIGHT
296 | * @return child which was actually added to container, subclasses can override to introduce frame views
297 | */
298 | protected View addAndMeasureChild(final View child, final int layoutMode) {
299 | if(child.getLayoutParams() == null) child.setLayoutParams(new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
300 |
301 | final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1;
302 | addViewInLayout(child, index, child.getLayoutParams(), true);
303 |
304 | final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY);
305 | final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY);
306 | measureChild(child, pwms, phms);
307 | child.setDrawingCacheEnabled(false);
308 |
309 | return child;
310 | }
311 |
312 | /**
313 | * Layout children from right to left
314 | */
315 | protected int layoutChildToBefore(View v, int right , MarginLayoutParams lp){
316 | final int left = right - v.getMeasuredWidth() - lp.leftMargin - lp.rightMargin;
317 | layoutChild(v, left, lp);
318 | return left;
319 | }
320 |
321 | /**
322 | * @param topline Y coordinate of topline
323 | * @param left X coordinate where should we start layout
324 | */
325 | protected int layoutChild(View v, int left, MarginLayoutParams lp){
326 | int l,t,r,b;
327 | l = left + lp.leftMargin;
328 | t = lp.topMargin;
329 | r = l + v.getMeasuredWidth();
330 | b = t + v.getMeasuredHeight();
331 |
332 | v.layout(l, t, r, b);
333 | return r + lp.rightMargin;
334 | }
335 |
336 |
337 | @Override
338 | public boolean onInterceptTouchEvent(MotionEvent ev) {
339 |
340 | /*
341 | * This method JUST determines whether we want to intercept the motion.
342 | * If we return true, onTouchEvent will be called and we do the actual
343 | * scrolling there.
344 | */
345 |
346 |
347 | /*
348 | * Shortcut the most recurring case: the user is in the dragging
349 | * state and he is moving his finger. We want to intercept this
350 | * motion.
351 | */
352 | final int action = ev.getAction();
353 | if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) {
354 | return true;
355 | }
356 |
357 | final float x = ev.getX();
358 | final float y = ev.getY();
359 | switch (action) {
360 | case MotionEvent.ACTION_MOVE:
361 | /*
362 | * not dragging, otherwise the shortcut would have caught it. Check
363 | * whether the user has moved far enough from his original down touch.
364 | */
365 |
366 | /*
367 | * Locally do absolute value. mLastMotionX is set to the x value
368 | * of the down event.
369 | */
370 | final int xDiff = (int) Math.abs(x - mLastMotionX);
371 |
372 | final int touchSlop = mTouchSlop;
373 | final boolean xMoved = xDiff > touchSlop;
374 |
375 | if (xMoved) {
376 | // Scroll if the user moved far enough along the axis
377 | mTouchState = TOUCH_STATE_SCROLLING;
378 | mHandleSelectionOnActionUp = false;
379 | enableChildrenCache();
380 | cancelLongPress();
381 | }
382 |
383 | break;
384 |
385 | case MotionEvent.ACTION_DOWN:
386 | // Remember location of down touch
387 | mLastMotionX = x;
388 |
389 | mDown.x = (int) x;
390 | mDown.y = (int) y;
391 |
392 | /*
393 | * If being flinged and user touches the screen, initiate drag;
394 | * otherwise don't. mScroller.isFinished should be false when
395 | * being flinged.
396 | */
397 | mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING;
398 | //if he had normal click in rested state, remember for action up check
399 | if(mTouchState == TOUCH_STATE_RESTING){
400 | mHandleSelectionOnActionUp = true;
401 | }
402 | break;
403 |
404 | case MotionEvent.ACTION_CANCEL:
405 | mDown.x = -1;
406 | mDown.y = -1;
407 | break;
408 | case MotionEvent.ACTION_UP:
409 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates
410 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){
411 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y);
412 | if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown);
413 | }
414 | // Release the drag
415 | mHandleSelectionOnActionUp = false;
416 | mDown.x = -1;
417 | mDown.y = -1;
418 |
419 | mTouchState = TOUCH_STATE_RESTING;
420 | clearChildrenCache();
421 | break;
422 | }
423 |
424 | return mTouchState == TOUCH_STATE_SCROLLING;
425 |
426 | }
427 |
428 | @Override
429 | public boolean onTouchEvent(MotionEvent event) {
430 | if (mVelocityTracker == null) {
431 | mVelocityTracker = VelocityTracker.obtain();
432 | }
433 | mVelocityTracker.addMovement(event);
434 |
435 | final int action = event.getAction();
436 | final float x = event.getX();
437 | final float y = event.getY();
438 |
439 | switch (action) {
440 | case MotionEvent.ACTION_DOWN:
441 | /*
442 | * If being flinged and user touches, stop the fling. isFinished
443 | * will be false if being flinged.
444 | */
445 | if (!mScroller.isFinished()) {
446 | mScroller.forceFinished(true);
447 | }
448 |
449 | // Remember where the motion event started
450 | mLastMotionX = x;
451 |
452 | break;
453 | case MotionEvent.ACTION_MOVE:
454 |
455 | if (mTouchState == TOUCH_STATE_SCROLLING) {
456 | // Scroll to follow the motion event
457 | final int deltaX = (int) (mLastMotionX - x);
458 | mLastMotionX = x;
459 |
460 | scrollByDelta(deltaX);
461 | }
462 | else{
463 | final int xDiff = (int) Math.abs(x - mLastMotionX);
464 |
465 | final int touchSlop = mTouchSlop;
466 | final boolean xMoved = xDiff > touchSlop;
467 |
468 |
469 | if (xMoved) {
470 | // Scroll if the user moved far enough along the axis
471 | mTouchState = TOUCH_STATE_SCROLLING;
472 | enableChildrenCache();
473 | cancelLongPress();
474 | }
475 | }
476 | break;
477 | case MotionEvent.ACTION_UP:
478 |
479 | //this must be here, in case no child view returns true,
480 | //events will propagate back here and on intercept touch event wont be called again
481 | //in case of no parent it propagates here, in case of parent it usually propagates to on cancel
482 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){
483 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y);
484 | if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown);
485 | mHandleSelectionOnActionUp = false;
486 | }
487 |
488 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates
489 | if (mTouchState == TOUCH_STATE_SCROLLING) {
490 |
491 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
492 | int initialXVelocity = (int) mVelocityTracker.getXVelocity();
493 | int initialYVelocity = (int) mVelocityTracker.getYVelocity();
494 |
495 | if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) {
496 | fling(-initialXVelocity, -initialYVelocity);
497 | }
498 | else{
499 | // Release the drag
500 | clearChildrenCache();
501 | mTouchState = TOUCH_STATE_RESTING;
502 |
503 | mDown.x = -1;
504 | mDown.y = -1;
505 | }
506 |
507 | if (mVelocityTracker != null) {
508 | mVelocityTracker.recycle();
509 | mVelocityTracker = null;
510 | }
511 |
512 | break;
513 | }
514 |
515 | // Release the drag
516 | clearChildrenCache();
517 | mTouchState = TOUCH_STATE_RESTING;
518 |
519 | mDown.x = -1;
520 | mDown.y = -1;
521 |
522 | break;
523 | case MotionEvent.ACTION_CANCEL:
524 | mTouchState = TOUCH_STATE_RESTING;
525 | }
526 |
527 | return true;
528 | }
529 |
530 | @Override
531 | public void computeScroll() {
532 | if(mRightEdge != NO_VALUE && mScroller.getFinalX() > mRightEdge - getWidth() + 1){
533 | mScroller.setFinalX(mRightEdge - getWidth() + 1);
534 | }
535 |
536 | if(mRightEdge != NO_VALUE && getScrollX() > mRightEdge - getWidth()) {
537 | if(mRightEdge - getWidth() > 0) scrollTo(mRightEdge - getWidth(), 0);
538 | else scrollTo(0, 0);
539 | return;
540 | }
541 |
542 | if (mScroller.computeScrollOffset()) {
543 | if(mScroller.getFinalX() == mScroller.getCurrX()){
544 | mScroller.abortAnimation();
545 | mTouchState = TOUCH_STATE_RESTING;
546 | clearChildrenCache();
547 | }
548 | else{
549 | final int x = mScroller.getCurrX();
550 | scrollTo(x, 0);
551 |
552 | postInvalidate();
553 | }
554 | }
555 | else if(mTouchState == TOUCH_STATE_FLING){
556 | mTouchState = TOUCH_STATE_RESTING;
557 | clearChildrenCache();
558 | }
559 |
560 | refill();
561 | }
562 |
563 | public void fling(int velocityX, int velocityY){
564 | if(isScrollingDisabled) return;
565 |
566 | mTouchState = TOUCH_STATE_FLING;
567 | final int x = getScrollX();
568 | final int y = getScrollY();
569 |
570 | final int rightInPixels;
571 | if(mRightEdge == NO_VALUE) rightInPixels = Integer.MAX_VALUE;
572 | else rightInPixels = mRightEdge;
573 |
574 | mScroller.fling(x, y, velocityX, velocityY, 0,rightInPixels - getWidth() + 1,0,0);
575 |
576 | invalidate();
577 | }
578 |
579 | protected void scrollByDelta(int deltaX){
580 | if(isScrollingDisabled) return;
581 |
582 | final int rightInPixels;
583 | if(mRightEdge == NO_VALUE) rightInPixels = Integer.MAX_VALUE;
584 | else {
585 | rightInPixels = mRightEdge;
586 | if(getScrollX() > mRightEdge - getWidth()) {
587 | if(mRightEdge - getWidth() > 0) scrollTo(mRightEdge - getWidth(), 0);
588 | else scrollTo(0, 0);
589 | return;
590 | }
591 | }
592 |
593 | final int x = getScrollX() + deltaX;
594 |
595 | if(x < 0 ) deltaX -= x;
596 | else if(x > rightInPixels - getWidth()) deltaX -= x - (rightInPixels - getWidth());
597 |
598 | scrollBy(deltaX, 0);
599 | }
600 |
601 | protected void handleClick(Point p){
602 | final int c = getChildCount();
603 | View v;
604 | final Rect r = new Rect();
605 | for(int i=0; i < c; i++){
606 | v = getChildAt(i);
607 | v.getHitRect(r);
608 | if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){
609 | if(mItemClickListener != null) mItemClickListener.onItemClick(v);
610 | }
611 | }
612 | }
613 |
614 |
615 | public void setAdapter(Adapter adapter) {
616 | if(mAdapter != null) {
617 | mAdapter.unregisterDataSetObserver(mDataObserver);
618 | }
619 | mAdapter = adapter;
620 | mAdapter.registerDataSetObserver(mDataObserver);
621 | reset();
622 | }
623 |
624 | private void enableChildrenCache() {
625 | setChildrenDrawnWithCacheEnabled(true);
626 | setChildrenDrawingCacheEnabled(true);
627 | }
628 |
629 | private void clearChildrenCache() {
630 | setChildrenDrawnWithCacheEnabled(false);
631 | }
632 |
633 | @Override
634 | protected MarginLayoutParams generateDefaultLayoutParams() {
635 | return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
636 | }
637 |
638 | @Override
639 | protected MarginLayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
640 | return new MarginLayoutParams(p);
641 | }
642 |
643 | @Override
644 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
645 | return p instanceof MarginLayoutParams;
646 | }
647 |
648 | public void setDefaultItemWidth(int width){
649 | //MTODO add xml attributes
650 | mDefaultItemWidth = width;
651 | }
652 |
653 | /**
654 | * Set listener which will fire if item in container is clicked
655 | */
656 | public void setOnItemClickListener(OnItemClickListener itemClickListener) {
657 | this.mItemClickListener = itemClickListener;
658 | }
659 |
660 | public void setViewObserver(IViewObserver viewObserver) {
661 | this.mViewObserver = viewObserver;
662 | }
663 |
664 | }
665 |
--------------------------------------------------------------------------------
/library/src/main/java/com/martinappl/components/ui/containers/contentbands/BasicContentBand.java:
--------------------------------------------------------------------------------
1 | package com.martinappl.components.ui.containers.contentbands;
2 |
3 | import java.lang.ref.WeakReference;
4 | import java.util.ArrayList;
5 | import java.util.Arrays;
6 | import java.util.Collections;
7 | import java.util.Comparator;
8 | import java.util.LinkedList;
9 | import java.util.List;
10 |
11 | import android.content.Context;
12 | import android.content.res.TypedArray;
13 | import android.graphics.Point;
14 | import android.graphics.Rect;
15 | import android.util.AttributeSet;
16 | import android.view.MotionEvent;
17 | import android.view.VelocityTracker;
18 | import android.view.View;
19 | import android.view.ViewConfiguration;
20 | import android.view.ViewGroup;
21 | import android.widget.Scroller;
22 |
23 | import com.martinappl.components.R;
24 | import com.martinappl.components.general.ToolBox;
25 | import com.martinappl.components.general.Validate;
26 |
27 |
28 | /**
29 | * @author Martin Appl
30 | *
31 | * Horizontally scrollable container with boundaries on the ends, which places Views on coordinates specified
32 | * by tile objects. Data binding is specified by adapter interface. Use abstract adapter which has already implemented
33 | * algorithms for searching views in requested ranges. You only need to implement getViewForTile method where you map
34 | * Tile objects from dataset to corresponding View objects, which get displayed. Position on screen is described by LayoutParams object.
35 | * Method getLayoutParamsForTile helps generate layout params from data objects. If you don't set Layout params in getViewForTile, this
36 | * methods is called automatically afterwards.
37 | *
38 | * DSP = device specific pixel
39 | */
40 | public class BasicContentBand extends ViewGroup {
41 | //CONSTANTS
42 | // private static final String LOG_TAG = "Basic_ContentBand_Component";
43 | private static final int NO_VALUE = -11;
44 | private static final int DSP_DEFAULT = 10;
45 |
46 | /** User is not touching the list */
47 | protected static final int TOUCH_STATE_RESTING = 0;
48 |
49 | /** User is scrolling the list */
50 | protected static final int TOUCH_STATE_SCROLLING = 1;
51 |
52 | /** Fling gesture in progress */
53 | protected static final int TOUCH_STATE_FLING = 2;
54 |
55 | /**
56 | * In this mode we have pixel size of DSP specified, if dspHeight is bigger than window, content band can be scrolled vertically.
57 | */
58 | public static final int GRID_MODE_FIXED_SIZE = 0;
59 | /**
60 | * In this mode is pixel size of DSP calculated dynamically, based on widget height in pixels and value of dspHeight which is fixed
61 | * and taken from adapters getBottom method
62 | */
63 | public static final int GRID_MODE_DYNAMIC_SIZE = 1;
64 |
65 | //to which direction on X axis are window coordinates sliding
66 | protected static final int DIRECTION_RIGHT = 0;
67 | protected static final int DIRECTION_LEFT = 1;
68 |
69 |
70 | //VARIABLES
71 | protected Adapter mAdapter;
72 | private int mGridMode = GRID_MODE_DYNAMIC_SIZE;
73 | /**How many normal pixels corresponds to one DSP pixel*/
74 | private int mDspPixelRatio = DSP_DEFAULT;
75 | private int mDspHeight = NO_VALUE;
76 | protected int mDspHeightModulo;
77 | //refilling
78 | protected int mCurrentlyLayoutedViewsLeftEdgeDsp;
79 | protected int mCurrentlyLayoutedViewsRightEdgeDsp;
80 | private final ArrayList mTempViewArray = new ArrayList();
81 | //touch, scrolling
82 | protected int mTouchState = TOUCH_STATE_RESTING;
83 | private float mLastMotionX;
84 | private float mLastMotionY;
85 | private final Point mDown = new Point();
86 | private VelocityTracker mVelocityTracker;
87 | protected final Scroller mScroller;
88 | private boolean mHandleSelectionOnActionUp = false;
89 | protected int mScrollDirection = NO_VALUE;
90 | //constant values
91 | private final int mTouchSlop;
92 | private final int mMinimumVelocity;
93 | private final int mMaximumVelocity;
94 | // private final Rect mTempRect = new Rect();
95 |
96 | private boolean mIsZOrderEnabled;
97 | private int[] mDrawingOrderArray;
98 |
99 | //listeners
100 | private OnItemClickListener mItemClickListener;
101 |
102 | public BasicContentBand(Context context, AttributeSet attrs, int defStyle) {
103 | super(context, attrs, defStyle);
104 |
105 | final ViewConfiguration configuration = ViewConfiguration.get(context);
106 | mTouchSlop = configuration.getScaledTouchSlop();
107 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
108 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
109 | mScroller = new Scroller(context);
110 |
111 | if(attrs != null){
112 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BasicContentBand, defStyle, 0);
113 |
114 | mDspPixelRatio = a.getInteger(R.styleable.BasicContentBand_deviceSpecificPixelSize, mDspPixelRatio);
115 | mGridMode = a.getInteger(R.styleable.BasicContentBand_gridMode, mGridMode);
116 |
117 | a.recycle();
118 | }
119 |
120 |
121 | }
122 |
123 | public BasicContentBand(Context context, AttributeSet attrs) {
124 | this(context, attrs, 0);
125 | }
126 |
127 | public BasicContentBand(Context context) {
128 | this(context,null);
129 | }
130 |
131 | protected int dspToPx(int dsp){
132 | return dsp * mDspPixelRatio;
133 | }
134 |
135 | protected int pxToDsp(int px){
136 | return px / mDspPixelRatio;
137 | }
138 |
139 | @Override
140 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
141 | int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
142 | int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
143 | int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
144 | int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
145 |
146 | if(mAdapter != null){
147 | mDspHeight = mAdapter.getBottom();
148 | Validate.isTrue(mDspHeight > 0, "Adapter getBottom must return value greater than zero");
149 | }
150 | else{
151 | setMeasuredDimension(widthSpecSize, heightSpecSize);
152 | return;
153 | }
154 |
155 | int measuredWidth, measuredHeight;
156 | if(mGridMode == GRID_MODE_FIXED_SIZE){
157 | /*HEIGHT*/
158 | measuredHeight = mDspPixelRatio * mDspHeight;
159 |
160 | if(heightSpecMode == MeasureSpec.AT_MOST){
161 | if(measuredHeight > heightSpecSize) measuredHeight = heightSpecSize;
162 | }
163 | else if(heightSpecMode == MeasureSpec.EXACTLY){
164 | measuredHeight = heightSpecSize;
165 | }
166 |
167 | /*WIDTH*/
168 | measuredWidth = widthSpecSize;
169 | if(mAdapter != null) measuredWidth = mAdapter.getEnd() * mDspPixelRatio;
170 |
171 | if(widthSpecMode == MeasureSpec.AT_MOST){
172 | if(measuredWidth > widthSpecSize) measuredWidth = widthSpecSize;
173 | }
174 | else if(widthSpecMode == MeasureSpec.EXACTLY){
175 | measuredWidth = widthSpecSize;
176 | }
177 | }
178 | else{
179 | if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
180 | throw new RuntimeException("Can not have unspecified hight dimension in dynamic grid mode");
181 | }
182 | /*HEIGHT*/
183 | measuredHeight = heightSpecSize;
184 |
185 | mDspPixelRatio = measuredHeight / mDspHeight;
186 | mDspHeightModulo = measuredHeight % mDspHeight;
187 |
188 | measuredHeight = mDspPixelRatio * mDspHeight;
189 |
190 | if(heightSpecMode == MeasureSpec.AT_MOST){
191 | if(measuredHeight > heightSpecSize) measuredHeight = heightSpecSize;
192 | else mDspHeightModulo = 0;
193 | }
194 | else if(heightSpecMode == MeasureSpec.EXACTLY){
195 | measuredHeight = heightSpecSize;
196 | }
197 |
198 | /*WIDTH*/
199 | measuredWidth = widthSpecSize;
200 | if(mAdapter != null) measuredWidth = mAdapter.getEnd() * mDspPixelRatio;
201 |
202 | if(widthSpecMode == MeasureSpec.AT_MOST){
203 | if(measuredWidth > widthSpecSize) measuredWidth = widthSpecSize;
204 | }
205 | else if(widthSpecMode == MeasureSpec.EXACTLY){
206 | measuredWidth = widthSpecSize;
207 | }
208 |
209 | }
210 |
211 | setMeasuredDimension(measuredWidth, measuredHeight);
212 | }
213 |
214 | @Override
215 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
216 | int c = getChildCount();
217 |
218 | if(c == 0) {
219 | fillEmptyContainer();
220 | c = getChildCount();
221 | }
222 |
223 | for(int i=0; i= 0; j--){ //start at the end, because mostly we are searching for view which was added to end in previous iterations
253 | // if(((LayoutParams)getChildAt(j).getLayoutParams()).tileNumber == lp.tileNumber) {
254 | // arr[i] = null;
255 | // nullCounter++;
256 | // break;
257 | // }
258 | // }
259 | // }
260 | //
261 | // final View[] res = new View[arr.length - nullCounter];
262 | // for(int i=0,j=0; i comparator = new Comparator() {
280 | @Override
281 | public int compare(View lhs, View rhs) {
282 | final LayoutParams l = (LayoutParams) lhs.getLayoutParams();
283 | final LayoutParams r = (LayoutParams) rhs.getLayoutParams();
284 |
285 | if(l.z == r.z) return 0;
286 | else if(l.z < r.z) return -1;
287 | else return 1;
288 | }
289 | };
290 |
291 | Arrays.sort(tempArr, comparator);
292 | mDrawingOrderArray = new int[tempArr.length];
293 | for(int i=0; i dspMostRight) dspMostRight = lp.getDspRight();
325 | if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft;
326 | addViewInLayout(list[i], -1, list[i].getLayoutParams(), true);
327 | }
328 |
329 | if(mIsZOrderEnabled) rearrangeViewsAccordingZOrder();
330 |
331 | mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft;
332 | mCurrentlyLayoutedViewsRightEdgeDsp= dspMostRight;
333 | }
334 |
335 | /**
336 | * Checks and refills empty area on the left edge of screen
337 | */
338 | protected void refillLeftSide(){
339 | if(mAdapter == null) return;
340 |
341 | final int leftScreenEdge = getScrollX();
342 | final int dspLeftScreenEdge = pxToDsp(leftScreenEdge);
343 | final int dspNextViewsRight = mCurrentlyLayoutedViewsLeftEdgeDsp;
344 |
345 | if(dspLeftScreenEdge >= dspNextViewsRight) return;
346 | // Logger.d(LOG_TAG, "from " + dspLeftScreenEdge + ", to " + dspNextViewsRight);
347 |
348 | View[] list = mAdapter.getViewsByRightSideRange(dspLeftScreenEdge, dspNextViewsRight);
349 | // list = filterAlreadyPresentViews(list);
350 |
351 | int dspMostLeft = dspNextViewsRight;
352 | LayoutParams lp;
353 | for(int i=0; i < list.length; i++){
354 | lp = (LayoutParams) list[i].getLayoutParams();
355 | if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft;
356 | addViewInLayout(list[i], -1, list[i].getLayoutParams(), true);
357 | }
358 |
359 | if(list.length > 0){
360 | layoutNewChildren(list);
361 | }
362 |
363 | mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft;
364 | }
365 |
366 | /**
367 | * Checks and refills empty area on the right
368 | */
369 | protected void refillRightSide(){
370 | if(mAdapter == null) return;
371 |
372 | final int rightScreenEdge = getScrollX() + getWidth();
373 | final int dspNextAddedViewsLeft = mCurrentlyLayoutedViewsRightEdgeDsp;
374 |
375 | int dspRightScreenEdge = pxToDsp(rightScreenEdge) + 1;
376 | if(dspRightScreenEdge > mAdapter.getEnd()) dspRightScreenEdge = mAdapter.getEnd();
377 |
378 | if(dspNextAddedViewsLeft >= dspRightScreenEdge) return;
379 |
380 | View[] list = mAdapter.getViewsByLeftSideRange(dspNextAddedViewsLeft, dspRightScreenEdge);
381 | // list = filterAlreadyPresentViews(list);
382 |
383 | int dspMostRight = 0;
384 | LayoutParams lp;
385 | for(int i=0; i < list.length; i++){
386 | lp = (LayoutParams) list[i].getLayoutParams();
387 | if(lp.getDspRight() > dspMostRight) dspMostRight = lp.getDspRight();
388 | addViewInLayout(list[i], -1, list[i].getLayoutParams(), true);
389 | }
390 |
391 | if(list.length > 0){
392 | layoutNewChildren(list);
393 | }
394 |
395 | mCurrentlyLayoutedViewsRightEdgeDsp = dspMostRight;
396 | }
397 |
398 | /**
399 | * Remove non visible views laid out of the screen
400 | */
401 | private void removeNonVisibleViews(){
402 | if(getChildCount() == 0) return;
403 |
404 | final int leftScreenEdge = getScrollX();
405 | final int rightScreenEdge = leftScreenEdge + getWidth();
406 |
407 | int dspRightScreenEdge = pxToDsp(rightScreenEdge);
408 | if(dspRightScreenEdge >= 0) dspRightScreenEdge++; //to avoid problem with rounding of values
409 |
410 | int dspLeftScreenEdge = pxToDsp(leftScreenEdge);
411 | if(dspLeftScreenEdge <= 0) dspLeftScreenEdge--; //when values are <0 they get floored to value which is larger
412 |
413 | mTempViewArray.clear();
414 | View v;
415 | for(int i=0; i dspMostRight) dspMostRight = lp.getDspRight();
433 | if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft;
434 | }
435 |
436 | mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft;
437 | mCurrentlyLayoutedViewsRightEdgeDsp = dspMostRight;
438 |
439 | }
440 |
441 | //check if View with specified LayoutParams is currently on screen
442 | private boolean isOnScreen(LayoutParams lp, int dspLeftScreenEdge, int dspRightScreenEdge){
443 | final int left = lp.dspLeft;
444 | final int right = left + lp.dspWidth;
445 |
446 | if(right > dspLeftScreenEdge && left < dspRightScreenEdge) return true;
447 | else return false;
448 | }
449 |
450 | @Override
451 | public boolean onInterceptTouchEvent(MotionEvent ev) {
452 |
453 | /*
454 | * This method JUST determines whether we want to intercept the motion.
455 | * If we return true, onTouchEvent will be called and we do the actual
456 | * scrolling there.
457 | */
458 |
459 |
460 | /*
461 | * Shortcut the most recurring case: the user is in the dragging
462 | * state and he is moving his finger. We want to intercept this
463 | * motion.
464 | */
465 | final int action = ev.getAction();
466 | if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) {
467 | return true;
468 | }
469 |
470 | final float x = ev.getX();
471 | final float y = ev.getY();
472 | switch (action) {
473 | case MotionEvent.ACTION_MOVE:
474 | /*
475 | * not dragging, otherwise the shortcut would have caught it. Check
476 | * whether the user has moved far enough from his original down touch.
477 | */
478 |
479 | /*
480 | * Locally do absolute value. mLastMotionX is set to the x value
481 | * of the down event.
482 | */
483 | final int xDiff = (int) Math.abs(x - mLastMotionX);
484 | final int yDiff = (int) Math.abs(y - mLastMotionY);
485 |
486 | final int touchSlop = mTouchSlop;
487 | final boolean xMoved = xDiff > touchSlop;
488 | final boolean yMoved = yDiff > touchSlop;
489 |
490 |
491 | if (xMoved || yMoved) {
492 | // Scroll if the user moved far enough along the axis
493 | mTouchState = TOUCH_STATE_SCROLLING;
494 | mHandleSelectionOnActionUp = false;
495 | enableChildrenCache();
496 | cancelLongPress();
497 | }
498 |
499 | break;
500 |
501 | case MotionEvent.ACTION_DOWN:
502 | // Remember location of down touch
503 | mLastMotionX = x;
504 |
505 | mDown.x = (int) x;
506 | mDown.y = (int) y;
507 |
508 | /*
509 | * If being flinged and user touches the screen, initiate drag;
510 | * otherwise don't. mScroller.isFinished should be false when
511 | * being flinged.
512 | */
513 | mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING;
514 | //if he had normal click in rested state, remember for action up check
515 | if(mTouchState == TOUCH_STATE_RESTING){
516 | mHandleSelectionOnActionUp = true;
517 | }
518 | break;
519 |
520 | case MotionEvent.ACTION_CANCEL:
521 | mDown.x = -1;
522 | mDown.y = -1;
523 | break;
524 | case MotionEvent.ACTION_UP:
525 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates
526 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){
527 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y);
528 | if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown);
529 | }
530 | // Release the drag
531 | mHandleSelectionOnActionUp = false;
532 | mDown.x = -1;
533 | mDown.y = -1;
534 |
535 | mTouchState = TOUCH_STATE_RESTING;
536 | clearChildrenCache();
537 | break;
538 | }
539 |
540 | return mTouchState == TOUCH_STATE_SCROLLING;
541 |
542 | }
543 |
544 | @Override
545 | public boolean onTouchEvent(MotionEvent event) {
546 | if (mVelocityTracker == null) {
547 | mVelocityTracker = VelocityTracker.obtain();
548 | }
549 | mVelocityTracker.addMovement(event);
550 |
551 | final int action = event.getAction();
552 | final float x = event.getX();
553 | final float y = event.getY();
554 |
555 | switch (action) {
556 | case MotionEvent.ACTION_DOWN:
557 | /*
558 | * If being flinged and user touches, stop the fling. isFinished
559 | * will be false if being flinged.
560 | */
561 | if (!mScroller.isFinished()) {
562 | mScroller.forceFinished(true);
563 | }
564 |
565 | // Remember where the motion event started
566 | mLastMotionX = x;
567 | mLastMotionY = y;
568 |
569 | break;
570 | case MotionEvent.ACTION_MOVE:
571 |
572 | if (mTouchState == TOUCH_STATE_SCROLLING) {
573 | // Scroll to follow the motion event
574 | final int deltaX = (int) (mLastMotionX - x);
575 | final int deltaY = (int) (mLastMotionY - y);
576 | mLastMotionX = x;
577 | mLastMotionY = y;
578 |
579 | scrollByDelta(deltaX, deltaY);
580 | }
581 | else{
582 | final int xDiff = (int) Math.abs(x - mLastMotionX);
583 | final int yDiff = (int) Math.abs(y - mLastMotionY);
584 |
585 | final int touchSlop = mTouchSlop;
586 | final boolean xMoved = xDiff > touchSlop;
587 | final boolean yMoved = yDiff > touchSlop;
588 |
589 |
590 | if (xMoved || yMoved) {
591 | // Scroll if the user moved far enough along the axis
592 | mTouchState = TOUCH_STATE_SCROLLING;
593 | enableChildrenCache();
594 | cancelLongPress();
595 | }
596 | }
597 | break;
598 | case MotionEvent.ACTION_UP:
599 |
600 | //this must be here, in case no child view returns true,
601 | //events will propagate back here and on intercept touch event wont be called again
602 | //in case of no parent it propagates here, in case of parent it usually propagates to on cancel
603 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){
604 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y);
605 | if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown);
606 | mHandleSelectionOnActionUp = false;
607 | }
608 |
609 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates
610 | if (mTouchState == TOUCH_STATE_SCROLLING) {
611 |
612 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
613 | int initialXVelocity = (int) mVelocityTracker.getXVelocity();
614 | int initialYVelocity = (int) mVelocityTracker.getYVelocity();
615 |
616 | if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) {
617 | fling(-initialXVelocity, -initialYVelocity);
618 | }
619 | else{
620 | // Release the drag
621 | clearChildrenCache();
622 | mTouchState = TOUCH_STATE_RESTING;
623 |
624 | mDown.x = -1;
625 | mDown.y = -1;
626 | }
627 |
628 | if (mVelocityTracker != null) {
629 | mVelocityTracker.recycle();
630 | mVelocityTracker = null;
631 | }
632 |
633 | break;
634 | }
635 |
636 | // Release the drag
637 | clearChildrenCache();
638 | mTouchState = TOUCH_STATE_RESTING;
639 |
640 | mDown.x = -1;
641 | mDown.y = -1;
642 |
643 | break;
644 | case MotionEvent.ACTION_CANCEL:
645 | mTouchState = TOUCH_STATE_RESTING;
646 | }
647 |
648 | return true;
649 | }
650 |
651 |
652 | @Override
653 | public void computeScroll() {
654 | if (mScroller.computeScrollOffset()) {
655 | if(mScroller.getFinalX() == mScroller.getCurrX()){
656 | mScroller.abortAnimation();
657 | mTouchState = TOUCH_STATE_RESTING;
658 | mScrollDirection = NO_VALUE;
659 | clearChildrenCache();
660 | }
661 | else{
662 | final int x = mScroller.getCurrX();
663 | final int y = mScroller.getCurrY();
664 | scrollTo(x, y);
665 |
666 | postInvalidate();
667 | }
668 | }
669 | else if(mTouchState == TOUCH_STATE_FLING){
670 | mTouchState = TOUCH_STATE_RESTING;
671 | mScrollDirection = NO_VALUE;
672 | clearChildrenCache();
673 | }
674 |
675 | removeNonVisibleViews();
676 | if(mScrollDirection == DIRECTION_LEFT) refillLeftSide();
677 | if(mScrollDirection == DIRECTION_RIGHT) refillRightSide();
678 |
679 | if(mIsZOrderEnabled) rearrangeViewsAccordingZOrder();
680 | }
681 |
682 | public void fling(int velocityX, int velocityY){
683 | mTouchState = TOUCH_STATE_FLING;
684 | final int x = getScrollX();
685 | final int y = getScrollY();
686 | final int rightInPixels = dspToPx(mAdapter.getEnd());
687 | final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo;
688 |
689 | mScroller.fling(x, y, velocityX, velocityY, 0,rightInPixels - getWidth(),0,bottomInPixels - getHeight());
690 |
691 | if(velocityX < 0) {
692 | mScrollDirection = DIRECTION_LEFT;
693 | }
694 | else if(velocityX > 0) {
695 | mScrollDirection = DIRECTION_RIGHT;
696 | }
697 |
698 |
699 | invalidate();
700 | }
701 |
702 | protected void scrollByDelta(int deltaX, int deltaY){
703 | final int rightInPixels = dspToPx(mAdapter.getEnd());
704 | final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo;
705 | final int x = getScrollX() + deltaX;
706 | final int y = getScrollY() + deltaY;
707 |
708 | if(x < 0 ) deltaX -= x;
709 | else if(x > rightInPixels - getWidth()) deltaX -= x - (rightInPixels - getWidth());
710 |
711 | if(y < 0 ) deltaY -= y;
712 | else if(y > bottomInPixels - getHeight()) deltaY -= y - (bottomInPixels - getHeight());
713 |
714 | if(deltaX < 0) {
715 | mScrollDirection = DIRECTION_LEFT;
716 | }
717 | else {
718 | mScrollDirection = DIRECTION_RIGHT;
719 | }
720 |
721 | scrollBy(deltaX, deltaY);
722 | }
723 |
724 | protected void handleClick(Point p){
725 | final int c = getChildCount();
726 | View v;
727 | final Rect r = new Rect();
728 | for(int i=0; i < c; i++){
729 | v = getChildAt(i);
730 | v.getHitRect(r);
731 | if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){
732 | if(mItemClickListener != null) mItemClickListener.onItemClick(v);
733 | }
734 | }
735 | }
736 |
737 | /**
738 | * Returns current Adapter with backing data
739 | */
740 | public Adapter getAdapter() {
741 | return mAdapter;
742 | }
743 |
744 | /**
745 | * Set Adapter with backing data
746 | */
747 | public void setAdapter(Adapter adapter) {
748 | this.mAdapter = adapter;
749 | requestLayout();
750 | }
751 |
752 | /**
753 | * Set listener which will fire if item in container is clicked
754 | */
755 | public void setOnItemClickListener(OnItemClickListener itemClickListener) {
756 | this.mItemClickListener = itemClickListener;
757 | }
758 |
759 | private void enableChildrenCache() {
760 | setChildrenDrawingCacheEnabled(true);
761 | setChildrenDrawnWithCacheEnabled(true);
762 | }
763 |
764 | private void clearChildrenCache() {
765 | setChildrenDrawnWithCacheEnabled(false);
766 | }
767 |
768 | /**
769 | * In GRID_MODE_FIXED_SIZE mode has one dsp dimension set by setDspSize(), If band height is after transformation to normal pixels bigger than
770 | * available space, content becomes scrollable also vertically.
771 | *
772 | * In GRID_MODE_DYNAMIC_SIZE is dsp dimension computed from measured height and band height to always
773 | */
774 | public void setGridMode(int mode){
775 | mGridMode = mode;
776 | }
777 |
778 | /**
779 | * Specifies how many normal pixels is in length of one device specific pixel
780 | * This method is significant only in GRID_MODE_FIXED_SIZE mode (use setGridMode)
781 | */
782 | public void setDspSize(int pixels){
783 | mDspPixelRatio = pixels;
784 | }
785 |
786 | /**
787 | * Set to true if you want component to work with tile z parameter;
788 | * If you don't have any overlapping view, leave it on default false, because computing
789 | * with z order makes rendering slower.
790 | */
791 | public void setZOrderEnabled(boolean enable){
792 | mIsZOrderEnabled = enable;
793 | setChildrenDrawingOrderEnabled(enable);
794 | }
795 |
796 | @Override
797 | protected LayoutParams generateDefaultLayoutParams() {
798 | return new LayoutParams();
799 | }
800 |
801 | @Override
802 | protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
803 | return new LayoutParams();
804 | }
805 |
806 | @Override
807 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
808 | return p instanceof LayoutParams;
809 | }
810 |
811 | //----------------CONTENT BAND END--------------------------------------------------------------------
812 |
813 | public interface OnItemClickListener{
814 | void onItemClick(View v);
815 | }
816 |
817 | public interface Adapter {
818 |
819 | /**
820 | * Return Views which have left edge in device specific coordinates in range from-to,
821 | * @param from inclusive
822 | * @param to exclusive
823 | */
824 | public abstract View[] getViewsByLeftSideRange(int from, int to);
825 |
826 | /**
827 | * Return Views which have right edge in device specific coordinates in range from-to,
828 | * @param from exclusive
829 | * @param to inclusive
830 | */
831 | public abstract View[] getViewsByRightSideRange(int from, int to);
832 |
833 | /**
834 | * @return Right Coordinate of last tile in DSP
835 | */
836 | int getEnd();
837 |
838 | /**
839 | * @return Bottom Coordinate of tiles on bottom edge in DSP, Must be > 0
840 | *
841 | */
842 | int getBottom();
843 |
844 | /**
845 | * @return total number of tiles
846 | */
847 | public int getCount();
848 |
849 | /**
850 | * Makes union between View returned by left side and right side ranges
851 | * Needed for initialization of component
852 | */
853 | public abstract View[] getViewsVisibleInRange(int from, int to);
854 |
855 | /**
856 | * Puts View, which is not needed anymore back to Adapter. View will be used later instead of creating or inflating same view.
857 | */
858 | public void offerViewForRecycling(View view);
859 | }
860 |
861 |
862 | public static class LayoutParams extends ViewGroup.LayoutParams{
863 | public int tileId;
864 | public int dspLeft;
865 | public int dspTop;
866 | public int dspWidth;
867 | public int dspHeight;
868 | public int z;
869 |
870 | private int viewgroupIndex;
871 |
872 | public LayoutParams() {
873 | super(NO_VALUE, NO_VALUE);
874 | }
875 |
876 | public int getDspRight(){
877 | return dspLeft + dspWidth;
878 | }
879 | }
880 |
881 |
882 |
883 | public static abstract class AbstractAdapter implements Adapter{
884 | private final ViewCache mViewCache = new ViewCache();
885 |
886 | protected ArrayList mTilesByBegining;
887 | protected ArrayList mTilesByEnd;
888 | // protected SparseArray mTilesByNumber;
889 | protected IDataListener mChangeListener;
890 |
891 | public AbstractAdapter(){}
892 | public AbstractAdapter(ArrayList tiles){
893 | initWithNewData(tiles);
894 | }
895 |
896 | private final Comparator beginingComparator = new Comparator() {
897 | @Override
898 | public int compare(Tile o1, Tile o2) {
899 | if(o1.getX() == o2.getX()) return 0;
900 | else if(o1.getX() < o2.getX()) return -1;
901 | else return 1;
902 | }
903 | };
904 |
905 | private final Comparator endComparator = new Comparator() {
906 | @Override
907 | public int compare(Tile o1, Tile o2) {
908 | if(o1.getXRight() == o2.getXRight()) return 0;
909 | else if(o1.getXRight() < o2.getXRight()) return -1;
910 | else return 1;
911 | }
912 | };
913 |
914 | @SuppressWarnings("unchecked")
915 | @Override
916 | public void offerViewForRecycling(View view){
917 | mViewCache.cacheView((V) view);
918 | }
919 |
920 |
921 | /**
922 | * Use getLayoutParamsForTile to get correct layout params for Tile data and set them with setLayoutParams before returning View
923 | * @param t Tile data from datamodel
924 | * @param recycled View no more used and returned for recycling. Use together with ViewHolder pattern to avoid performance loss
925 | * in inflating and searching by ids in more complex xml layouts.
926 | * @return View which will be displayed in component using layout data from Tile
927 | *
928 | *
929 | * public ImageView getViewForTile(Tile t, ImageView recycled) {
930 | * ImageView iw;
931 | * if(recycled != null) iw = recycled;
932 | * else iw = new ImageView(MainActivity.this);
933 | *
934 | * iw.setLayoutParams(getLayoutParamsForTile(t));
935 | * return iw;
936 | * }
937 | *
938 | */
939 | public abstract V getViewForTile(Tile t, V recycled);
940 |
941 | /**
942 | * @return total number of tiles
943 | */
944 | public int getCount(){
945 | return mTilesByBegining.size();
946 | }
947 |
948 | public int getEnd(){
949 | if(mTilesByEnd.size() > 0)return mTilesByEnd.get(mTilesByEnd.size()-1).getXRight();
950 | else return 0;
951 | }
952 |
953 | private void checkAndFixLayoutParams(View v, Tile t){
954 | if(!(v.getLayoutParams() instanceof LayoutParams)) v.setLayoutParams(getLayoutParamsForTile(t));
955 | }
956 |
957 | @Override
958 | public View[] getViewsByLeftSideRange(int from, int to) {
959 | if(from == to) return new View[0];
960 | final List list = getTilesWithLeftRange(from, to);
961 |
962 | final View[] arr = new View[list.size()];
963 | for(int i=0; i < arr.length; i++){
964 | Tile t = list.get(i);
965 | arr[i] = getViewForTile(t, mViewCache.getCachedView());
966 | checkAndFixLayoutParams(arr[i], t);
967 | }
968 |
969 | return arr;
970 | }
971 |
972 | @Override
973 | public View[] getViewsByRightSideRange(int from, int to) {
974 | if(from == to) return new View[0];
975 | final List list = getTilesWithRightRange(from, to);
976 |
977 | final View[] arr = new View[list.size()];
978 | for(int i=0; i < arr.length; i++){
979 | Tile t = list.get(i);
980 | arr[i] = getViewForTile(t, mViewCache.getCachedView());
981 | checkAndFixLayoutParams(arr[i], t);
982 | }
983 |
984 | return arr;
985 | }
986 |
987 | public View[] getViewsVisibleInRange(int from, int to){
988 | final List listLeft = getTilesWithLeftRange(from, to);
989 | final List listRight = getTilesWithRightRange(from, to);
990 |
991 | ArrayList union = ToolBox.union(listLeft, listRight);
992 |
993 | final View[] arr = new View[union.size()];
994 | for(int i=0; i < arr.length; i++){
995 | Tile t = union.get(i);
996 | arr[i] = getViewForTile(t, mViewCache.getCachedView());
997 | checkAndFixLayoutParams(arr[i], t);
998 | }
999 |
1000 | return arr;
1001 | }
1002 |
1003 | public void setTiles(ArrayList tiles) {
1004 | initWithNewData(tiles);
1005 | if(mChangeListener != null) mChangeListener.onDataSetChanged();
1006 | }
1007 |
1008 | public void setDataChangeListener(IDataListener listener){
1009 | mChangeListener = listener;
1010 | }
1011 |
1012 | @SuppressWarnings("unchecked")
1013 | protected void initWithNewData(ArrayList tiles){
1014 | mTilesByBegining = (ArrayList) tiles.clone();
1015 |
1016 | Collections.sort(mTilesByBegining, beginingComparator);
1017 |
1018 | mTilesByEnd = (ArrayList) mTilesByBegining.clone();
1019 | Collections.sort(mTilesByEnd, endComparator);
1020 | }
1021 |
1022 | /**
1023 | * @param from inclusive
1024 | * @param to exclusive
1025 | */
1026 | public List getTilesWithLeftRange(int from, int to){
1027 | if(mTilesByBegining.size() == 0) return Collections.emptyList();
1028 | final int fromIndex = binarySearchLeftEdges(from);
1029 | if(mTilesByBegining.get(fromIndex).getX() > to) return Collections.emptyList();
1030 |
1031 | int i = fromIndex;
1032 | Tile t = mTilesByBegining.get(i);
1033 | while(t.getX() < to){
1034 | i++;
1035 | if(i < mTilesByBegining.size())t = mTilesByBegining.get(i);
1036 | else break;
1037 | }
1038 |
1039 | return mTilesByBegining.subList(fromIndex, i);
1040 | }
1041 |
1042 | /**
1043 | *
1044 | * @param from exclusive
1045 | * @param to inclusive
1046 | */
1047 | public List getTilesWithRightRange(int from, int to){
1048 | if(mTilesByEnd.size() == 0) return Collections.emptyList();
1049 |
1050 | final int fromIndex = binarySearchRightEdges(from + 1); //from is exclusive
1051 | final int fromRight = mTilesByEnd.get(fromIndex).getXRight();
1052 |
1053 | if(fromRight > to) return Collections.emptyList();
1054 |
1055 | int i = fromIndex;
1056 | Tile t = mTilesByEnd.get(i);
1057 | while(t.getXRight() <= to){
1058 | i++;
1059 | if(i < mTilesByEnd.size()) t = mTilesByEnd.get(i);
1060 | else break;
1061 | }
1062 |
1063 | return mTilesByEnd.subList(fromIndex, i);
1064 | }
1065 |
1066 | /** Continues to split same values until it rests on first of them
1067 | * returns first tile with left equal than value or greater
1068 | */
1069 | private int binarySearchLeftEdges(int value){
1070 | int lo = 0;
1071 | int hi = mTilesByBegining.size() - 1;
1072 | int mid = 0;
1073 | Tile t = null;
1074 | while (lo <= hi) {
1075 | // Key is in a[lo..hi] or not present.
1076 | mid = lo + (hi - lo) / 2;
1077 | t = mTilesByBegining.get(mid);
1078 |
1079 | if (value > t.getX()) lo = mid + 1;
1080 | else hi = mid - 1;
1081 |
1082 | }
1083 |
1084 | while(t != null && t.getX() < value && mid < mTilesByBegining.size()-1){
1085 | mid++;
1086 | t = mTilesByBegining.get(mid);
1087 | }
1088 |
1089 | return mid;
1090 | }
1091 |
1092 | /** Continues to split same values until it rests on first of them
1093 | * returns first tile with right equal than value or greater
1094 | */
1095 | private int binarySearchRightEdges(int value){
1096 | int lo = 0;
1097 | int hi = mTilesByEnd.size() - 1;
1098 | int mid = 0;
1099 | Tile t = null;
1100 | while (lo <= hi) {
1101 | // Key is in a[lo..hi] or not present.
1102 | mid = lo + (hi - lo) / 2;
1103 | t = mTilesByEnd.get(mid);
1104 |
1105 | final int r = t.getXRight();
1106 | if (value > r) lo = mid + 1;
1107 | else hi = mid - 1;
1108 | }
1109 |
1110 | while(t != null && t.getXRight() < value && mid < mTilesByEnd.size()-1){
1111 | mid++;
1112 | t = mTilesByEnd.get(mid);
1113 | }
1114 |
1115 | return mid;
1116 | }
1117 |
1118 |
1119 |
1120 | /**
1121 | * Use this in getViewForTile implementation to provide correctly initialized layout params for component
1122 | * @param t Tile data from datamodel
1123 | * @return ContendBand layout params
1124 | */
1125 | public LayoutParams getLayoutParamsForTile(Tile t){
1126 | LayoutParams lp = new LayoutParams();
1127 | lp.tileId = t.getId();
1128 | lp.dspLeft = t.getX();
1129 | lp.dspTop = t.getY();
1130 | lp.dspWidth = t.getWidth();
1131 | lp.dspHeight = t.getHeight();
1132 | lp.z = t.getZ();
1133 | return lp;
1134 | }
1135 |
1136 |
1137 | interface IDataListener {
1138 | void onDataSetChanged();
1139 | }
1140 |
1141 | }
1142 |
1143 | private static class ViewCache {
1144 | final LinkedList> mCachedItemViews = new LinkedList>();
1145 |
1146 | /**
1147 | * Check if list of weak references has any view still in memory to offer for recycling
1148 | * @return cached view
1149 | */
1150 | T getCachedView(){
1151 | if (mCachedItemViews.size() != 0) {
1152 | T v;
1153 | do{
1154 | v = mCachedItemViews.removeFirst().get();
1155 | }
1156 | while(v == null && mCachedItemViews.size() != 0);
1157 | return v;
1158 | }
1159 | return null;
1160 | }
1161 |
1162 | void cacheView(T v){
1163 | WeakReference ref = new WeakReference(v);
1164 | mCachedItemViews.addLast(ref);
1165 | }
1166 | }
1167 |
1168 | }
1169 |
--------------------------------------------------------------------------------
/library/src/main/java/com/martinappl/components/ui/containers/EndlessLoopAdapterContainer.java:
--------------------------------------------------------------------------------
1 | package com.martinappl.components.ui.containers;
2 |
3 |
4 | import java.lang.ref.WeakReference;
5 | import java.util.LinkedList;
6 |
7 | import android.content.Context;
8 | import android.content.res.TypedArray;
9 | import android.database.DataSetObserver;
10 | import android.graphics.Point;
11 | import android.graphics.Rect;
12 | import android.util.AttributeSet;
13 | import android.util.Log;
14 | import android.view.KeyEvent;
15 | import android.view.MotionEvent;
16 | import android.view.VelocityTracker;
17 | import android.view.View;
18 | import android.view.ViewConfiguration;
19 | import android.view.ViewDebug.CapturedViewProperty;
20 | import android.widget.Adapter;
21 | import android.widget.AdapterView;
22 | import android.widget.Scroller;
23 |
24 | import com.martinappl.components.R;
25 | import com.martinappl.components.general.ToolBox;
26 | import com.martinappl.components.ui.containers.interfaces.IViewObserver;
27 |
28 | /**
29 | *
30 | * @author Martin Appl
31 | *
32 | * Endless loop with items filling from adapter. Currently only horizontal orientation is implemented
33 | * View recycling in adapter is supported. You are encouraged to recycle view in adapter if possible
34 | *
35 | */
36 | public class EndlessLoopAdapterContainer extends AdapterView {
37 | /** Children added with this layout mode will be added after the last child */
38 | protected static final int LAYOUT_MODE_AFTER = 0;
39 |
40 | /** Children added with this layout mode will be added before the first child */
41 | protected static final int LAYOUT_MODE_TO_BEFORE = 1;
42 |
43 | protected static final int SCROLLING_DURATION = 500;
44 |
45 |
46 |
47 | /** The adapter providing data for container */
48 | protected Adapter mAdapter;
49 |
50 | /** The adaptor position of the first visible item */
51 | protected int mFirstItemPosition;
52 |
53 | /** The adaptor position of the last visible item */
54 | protected int mLastItemPosition;
55 |
56 | /** The adaptor position of selected item */
57 | protected int mSelectedPosition = INVALID_POSITION;
58 |
59 | /** Left of current most left child*/
60 | protected int mLeftChildEdge;
61 |
62 | /** User is not touching the list */
63 | protected static final int TOUCH_STATE_RESTING = 1;
64 |
65 | /** User is scrolling the list */
66 | protected static final int TOUCH_STATE_SCROLLING = 2;
67 |
68 | /** Fling gesture in progress */
69 | protected static final int TOUCH_STATE_FLING = 3;
70 |
71 | /** Aligning in progress */
72 | protected static final int TOUCH_STATE_ALIGN = 4;
73 |
74 | protected static final int TOUCH_STATE_DISTANCE_SCROLL = 5;
75 |
76 | /** A list of cached (re-usable) item views */
77 | protected final LinkedList> mCachedItemViews = new LinkedList>();
78 |
79 | /** If there is not enough items to fill adapter, this value is set to true and scrolling is disabled. Since all items from adapter are on screen*/
80 | protected boolean isSrollingDisabled = false;
81 |
82 | /** Whether content should be repeated when there is not enough items to fill container */
83 | protected boolean shouldRepeat = true;
84 |
85 | /** Position to scroll adapter only if is in endless mode. This is done after layout if we find out we are endless, we must relayout*/
86 | protected int mScrollPositionIfEndless = -1;
87 |
88 | private IViewObserver mViewObserver;
89 |
90 |
91 | protected int mTouchState = TOUCH_STATE_RESTING;
92 |
93 | protected final Scroller mScroller = new Scroller(getContext());
94 | private VelocityTracker mVelocityTracker;
95 | private boolean mDataChanged;
96 |
97 | private int mTouchSlop;
98 | private int mMinimumVelocity;
99 | private int mMaximumVelocity;
100 |
101 | private boolean mAllowLongPress;
102 | private float mLastMotionX;
103 | private float mLastMotionY;
104 | // private long mDownTime;
105 |
106 | private final Point mDown = new Point();
107 | private boolean mHandleSelectionOnActionUp = false;
108 | private boolean mInterceptTouchEvents;
109 | // private boolean mCancelInIntercept;
110 |
111 | protected OnItemClickListener mOnItemClickListener;
112 | protected OnItemSelectedListener mOnItemSelectedListener;
113 |
114 | public EndlessLoopAdapterContainer(Context context, AttributeSet attrs,
115 | int defStyle) {
116 | super(context, attrs, defStyle);
117 |
118 | final ViewConfiguration configuration = ViewConfiguration.get(context);
119 | mTouchSlop = configuration.getScaledTouchSlop();
120 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
121 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
122 |
123 | //init params from xml
124 | if(attrs != null){
125 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EndlessLoopAdapterContainer, defStyle, 0);
126 |
127 | shouldRepeat = a.getBoolean(R.styleable.EndlessLoopAdapterContainer_shouldRepeat, false);
128 |
129 | a.recycle();
130 | }
131 | }
132 |
133 | public EndlessLoopAdapterContainer(Context context, AttributeSet attrs) {
134 | this(context, attrs,0);
135 |
136 | }
137 |
138 | public EndlessLoopAdapterContainer(Context context) {
139 | this(context,null);
140 | }
141 |
142 | private final DataSetObserver fDataObserver = new DataSetObserver() {
143 |
144 | @Override
145 | public void onChanged() {
146 | synchronized(this){
147 | mDataChanged = true;
148 | }
149 | invalidate();
150 | }
151 |
152 | @Override
153 | public void onInvalidated() {
154 | mAdapter = null;
155 | }
156 | };
157 |
158 |
159 | /**
160 | * Params describing position of child view in container
161 | * in HORIZONTAL mode TOP,CENTER,BOTTOM are active in VERTICAL mode LEFT,CENTER,RIGHT are active
162 | * @author Martin Appl
163 | *
164 | */
165 | public static class LoopLayoutParams extends MarginLayoutParams{
166 | public static final int TOP = 0;
167 | public static final int CENTER = 1;
168 | public static final int BOTTOM = 2;
169 | public static final int LEFT = 3;
170 | public static final int RIGHT = 4;
171 |
172 | public int position;
173 | // public int actualWidth;
174 | // public int actualHeight;
175 |
176 | public LoopLayoutParams(int w, int h) {
177 | super(w, h);
178 | position = CENTER;
179 | }
180 |
181 | public LoopLayoutParams(int w, int h,int pos){
182 | super(w, h);
183 | position = pos;
184 | }
185 |
186 | public LoopLayoutParams(android.view.ViewGroup.LayoutParams lp) {
187 | super(lp);
188 |
189 | if(lp!=null && lp instanceof MarginLayoutParams){
190 | MarginLayoutParams mp = (MarginLayoutParams) lp;
191 | leftMargin = mp.leftMargin;
192 | rightMargin = mp.rightMargin;
193 | topMargin = mp.topMargin;
194 | bottomMargin = mp.bottomMargin;
195 | }
196 |
197 | position = CENTER;
198 | }
199 |
200 |
201 | }
202 |
203 | protected LoopLayoutParams createLayoutParams(int w, int h){
204 | return new LoopLayoutParams(w, h);
205 | }
206 |
207 | protected LoopLayoutParams createLayoutParams(int w, int h,int pos){
208 | return new LoopLayoutParams(w, h, pos);
209 | }
210 |
211 | protected LoopLayoutParams createLayoutParams(android.view.ViewGroup.LayoutParams lp){
212 | return new LoopLayoutParams(lp);
213 | }
214 |
215 |
216 | public boolean isRepeatable() {
217 | return shouldRepeat;
218 | }
219 |
220 | public boolean isEndlessRightNow(){
221 | return !isSrollingDisabled;
222 | }
223 |
224 | public void setShouldRepeat(boolean shouldRepeat) {
225 | this.shouldRepeat = shouldRepeat;
226 | }
227 |
228 | /**
229 | * Sets position in adapter of first shown item in container
230 | * @param position
231 | */
232 | public void scrollToPosition(int position){
233 | if(position < 0 || position >= mAdapter.getCount()) throw new IndexOutOfBoundsException("Position must be in bounds of adapter values count");
234 |
235 | reset();
236 | refillInternal(position-1, position);
237 | invalidate();
238 | }
239 |
240 | public void scrollToPositionIfEndless(int position){
241 | if(position < 0 || position >= mAdapter.getCount()) throw new IndexOutOfBoundsException("Position must be in bounds of adapter values count");
242 |
243 | if(isEndlessRightNow() && getChildCount() != 0){
244 | scrollToPosition(position);
245 | }
246 | else{
247 | mScrollPositionIfEndless = position;
248 | }
249 | }
250 |
251 | /**
252 | * Returns position to which will container scroll on next relayout
253 | * @return scroll position on next layout or -1 if it will scroll nowhere
254 | */
255 | public int getScrollPositionIfEndless(){
256 | return mScrollPositionIfEndless;
257 | }
258 |
259 | /**
260 | * Get index of currently first item in adapter
261 | * @return
262 | */
263 | public int getScrollPosition(){
264 | return mFirstItemPosition;
265 | }
266 |
267 | /**
268 | * Return offset by which is edge off first item moved off screen.
269 | * You can persist it and insert to setFirstItemOffset() to restore exact scroll position
270 | *
271 | * @return offset of first item, or 0 if there is not enough items to fill container and scrolling is disabled
272 | */
273 | public int getFirstItemOffset(){
274 | if(isSrollingDisabled) return 0;
275 | else return getScrollX() - mLeftChildEdge;
276 | }
277 |
278 | /**
279 | * Negative number. Offset by which is left edge of first item moved off screen.
280 | * @param offset
281 | */
282 | public void setFirstItemOffset(int offset){
283 | scrollTo(offset, 0);
284 | }
285 |
286 | @Override
287 | public Adapter getAdapter() {
288 | return mAdapter;
289 | }
290 |
291 | @Override
292 | public void setAdapter(Adapter adapter) {
293 | if(mAdapter != null) {
294 | mAdapter.unregisterDataSetObserver(fDataObserver);
295 | }
296 | mAdapter = adapter;
297 | mAdapter.registerDataSetObserver(fDataObserver);
298 |
299 | if(adapter instanceof IViewObserver){
300 | setViewObserver((IViewObserver) adapter);
301 | }
302 |
303 | reset();
304 | refill();
305 | invalidate();
306 | }
307 |
308 | @Override
309 | public View getSelectedView() {
310 | if(mSelectedPosition == INVALID_POSITION) return null;
311 |
312 | final int index;
313 | if(mFirstItemPosition > mSelectedPosition){
314 | index = mSelectedPosition + mAdapter.getCount() - mFirstItemPosition;
315 | }
316 | else{
317 | index = mSelectedPosition - mFirstItemPosition;
318 | }
319 | if(index < 0 || index >= getChildCount()) return null;
320 |
321 | return getChildAt(index);
322 | }
323 |
324 |
325 | /**
326 | * Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect
327 | */
328 | @Override
329 | public void setSelection(int position) {
330 | if(mAdapter == null) throw new IllegalStateException("You are trying to set selection on widget without adapter");
331 | if(mAdapter.getCount() == 0 && position == 0) position = -1;
332 | if(position < -1 || position > mAdapter.getCount()-1)
333 | throw new IllegalArgumentException("Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect");
334 |
335 | View v = getSelectedView();
336 | if(v != null) v.setSelected(false);
337 |
338 |
339 | final int oldPos = mSelectedPosition;
340 | mSelectedPosition = position;
341 |
342 | if(position == -1){
343 | if(mOnItemSelectedListener != null) mOnItemSelectedListener.onNothingSelected(this);
344 | return;
345 | }
346 |
347 | v = getSelectedView();
348 | if(v != null) v.setSelected(true);
349 |
350 | if(oldPos != mSelectedPosition && mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, mSelectedPosition, getSelectedItemId());
351 | }
352 |
353 |
354 | private void reset() {
355 | scrollTo(0, 0);
356 | removeAllViewsInLayout();
357 | mFirstItemPosition = 0;
358 | mLastItemPosition = -1;
359 | mLeftChildEdge = 0;
360 | }
361 |
362 |
363 | @Override
364 | public void computeScroll() {
365 | // if we don't have an adapter, we don't need to do anything
366 | if (mAdapter == null) {
367 | return;
368 | }
369 | if(mAdapter.getCount() == 0){
370 | return;
371 | }
372 |
373 | if (mScroller.computeScrollOffset()) {
374 | if(mScroller.getFinalX() == mScroller.getCurrX()){
375 | mScroller.abortAnimation();
376 | mTouchState = TOUCH_STATE_RESTING;
377 | if(!checkScrollPosition())
378 | clearChildrenCache();
379 | return;
380 | }
381 |
382 | int x = mScroller.getCurrX();
383 | scrollTo(x, 0);
384 |
385 | postInvalidate();
386 | }
387 | else if(mTouchState == TOUCH_STATE_FLING || mTouchState == TOUCH_STATE_DISTANCE_SCROLL){
388 | mTouchState = TOUCH_STATE_RESTING;
389 | if(!checkScrollPosition())
390 | clearChildrenCache();
391 | }
392 |
393 | if(mDataChanged){
394 | removeAllViewsInLayout();
395 | refillOnChange(mFirstItemPosition);
396 | return;
397 | }
398 |
399 | relayout();
400 | removeNonVisibleViews();
401 | refillRight();
402 | refillLeft();
403 |
404 | }
405 |
406 | /**
407 | *
408 | * @param velocityY The initial velocity in the Y direction. Positive
409 | * numbers mean that the finger/cursor is moving down the screen,
410 | * which means we want to scroll towards the top.
411 | * @param velocityX The initial velocity in the X direction. Positive
412 | * numbers mean that the finger/cursor is moving right the screen,
413 | * which means we want to scroll towards the top.
414 | */
415 | public void fling(int velocityX, int velocityY){
416 | mTouchState = TOUCH_STATE_FLING;
417 | final int x = getScrollX();
418 | final int y = getScrollY();
419 |
420 | mScroller.fling(x, y, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, Integer.MIN_VALUE,Integer.MAX_VALUE);
421 |
422 | invalidate();
423 | }
424 |
425 | /**
426 | * Scroll widget by given distance in pixels
427 | * @param dx
428 | */
429 | public void scroll(int dx){
430 | mScroller.startScroll(getScrollX(), 0, dx, 0, SCROLLING_DURATION);
431 | mTouchState = TOUCH_STATE_DISTANCE_SCROLL;
432 | invalidate();
433 | }
434 |
435 | @Override
436 | protected void onLayout(boolean changed, int left, int top, int right,
437 | int bottom) {
438 | super.onLayout(changed, left, top, right, bottom);
439 |
440 | // if we don't have an adapter, we don't need to do anything
441 | if (mAdapter == null) {
442 | return;
443 | }
444 |
445 | refillInternal(mLastItemPosition,mFirstItemPosition);
446 | }
447 |
448 | /**
449 | * Method for actualizing content after data change in adapter. It is expected container was emptied before
450 | * @param firstItemPosition
451 | */
452 | protected void refillOnChange(int firstItemPosition){
453 | refillInternal(firstItemPosition-1, firstItemPosition);
454 | }
455 |
456 |
457 | protected void refillInternal(final int lastItemPos,final int firstItemPos){
458 | // if we don't have an adapter, we don't need to do anything
459 | if (mAdapter == null) {
460 | return;
461 | }
462 | if(mAdapter.getCount() == 0){
463 | return;
464 | }
465 |
466 | if(getChildCount() == 0){
467 | fillFirstTime(lastItemPos, firstItemPos);
468 | }
469 | else{
470 | relayout();
471 | removeNonVisibleViews();
472 | refillRight();
473 | refillLeft();
474 | }
475 | }
476 |
477 | /**
478 | * Check if container visible area is filled and refill empty areas
479 | */
480 | private void refill(){
481 | scrollTo(0, 0);
482 | refillInternal(-1, 0);
483 | }
484 |
485 | // protected void measureChild(View child, LoopLayoutParams params){
486 | // //prepare spec for measurement
487 | // final int specW, specH;
488 | //
489 | // specW = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED), 0, params.width);
490 | // specH = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED), 0, params.height);
491 | //
492 | ////// final boolean useMeasuredW, useMeasuredH;
493 | //// if(params.height >= 0){
494 | //// specH = MeasureSpec.EXACTLY | params.height;
495 | ////// useMeasuredH = false;
496 | //// }
497 | //// else{
498 | //// if(params.height == LayoutParams.MATCH_PARENT){
499 | //// specH = MeasureSpec.EXACTLY | getHeight();
500 | //// params.height = getHeight();
501 | ////// useMeasuredH = false;
502 | //// }else{
503 | //// specH = MeasureSpec.AT_MOST | getHeight();
504 | ////// useMeasuredH = true;
505 | //// }
506 | //// }
507 | ////
508 | //// if(params.width >= 0){
509 | //// specW = MeasureSpec.EXACTLY | params.width;
510 | ////// useMeasuredW = false;
511 | //// }
512 | //// else{
513 | //// if(params.width == LayoutParams.MATCH_PARENT){
514 | //// specW = MeasureSpec.EXACTLY | getWidth();
515 | //// params.width = getWidth();
516 | ////// useMeasuredW = false;
517 | //// }else{
518 | //// specW = MeasureSpec.UNSPECIFIED;
519 | ////// useMeasuredW = true;
520 | //// }
521 | //// }
522 | //
523 | // //measure
524 | // child.measure(specW, specH);
525 | // //put measured values into layout params from where they will be used in layout.
526 | // //Use measured values only if exact values was not specified in layout params.
527 | //// if(useMeasuredH) params.actualHeight = child.getMeasuredHeight();
528 | //// else params.actualHeight = params.height;
529 | ////
530 | //// if(useMeasuredW) params.actualWidth = child.getMeasuredWidth();
531 | //// else params.actualWidth = params.width;
532 | // }
533 |
534 | protected void measureChild(View child){
535 | final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY);
536 | final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY);
537 | measureChild(child, pwms, phms);
538 | }
539 |
540 | private void relayout(){
541 | final int c = getChildCount();
542 | int left = mLeftChildEdge;
543 |
544 | View child;
545 | LoopLayoutParams lp;
546 | for(int i = 0; i < c; i++){
547 | child = getChildAt(i);
548 | lp = (LoopLayoutParams) child.getLayoutParams();
549 | measureChild(child);
550 |
551 | left = layoutChildHorizontal(child, left, lp);
552 | }
553 |
554 | }
555 |
556 |
557 | protected void fillFirstTime(final int lastItemPos,final int firstItemPos){
558 | final int leftScreenEdge = 0;
559 | final int rightScreenEdge = leftScreenEdge + getWidth();
560 |
561 | int right;
562 | int left;
563 | View child;
564 |
565 | boolean isRepeatingNow = false;
566 |
567 | //scrolling is enabled until we find out we don't have enough items
568 | isSrollingDisabled = false;
569 |
570 | mLastItemPosition = lastItemPos;
571 | mFirstItemPosition = firstItemPos;
572 | mLeftChildEdge = 0;
573 | right = mLeftChildEdge;
574 | left = mLeftChildEdge;
575 |
576 | while(right < rightScreenEdge){
577 | mLastItemPosition++;
578 |
579 | if(isRepeatingNow && mLastItemPosition >= firstItemPos) return;
580 |
581 | if(mLastItemPosition >= mAdapter.getCount()){
582 | if(firstItemPos == 0 && shouldRepeat) mLastItemPosition = 0;
583 | else{
584 | if(firstItemPos > 0){
585 | mLastItemPosition = 0;
586 | isRepeatingNow = true;
587 | }
588 | else if(!shouldRepeat){
589 | mLastItemPosition--;
590 | isSrollingDisabled = true;
591 | final int w = right-mLeftChildEdge;
592 | final int dx = (getWidth() - w)/2;
593 | scrollTo(-dx, 0);
594 | return;
595 | }
596 |
597 | }
598 | }
599 |
600 | if(mLastItemPosition >= mAdapter.getCount() ){
601 | Log.wtf("EndlessLoop", "mLastItemPosition > mAdapter.getCount()");
602 | return;
603 | }
604 |
605 | child = mAdapter.getView(mLastItemPosition, getCachedView(), this);
606 | child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER);
607 | left = layoutChildHorizontal(child, left, (LoopLayoutParams) child.getLayoutParams());
608 | right = child.getRight();
609 |
610 | //if selected view is going to screen, set selected state on him
611 | if(mLastItemPosition == mSelectedPosition){
612 | child.setSelected(true);
613 | }
614 |
615 | }
616 |
617 | if(mScrollPositionIfEndless > 0){
618 | final int p = mScrollPositionIfEndless;
619 | mScrollPositionIfEndless = -1;
620 | removeAllViewsInLayout();
621 | refillOnChange(p);
622 | }
623 | }
624 |
625 |
626 | /**
627 | * Checks and refills empty area on the right
628 | */
629 | protected void refillRight(){
630 | if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch
631 | if(getChildCount() == 0) return;
632 |
633 | final int leftScreenEdge = getScrollX();
634 | final int rightScreenEdge = leftScreenEdge + getWidth();
635 |
636 | View child = getChildAt(getChildCount() - 1);
637 | int right = child.getRight();
638 | int currLayoutLeft = right + ((LoopLayoutParams)child.getLayoutParams()).rightMargin;
639 | while(right < rightScreenEdge){
640 | mLastItemPosition++;
641 | if(mLastItemPosition >= mAdapter.getCount()) mLastItemPosition = 0;
642 |
643 | child = mAdapter.getView(mLastItemPosition, getCachedView(), this);
644 | child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER);
645 | currLayoutLeft = layoutChildHorizontal(child, currLayoutLeft, (LoopLayoutParams) child.getLayoutParams());
646 | right = child.getRight();
647 |
648 | //if selected view is going to screen, set selected state on him
649 | if(mLastItemPosition == mSelectedPosition){
650 | child.setSelected(true);
651 | }
652 | }
653 | }
654 |
655 | /**
656 | * Checks and refills empty area on the left
657 | */
658 | protected void refillLeft(){
659 | if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override first init to scrolling disabled by falling to this branch
660 | if(getChildCount() == 0) return;
661 |
662 | final int leftScreenEdge = getScrollX();
663 |
664 | View child = getChildAt(0);
665 | int childLeft = child.getLeft();
666 | int currLayoutRight = childLeft - ((LoopLayoutParams)child.getLayoutParams()).leftMargin;
667 | while(currLayoutRight > leftScreenEdge){
668 | mFirstItemPosition--;
669 | if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1;
670 |
671 | child = mAdapter.getView(mFirstItemPosition, getCachedView(), this);
672 | child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE);
673 | currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams());
674 | childLeft = child.getLeft() - ((LoopLayoutParams)child.getLayoutParams()).leftMargin;
675 | //update left edge of children in container
676 | mLeftChildEdge = childLeft;
677 |
678 | //if selected view is going to screen, set selected state on him
679 | if(mFirstItemPosition == mSelectedPosition){
680 | child.setSelected(true);
681 | }
682 | }
683 | }
684 |
685 | // /**
686 | // * Checks and refills empty area on the left
687 | // */
688 | // protected void refillLeft(){
689 | // if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch
690 | // final int leftScreenEdge = getScrollX();
691 | //
692 | // View child = getChildAt(0);
693 | // int currLayoutRight = child.getRight();
694 | // while(currLayoutRight > leftScreenEdge){
695 | // mFirstItemPosition--;
696 | // if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1;
697 | //
698 | // child = mAdapter.getView(mFirstItemPosition, getCachedView(mFirstItemPosition), this);
699 | // child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE);
700 | // currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams());
701 | //
702 | // //update left edge of children in container
703 | // mLeftChildEdge = child.getLeft();
704 | //
705 | // //if selected view is going to screen, set selected state on him
706 | // if(mFirstItemPosition == mSelectedPosition){
707 | // child.setSelected(true);
708 | // }
709 | // }
710 | // }
711 |
712 | /**
713 | * Removes view that are outside of the visible part of the list. Will not
714 | * remove all views.
715 | */
716 | protected void removeNonVisibleViews() {
717 | if(getChildCount() == 0) return;
718 |
719 | final int leftScreenEdge = getScrollX();
720 | final int rightScreenEdge = leftScreenEdge + getWidth();
721 |
722 | // check if we should remove any views in the left
723 | View firstChild = getChildAt(0);
724 | final int leftedge = firstChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin;
725 | if(leftedge != mLeftChildEdge) throw new IllegalStateException("firstChild.getLeft() != mLeftChildEdge");
726 | while (firstChild != null && firstChild.getRight() + ((LoopLayoutParams)firstChild.getLayoutParams()).rightMargin < leftScreenEdge) {
727 | //if selected view is going off screen, remove selected state
728 | firstChild.setSelected(false);
729 |
730 | // remove view
731 | removeViewInLayout(firstChild);
732 |
733 | if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(firstChild, mFirstItemPosition);
734 | WeakReference ref = new WeakReference(firstChild);
735 | mCachedItemViews.addLast(ref);
736 |
737 | mFirstItemPosition++;
738 | if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0;
739 |
740 | // update left item position
741 | mLeftChildEdge = getChildAt(0).getLeft() - ((LoopLayoutParams)getChildAt(0).getLayoutParams()).leftMargin;
742 |
743 | // Continue to check the next child only if we have more than
744 | // one child left
745 | if (getChildCount() > 1) {
746 | firstChild = getChildAt(0);
747 | } else {
748 | firstChild = null;
749 | }
750 | }
751 |
752 | // check if we should remove any views in the right
753 | View lastChild = getChildAt(getChildCount() - 1);
754 | while (lastChild != null && firstChild!=null && lastChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin > rightScreenEdge) {
755 | //if selected view is going off screen, remove selected state
756 | lastChild.setSelected(false);
757 |
758 | // remove the right view
759 | removeViewInLayout(lastChild);
760 |
761 | if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(lastChild, mLastItemPosition);
762 | WeakReference ref = new WeakReference(lastChild);
763 | mCachedItemViews.addLast(ref);
764 |
765 | mLastItemPosition--;
766 | if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1;
767 |
768 | // Continue to check the next child only if we have more than
769 | // one child left
770 | if (getChildCount() > 1) {
771 | lastChild = getChildAt(getChildCount() - 1);
772 | } else {
773 | lastChild = null;
774 | }
775 | }
776 | }
777 |
778 |
779 | /**
780 | * Adds a view as a child view and takes care of measuring it
781 | *
782 | * @param child The view to add
783 | * @param layoutMode Either LAYOUT_MODE_LEFT or LAYOUT_MODE_RIGHT
784 | * @return child which was actually added to container, subclasses can override to introduce frame views
785 | */
786 | protected View addAndMeasureChildHorizontal(final View child, final int layoutMode) {
787 | LayoutParams lp = child.getLayoutParams();
788 | LoopLayoutParams params;
789 | if (lp == null) {
790 | params = createLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
791 | }
792 | else{
793 | if(lp!=null && lp instanceof LoopLayoutParams) params = (LoopLayoutParams) lp;
794 | else params = createLayoutParams(lp);
795 | }
796 | final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1;
797 | addViewInLayout(child, index, params, true);
798 |
799 | measureChild(child);
800 | child.setDrawingCacheEnabled(true);
801 |
802 | return child;
803 | }
804 |
805 |
806 |
807 | /**
808 | * Layouts children from left to right
809 | * @param left positon for left edge in parent container
810 | * @param lp layout params
811 | * @return new left
812 | */
813 | protected int layoutChildHorizontal(View v,int left, LoopLayoutParams lp){
814 | int l,t,r,b;
815 |
816 | switch(lp.position){
817 | case LoopLayoutParams.TOP:
818 | l = left + lp.leftMargin;
819 | t = lp.topMargin;
820 | r = l + v.getMeasuredWidth();
821 | b = t + v.getMeasuredHeight();
822 | break;
823 | case LoopLayoutParams.BOTTOM:
824 | b = getHeight() - lp.bottomMargin;
825 | t = b - v.getMeasuredHeight();
826 | l = left + lp.leftMargin;
827 | r = l + v.getMeasuredWidth();
828 | break;
829 | case LoopLayoutParams.CENTER:
830 | l = left + lp.leftMargin;
831 | r = l + v.getMeasuredWidth();
832 | final int x = (getHeight() - v.getMeasuredHeight())/2;
833 | t = x;
834 | b = t + v.getMeasuredHeight();
835 | break;
836 | default:
837 | throw new RuntimeException("Only TOP,BOTTOM,CENTER are alowed in horizontal orientation");
838 | }
839 |
840 |
841 | v.layout(l, t, r, b);
842 | return r + lp.rightMargin;
843 | }
844 |
845 | /**
846 | * Layout children from right to left
847 | */
848 | protected int layoutChildHorizontalToBefore(View v,int right , LoopLayoutParams lp){
849 | final int left = right - v.getMeasuredWidth() - lp.leftMargin - lp.rightMargin;
850 | layoutChildHorizontal(v, left, lp);
851 | return left;
852 | }
853 |
854 | /**
855 | * Allows to make scroll alignments
856 | * @return true if invalidate() was issued, and container is going to scroll
857 | */
858 | protected boolean checkScrollPosition(){
859 | return false;
860 | }
861 |
862 | @Override
863 | public boolean onInterceptTouchEvent(MotionEvent ev) {
864 |
865 | /*
866 | * This method JUST determines whether we want to intercept the motion.
867 | * If we return true, onTouchEvent will be called and we do the actual
868 | * scrolling there.
869 | */
870 |
871 |
872 | /*
873 | * Shortcut the most recurring case: the user is in the dragging
874 | * state and he is moving his finger. We want to intercept this
875 | * motion.
876 | */
877 | final int action = ev.getAction();
878 | if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) {
879 | return true;
880 | }
881 |
882 | final float x = ev.getX();
883 | final float y = ev.getY();
884 | switch (action) {
885 | case MotionEvent.ACTION_MOVE:
886 | //if we have scrolling disabled, we don't do anything
887 | if(!shouldRepeat && isSrollingDisabled) return false;
888 |
889 | /*
890 | * not dragging, otherwise the shortcut would have caught it. Check
891 | * whether the user has moved far enough from his original down touch.
892 | */
893 |
894 | /*
895 | * Locally do absolute value. mLastMotionX is set to the x value
896 | * of the down event.
897 | */
898 | final int xDiff = (int) Math.abs(x - mLastMotionX);
899 | final int yDiff = (int) Math.abs(y - mLastMotionY);
900 |
901 | final int touchSlop = mTouchSlop;
902 | final boolean xMoved = xDiff > touchSlop;
903 | final boolean yMoved = yDiff > touchSlop;
904 |
905 | if (xMoved) {
906 |
907 | // Scroll if the user moved far enough along the X axis
908 | mTouchState = TOUCH_STATE_SCROLLING;
909 | mHandleSelectionOnActionUp = false;
910 | enableChildrenCache();
911 |
912 | // Either way, cancel any pending longpress
913 | if (mAllowLongPress) {
914 | mAllowLongPress = false;
915 | // Try canceling the long press. It could also have been scheduled
916 | // by a distant descendant, so use the mAllowLongPress flag to block
917 | // everything
918 | cancelLongPress();
919 | }
920 | }
921 | if(yMoved){
922 | mHandleSelectionOnActionUp = false;
923 | if (mAllowLongPress) {
924 | mAllowLongPress = false;
925 | cancelLongPress();
926 | }
927 | }
928 | break;
929 |
930 | case MotionEvent.ACTION_DOWN:
931 | // Remember location of down touch
932 | mLastMotionX = x;
933 | mLastMotionY = y;
934 | mAllowLongPress = true;
935 | // mCancelInIntercept = false;
936 |
937 | mDown.x = (int) x;
938 | mDown.y = (int) y;
939 |
940 | /*
941 | * If being flinged and user touches the screen, initiate drag;
942 | * otherwise don't. mScroller.isFinished should be false when
943 | * being flinged.
944 | */
945 | mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING;
946 | //if he had normal click in rested state, remember for action up check
947 | if(mTouchState == TOUCH_STATE_RESTING){
948 | mHandleSelectionOnActionUp = true;
949 | }
950 | break;
951 |
952 | case MotionEvent.ACTION_CANCEL:
953 | mDown.x = -1;
954 | mDown.y = -1;
955 | // mCancelInIntercept = true;
956 | break;
957 | case MotionEvent.ACTION_UP:
958 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates
959 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){
960 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y);
961 | if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown);
962 | }
963 | // Release the drag
964 | mAllowLongPress = false;
965 | mHandleSelectionOnActionUp = false;
966 | mDown.x = -1;
967 | mDown.y = -1;
968 | if(mTouchState == TOUCH_STATE_SCROLLING){
969 | if(checkScrollPosition()){
970 | break;
971 | }
972 | }
973 | mTouchState = TOUCH_STATE_RESTING;
974 | clearChildrenCache();
975 | break;
976 | }
977 |
978 | mInterceptTouchEvents = mTouchState == TOUCH_STATE_SCROLLING;
979 | return mInterceptTouchEvents;
980 |
981 | }
982 |
983 | // /**
984 | // * Allow subclasses to override this to always intercept events
985 | // * @return
986 | // */
987 | // protected boolean interceptEvents(){
988 | // /*
989 | // * The only time we want to intercept motion events is if we are in the
990 | // * drag mode.
991 | // */
992 | // return mTouchState == TOUCH_STATE_SCROLLING;
993 | // }
994 |
995 | protected void handleClick(Point p){
996 | final int c = getChildCount();
997 | View v;
998 | final Rect r = new Rect();
999 | for(int i=0; i < c; i++){
1000 | v = getChildAt(i);
1001 | v.getHitRect(r);
1002 | if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){
1003 | final View old = getSelectedView();
1004 | if(old != null) old.setSelected(false);
1005 |
1006 | int position = mFirstItemPosition + i;
1007 | if(position >= mAdapter.getCount()) position = position - mAdapter.getCount();
1008 |
1009 |
1010 | mSelectedPosition = position;
1011 | v.setSelected(true);
1012 |
1013 | if(mOnItemClickListener != null) mOnItemClickListener.onItemClick(this, v, position , getItemIdAtPosition(position));
1014 | if(mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, position, getItemIdAtPosition(position));
1015 |
1016 | break;
1017 | }
1018 | }
1019 | }
1020 |
1021 |
1022 | @Override
1023 | public boolean onTouchEvent(MotionEvent event) {
1024 | // if we don't have an adapter, we don't need to do anything
1025 | if (mAdapter == null) {
1026 | return false;
1027 | }
1028 |
1029 |
1030 |
1031 | if (mVelocityTracker == null) {
1032 | mVelocityTracker = VelocityTracker.obtain();
1033 | }
1034 | mVelocityTracker.addMovement(event);
1035 |
1036 | final int action = event.getAction();
1037 | final float x = event.getX();
1038 | final float y = event.getY();
1039 |
1040 | switch (action) {
1041 | case MotionEvent.ACTION_DOWN:
1042 | /*
1043 | * If being flinged and user touches, stop the fling. isFinished
1044 | * will be false if being flinged.
1045 | */
1046 | if (!mScroller.isFinished()) {
1047 | mScroller.forceFinished(true);
1048 | }
1049 |
1050 | // Remember where the motion event started
1051 | mLastMotionX = x;
1052 | mLastMotionY = y;
1053 |
1054 | break;
1055 | case MotionEvent.ACTION_MOVE:
1056 | //if we have scrolling disabled, we don't do anything
1057 | if(!shouldRepeat && isSrollingDisabled) return false;
1058 |
1059 | if (mTouchState == TOUCH_STATE_SCROLLING) {
1060 | // Scroll to follow the motion event
1061 | final int deltaX = (int) (mLastMotionX - x);
1062 | mLastMotionX = x;
1063 | mLastMotionY = y;
1064 |
1065 | int sx = getScrollX() + deltaX;
1066 |
1067 | scrollTo(sx, 0);
1068 |
1069 | }
1070 | else{
1071 | final int xDiff = (int) Math.abs(x - mLastMotionX);
1072 |
1073 | final int touchSlop = mTouchSlop;
1074 | final boolean xMoved = xDiff > touchSlop;
1075 |
1076 |
1077 | if (xMoved) {
1078 |
1079 | // Scroll if the user moved far enough along the X axis
1080 | mTouchState = TOUCH_STATE_SCROLLING;
1081 | enableChildrenCache();
1082 |
1083 | // Either way, cancel any pending longpress
1084 | if (mAllowLongPress) {
1085 | mAllowLongPress = false;
1086 | // Try canceling the long press. It could also have been scheduled
1087 | // by a distant descendant, so use the mAllowLongPress flag to block
1088 | // everything
1089 | cancelLongPress();
1090 | }
1091 | }
1092 | }
1093 | break;
1094 | case MotionEvent.ACTION_UP:
1095 |
1096 | //this must be here, in case no child view returns true,
1097 | //events will propagate back here and on intercept touch event wont be called again
1098 | //in case of no parent it propagates here, in case of parent it usualy propagates to on cancel
1099 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){
1100 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y);
1101 | if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown);
1102 | mHandleSelectionOnActionUp = false;
1103 | }
1104 |
1105 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates
1106 | if (mTouchState == TOUCH_STATE_SCROLLING) {
1107 |
1108 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1109 | int initialXVelocity = (int) mVelocityTracker.getXVelocity();
1110 | int initialYVelocity = (int) mVelocityTracker.getYVelocity();
1111 |
1112 | if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) {
1113 | fling(-initialXVelocity, -initialYVelocity);
1114 | }
1115 | else{
1116 | // Release the drag
1117 | clearChildrenCache();
1118 | mTouchState = TOUCH_STATE_RESTING;
1119 | checkScrollPosition();
1120 | mAllowLongPress = false;
1121 |
1122 | mDown.x = -1;
1123 | mDown.y = -1;
1124 | }
1125 |
1126 | if (mVelocityTracker != null) {
1127 | mVelocityTracker.recycle();
1128 | mVelocityTracker = null;
1129 | }
1130 |
1131 | break;
1132 | }
1133 |
1134 | // Release the drag
1135 | clearChildrenCache();
1136 | mTouchState = TOUCH_STATE_RESTING;
1137 | mAllowLongPress = false;
1138 |
1139 | mDown.x = -1;
1140 | mDown.y = -1;
1141 |
1142 | break;
1143 | case MotionEvent.ACTION_CANCEL:
1144 |
1145 | //this must be here, in case no child view returns true,
1146 | //events will propagate back here and on intercept touch event wont be called again
1147 | //instead we get cancel here, since we stated we shouldn't intercept events and propagate them to children
1148 | //but events propagated back here, because no child was interested
1149 | // if(!mInterceptTouchEvents && mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){
1150 | // handleClick(mDown);
1151 | // mHandleSelectionOnActionUp = false;
1152 | // }
1153 |
1154 | mAllowLongPress = false;
1155 |
1156 | mDown.x = -1;
1157 | mDown.y = -1;
1158 |
1159 | if(mTouchState == TOUCH_STATE_SCROLLING){
1160 | if(checkScrollPosition()){
1161 | break;
1162 | }
1163 | }
1164 |
1165 | mTouchState = TOUCH_STATE_RESTING;
1166 | }
1167 |
1168 | return true;
1169 | }
1170 |
1171 | @Override
1172 | public boolean onKeyDown(int keyCode, KeyEvent event) {
1173 | switch (keyCode) {
1174 | case KeyEvent.KEYCODE_DPAD_LEFT:
1175 | checkScrollFocusLeft();
1176 | break;
1177 | case KeyEvent.KEYCODE_DPAD_RIGHT:
1178 | checkScrollFocusRight();
1179 | break;
1180 | default:
1181 | break;
1182 | }
1183 |
1184 | return super.onKeyDown(keyCode, event);
1185 | }
1186 |
1187 | /**
1188 | * Moves with scroll window if focus hits one view before end of screen
1189 | */
1190 | private void checkScrollFocusLeft(){
1191 | final View focused = getFocusedChild();
1192 | if(getChildCount() >= 2 ){
1193 | View second = getChildAt(1);
1194 | View first = getChildAt(0);
1195 |
1196 | if(focused == second){
1197 | scroll(-first.getWidth());
1198 | }
1199 | }
1200 | }
1201 |
1202 | private void checkScrollFocusRight(){
1203 | final View focused = getFocusedChild();
1204 | if(getChildCount() >= 2 ){
1205 | View last = getChildAt(getChildCount()-1);
1206 | View lastButOne = getChildAt(getChildCount()-2);
1207 |
1208 | if(focused == lastButOne){
1209 | scroll(last.getWidth());
1210 | }
1211 | }
1212 | }
1213 |
1214 | /**
1215 | * Check if list of weak references has any view still in memory to offer for recyclation
1216 | * @return cached view
1217 | */
1218 | protected View getCachedView(){
1219 | if (mCachedItemViews.size() != 0) {
1220 | View v;
1221 | do{
1222 | v = mCachedItemViews.removeFirst().get();
1223 | }
1224 | while(v == null && mCachedItemViews.size() != 0);
1225 | return v;
1226 | }
1227 | return null;
1228 | }
1229 |
1230 | protected void enableChildrenCache() {
1231 | setChildrenDrawnWithCacheEnabled(true);
1232 | setChildrenDrawingCacheEnabled(true);
1233 | }
1234 |
1235 | protected void clearChildrenCache() {
1236 | setChildrenDrawnWithCacheEnabled(false);
1237 | }
1238 |
1239 | @Override
1240 | public void setOnItemClickListener(
1241 | android.widget.AdapterView.OnItemClickListener listener) {
1242 | mOnItemClickListener = listener;
1243 | }
1244 |
1245 | @Override
1246 | public void setOnItemSelectedListener(
1247 | android.widget.AdapterView.OnItemSelectedListener listener) {
1248 | mOnItemSelectedListener = listener;
1249 | }
1250 |
1251 | @Override
1252 | @CapturedViewProperty
1253 | public int getSelectedItemPosition() {
1254 | return mSelectedPosition;
1255 | }
1256 |
1257 | /**
1258 | * Only set value for selection position field, no gui updates are done
1259 | * for setting selection with gui updates and callback calls use setSelection
1260 | * @param position
1261 | */
1262 | public void setSeletedItemPosition(int position){
1263 | if(mAdapter.getCount() == 0 && position == 0) position = -1;
1264 | if(position < -1 || position > mAdapter.getCount()-1)
1265 | throw new IllegalArgumentException("Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect");
1266 |
1267 | mSelectedPosition = position;
1268 | }
1269 |
1270 | @Override
1271 | @CapturedViewProperty
1272 | public long getSelectedItemId() {
1273 | return mAdapter.getItemId(mSelectedPosition);
1274 | }
1275 |
1276 | @Override
1277 | public Object getSelectedItem() {
1278 | return getSelectedView();
1279 | }
1280 |
1281 | @Override
1282 | @CapturedViewProperty
1283 | public int getCount() {
1284 | if(mAdapter != null) return mAdapter.getCount();
1285 | else return 0;
1286 | }
1287 |
1288 | @Override
1289 | public int getPositionForView(View view) {
1290 | final int c = getChildCount();
1291 | View v;
1292 | for(int i = 0; i < c; i++){
1293 | v = getChildAt(i);
1294 | if(v == view) return mFirstItemPosition + i;
1295 | }
1296 | return INVALID_POSITION;
1297 | }
1298 |
1299 | @Override
1300 | public int getFirstVisiblePosition() {
1301 | return mFirstItemPosition;
1302 | }
1303 |
1304 | @Override
1305 | public int getLastVisiblePosition() {
1306 | return mLastItemPosition;
1307 | }
1308 |
1309 | @Override
1310 | public Object getItemAtPosition(int position) {
1311 | final int index;
1312 | if(mFirstItemPosition > position){
1313 | index = position + mAdapter.getCount() - mFirstItemPosition;
1314 | }
1315 | else{
1316 | index = position - mFirstItemPosition;
1317 | }
1318 | if(index < 0 || index >= getChildCount()) return null;
1319 |
1320 | return getChildAt(index);
1321 | }
1322 |
1323 | @Override
1324 | public long getItemIdAtPosition(int position) {
1325 | return mAdapter.getItemId(position);
1326 | }
1327 |
1328 | @Override
1329 | public boolean performItemClick(View view, int position, long id) {
1330 | throw new UnsupportedOperationException();
1331 | }
1332 |
1333 |
1334 | public void setViewObserver(IViewObserver viewObserver) {
1335 | this.mViewObserver = viewObserver;
1336 | }
1337 |
1338 |
1339 | }
1340 |
1341 |
1342 |
--------------------------------------------------------------------------------